Compare commits

..

91 Commits

Author SHA1 Message Date
Kacper
3559e0104c feat: add token consumption tracking per task
- Add TokenUsage interface to track input/output tokens and cost
- Capture usage from Claude SDK result messages in auto-mode-service
- Accumulate token usage across multiple streams and follow-up sessions
- Store token usage in feature.json for persistence
- Display token count and cost on Kanban cards with tooltip breakdown
- Add token usage summary header in log viewer

Closes #166

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 03:05:13 +01:00
Web Dev Cody
d104a24446 Merge pull request #148 from AutoMaker-Org/refactor/frontend
refactor: migrate frontend from next.js to vite + tanStack router
2025-12-19 16:44:53 -05:00
Cody Seibert
a26ef4347a refactor: remove CoursePromoBadge component and related settings
- Deleted the CoursePromoBadge component from the sidebar and its associated logic.
- Removed references to the hideMarketingContent setting from the settings view and appearance section.
- Cleaned up related tests for marketing content visibility as they are no longer applicable.
2025-12-19 16:22:03 -05:00
Cody Seibert
e9dba8c9e5 refactor: update kanban responsive scaling tests to adjust column width bounds and improve margin calculations
- Changed minimum column width from 240px to 280px to better align with design requirements.
- Enhanced margin calculations to account for the actual container width and sidebar positioning, ensuring more accurate layout testing.
2025-12-19 15:57:36 -05:00
Cody Seibert
2b02db8ae3 fixing the package locks to not use ssh 2025-12-19 14:45:23 -05:00
Cody Seibert
1ad3b1739b Merge branch 'main' into refactor/frontend 2025-12-19 14:42:31 -05:00
trueheads
17a2053e79 e2e fixes 2025-12-18 21:01:14 -06:00
Web Dev Cody
46c3dd252f Merge pull request #168 from AutoMaker-Org/feature/cards-in-worktrees
feat: add branch card counts to UI components
2025-12-18 21:43:45 -05:00
trueheads
96b0e74794 build error fix 2025-12-18 20:38:59 -06:00
Cody Seibert
18a2ed2a44 feat: implement initial commit creation for empty git repositories
- Added a function to ensure that a git repository has at least one commit before executing worktree commands. This function creates an empty initial commit with a predefined message if the repository is empty.
- Updated the create route handler to call this function, ensuring smooth operation when adding worktrees to repositories without existing commits.
- Introduced integration tests to verify the creation of the initial commit when no commits are present in the repository.
2025-12-18 21:36:50 -05:00
trueheads
7d6ed0cb37 Merge branch 'refactor/frontend' of https://github.com/AutoMaker-Org/automaker into refactor/frontend 2025-12-18 20:33:24 -06:00
trueheads
396100686c feat: When clicking on the spec editor tab, get this network er... 2025-12-18 20:25:45 -06:00
trueheads
35ecb0dd2d feat: I'm noticing that when the application is launched, it do... 2025-12-18 20:16:33 -06:00
trueheads
8d6dae7495 feat: Add a settings toggle to disable marketing content within... 2025-12-18 19:00:17 -06:00
Cody Seibert
340e76c3ed fix: handle null branch names in AutoModeService
- Updated branchName assignment to use nullish coalescing, ensuring that unassigned features are correctly set to null instead of an empty string. This change improves the handling of feature states during the update process.
2025-12-18 19:55:43 -05:00
Cody Seibert
275037c73d test: update worktree integration tests for feature branch handling
- Modified test descriptions to clarify when worktrees are created during feature addition and editing.
- Updated assertions to verify that worktrees and branches are created as expected when features are added or edited.
- Enhanced test logic to ensure accurate verification of worktree existence and branch creation, reflecting recent changes in worktree management.
2025-12-18 19:46:10 -05:00
trueheads
a14ef30c69 feat: In the Agent Runner tab, in an Agent Conversation, when s... 2025-12-18 18:33:56 -06:00
Cody Seibert
18fa0f3066 refactor: streamline session and board view components
- Consolidated imports in session-manager.tsx for cleaner code.
- Improved state initialization formatting for better readability.
- Updated board-view.tsx to enhance feature management, including the use of refs to track running tasks and prevent unnecessary effect re-runs.
- Added affectedFeatureCount prop to DeleteWorktreeDialog for better user feedback on feature assignments.
- Refactored useBoardActions to ensure worktrees are created when features are added or updated, improving overall workflow efficiency.
2025-12-18 19:24:19 -05:00
trueheads
3e015591d3 feat: I need you to review all styling for button, menu, log vi... 2025-12-18 18:23:50 -06:00
Cody Seibert
81d631ea72 fix: address PR review comments
- Fix branch fallback logic: use nullish coalescing (??) instead of || to handle empty strings correctly (empty string represents unassigned features, not main branch)
- Refactor branchCardCounts calculation: use reduce instead of forEach for better conciseness and readability
- Fix badge semantics in BranchAutocomplete: check branchCardCounts !== undefined first to ensure numeric badges (including 0) only appear when actual count data exists, while 'default' is reserved for when count data is unavailable
2025-12-18 17:53:37 -05:00
Kacper
2a1ab218ec Merge remote-tracking branch 'origin/main' into refactor/frontend
# Conflicts:
#	apps/ui/src/components/views/terminal-view/terminal-panel.tsx
2025-12-18 23:41:00 +01:00
Web Dev Cody
6c31f725ff Merge pull request #167 from AutoMaker-Org/add-copy-paste-terminals
feat: implement context menu for terminal actions
2025-12-18 17:35:33 -05:00
Cody Seibert
f0bea76141 feat: add branch card counts to UI components
- Introduced branchCardCounts prop to various components to display unarchived card counts per branch.
- Updated BranchAutocomplete, BoardView, AddFeatureDialog, EditFeatureDialog, BranchSelector, WorktreePanel, and WorktreeTab to utilize the new prop for enhanced branch management visibility.
- Enhanced user experience by showing card counts alongside branch names in relevant UI elements.
2025-12-18 17:34:37 -05:00
Alec Koifman
46933a2a81 refactor: synchronize focused menu index with refs for improved keyboard navigation
- Introduced a ref to keep the focused menu index in sync with state, enhancing keyboard navigation within the terminal context menu.
- Updated event handlers to utilize the ref for managing focus, ensuring consistent behavior during menu interactions.
- Simplified dependencies in the effect hook for better performance and clarity.
2025-12-18 17:12:49 -05:00
Alec Koifman
dccb5faa4b fix: improve terminal context menu positioning and platform detection
- Enhanced context menu positioning with boundary checks to prevent overflow on screen edges.
- Updated platform detection logic for Mac users to utilize modern userAgentData API with a fallback to deprecated navigator.platform.
- Ensured context menu opens correctly within viewport limits, improving user experience.
2025-12-18 17:02:57 -05:00
Alec Koifman
15ae1fe147 feat: enhance terminal context menu with keyboard navigation
- Improved context menu functionality by adding keyboard navigation support for actions (copy, paste, select all, clear).
- Utilized refs to manage focus on menu items and updated platform detection for Mac users.
- Ensured context menu closes on outside clicks and handles keyboard events effectively.
2025-12-18 16:54:19 -05:00
Alec Koifman
6f82f64195 feat: implement context menu for terminal actions
- Added context menu with options to copy, paste, select all, and clear terminal content.
- Integrated keyboard shortcuts for copy (Ctrl/Cmd+C), paste (Ctrl/Cmd+V), and select all (Ctrl/Cmd+A).
- Enhanced platform detection for Mac users to adjust key bindings accordingly.
- Implemented functionality to handle context menu actions and close the menu on outside clicks or key events.
2025-12-18 16:23:05 -05:00
Kacper
1656b4fb7a Merge remote-tracking branch 'origin/main' into refactor/frontend 2025-12-18 21:03:23 +01:00
Kacper
419e954230 feat: add setup-project action for common CI setup steps
Introduced a new GitHub Action to streamline project setup in CI workflows. This action handles Node.js setup, dependency installation, and native module rebuilding, replacing repetitive steps in multiple workflow files. Updated e2e-tests, pr-check, and test workflows to utilize the new action, enhancing maintainability and reducing duplication.
2025-12-18 20:59:33 +01:00
Kacper
1a83c9b256 chore: downgrade @types/node to match ci version 2025-12-18 20:56:21 +01:00
Web Dev Cody
a0efa5d351 Merge pull request #165 from AutoMaker-Org/fix-automode-button-ui
fix: update Switch component styles for improved UI consistency
2025-12-18 14:39:06 -05:00
Alec Koifman
afcda98dc4 fix: update Switch component styles for improved UI consistency
- Changed border color from transparent to border-border for better visibility.
- Updated thumb background color from bg-background to bg-foreground to enhance contrast.
2025-12-18 14:27:11 -05:00
Kacper
e508f9c1d1 Merge remote-tracking branch 'origin/main' into refactor/frontend 2025-12-18 20:16:47 +01:00
Kacper
8c2c54b0a4 feat: load context files as system prompt for higher priority
Context files from .automaker/context/ (CLAUDE.md, CODE_QUALITY.md, etc.)
are now passed as system prompt instead of prepending to user prompt.
This ensures the agent follows project-specific rules like package manager
preferences (pnpm vs npm) and coding conventions.

Changes:
- Add getContextDir() utility to automaker-paths.ts
- Add loadContextFiles() method to load .md/.txt files from context dir
- Pass context as systemPrompt in executeFeature() and followUpFeature()
- Add debug logging to confirm system prompt is provided

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 20:11:05 +01:00
Web Dev Cody
65edddbc36 Merge pull request #164 from AutoMaker-Org/another-template
feat: add Automaker Starter Kit template to starterTemplates
2025-12-18 12:19:10 -05:00
Cody Seibert
0dfe5a91e7 feat: add Automaker Starter Kit template to starterTemplates
Introduced a new starter template for the Automaker Starter Kit, which includes a comprehensive description, tech stack, features, and author information. This template aims to support aspiring full stack engineers in their learning journey.
2025-12-18 12:18:22 -05:00
Web Dev Cody
2f75c0bec5 Merge pull request #163 from AutoMaker-Org/fix/automode-infinite-loop
fix: prevent infinite loop when resuming feature with existing context
2025-12-18 11:15:12 -05:00
Kacper
516f26edae fix: prevent infinite loop when resuming feature with existing context
When executeFeatureWithContext calls executeFeature with a continuation
prompt, skip the context existence check to avoid the loop:
executeFeature -> resumeFeature -> executeFeatureWithContext -> executeFeature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 16:22:20 +01:00
Kacper
dd8862ce21 fix: prevent infinite loop when resuming feature with existing context
When executeFeatureWithContext calls executeFeature with a continuation
prompt, skip the context existence check to avoid the loop:
executeFeature -> resumeFeature -> executeFeatureWithContext -> executeFeature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 16:10:10 +01:00
Kacper
0c2192d039 chore: update dependencies 2025-12-18 16:06:17 +01:00
Kacper
a85390b289 feat: apply coderabbit suggestions 2025-12-18 15:49:49 +01:00
Kacper
c4a90d7f29 fix: update port configuration across application files
- Changed the static server port from 5173 to 3007 in init.mjs, playwright.config.ts, vite.config.mts, and main.ts to ensure consistency in server setup and availability.
- Updated logging messages to reflect the new port configuration.
2025-12-18 15:35:05 +01:00
Kacper
8794156f28 fix: web mode not loading from init.mjs file 2025-12-18 15:29:35 +01:00
Kacper
7603c827f6 chore: remove init.sh script for development environment setup
- Deleted the init.sh script, which was responsible for setting up and launching the development environment, including dependency installation and server management.
2025-12-18 15:24:56 +01:00
Kacper
7ad70a3923 fix: update port references in init.mjs for server availability
- Changed the port from 3007 to 5173 in the logging and server availability messages to reflect the new configuration.
- Ensured that the process killing function targets the correct port for consistency in server setup.
2025-12-18 15:24:38 +01:00
Kacper
95c6a69610 chore: disable npm rebuild in package.json
- Set "npmRebuild" to false in package.json to prevent automatic rebuilding of native modules during installation.
2025-12-18 15:19:28 +01:00
Kacper
157dd71efa test: enhance app specification and automaker paths tests
- Added comprehensive tests for the `specToXml` function, covering various scenarios including minimal specs, XML escaping, and optional sections.
- Updated tests for `getStructuredSpecPromptInstruction` and `getAppSpecFormatInstruction` to ensure they return valid instructions.
- Refactored automaker paths tests to use `path.join` for cross-platform compatibility, ensuring correct directory paths are generated.
2025-12-18 14:58:48 +01:00
Kacper
06ed965a3b chore: regenerate package-lock.json 2025-12-18 14:54:24 +01:00
Kacper
ea7e273fb4 Merge main into refactor/frontend
- Merge PR #162: cross-platform dev script (init.mjs)
- Updated init.mjs to reference apps/ui instead of apps/app
- Updated root package.json scripts to use apps/ui workspace

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 14:46:23 +01:00
Kacper
dcf05e4f1c refactor: update build scripts and add new server preparation scripts
- Replaced JavaScript files with ES module versions for server preparation and setup scripts.
- Introduced `prepare-server.mjs` for bundling server with Electron, enhancing dependency management.
- Added `rebuild-server-natives.cjs` for rebuilding native modules during the Electron packaging process.
- Updated `setup-e2e-fixtures.mjs` to create necessary directories and files for Playwright tests.
- Adjusted `package.json` scripts to reflect changes in file extensions and improve build process clarity.
2025-12-18 14:38:21 +01:00
Web Dev Cody
50fb7ece4f Merge pull request #162 from leonvanzyl/main
feat: add cross-platform dev script (Windows/macOS/Linux support)
2025-12-18 08:13:59 -05:00
Kacper
f9db4fffa7 chore: remove comment on maxTurns in sdk-options
- Cleaned up the code by removing the comment on maxTurns, which previously explained the increase from quick to standard. The value remains set to MAX_TURNS.extended.
2025-12-18 13:38:11 +01:00
Kacper
1cb6daaa07 fix: update permission mode in sdk-options test
- Changed expected permission mode in sdk-options test from "acceptEdits" to "default" to align with recent updates in spec generation options.
2025-12-18 13:34:58 +01:00
Kacper
adf9307796 feat: enhance app specification structure and XML conversion
- Introduced a TypeScript interface for structured specification output to standardize project details.
- Added a JSON schema for reliable parsing of structured output.
- Implemented XML conversion for structured specifications, ensuring comprehensive project representation.
- Updated spec generation options to include output format configuration.
- Enhanced prompt instructions for generating specifications to improve clarity and completeness.
2025-12-18 13:32:16 +01:00
Kacper
7fdc2b2fab refactor: update app specification generation and XML handling
- Enhanced instructions for generating app specifications to clarify XML output requirements.
- Updated permission mode in spec generation options to ensure read-only access.
- Improved logging to capture XML content extraction and handle potential issues with incomplete responses.
- Ensured that only valid XML is saved, avoiding conversational text from the response.
2025-12-18 13:09:50 +01:00
Kacper
0d8043f1f2 fix(ci): rebuild node-pty after install to fix native module errors
The --ignore-scripts flag also skips building native modules like
node-pty which the server needs. Added explicit rebuild step for
node-pty in test.yml and e2e-tests.yml workflows.

This is more targeted than electron-builder install-app-deps which
rebuilds ALL native modules and causes OOM.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:33:58 +01:00
Kacper
2c079623a8 fix(ci): add --ignore-scripts to Linux native bindings install
The npm install for Linux bindings was also triggering electron-builder
postinstall script. Added --ignore-scripts to all three workflows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:21:43 +01:00
Kacper
899c45fc1b fix(ci): skip postinstall scripts to avoid OOM in all workflows
electron-builder install-app-deps rebuilds native modules and uses
too much memory, causing npm install to be killed (exit code 143).

Updated workflows:
- e2e-tests.yml
- test.yml
- pr-check.yml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:20:14 +01:00
Kacper
c763f2a545 fix: replace git+ssh URLs with https in package-lock.json
CI environments don't have SSH keys, so GitHub URLs must use HTTPS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:10:20 +01:00
Kacper
45dd1d45a1 chore: regenerate package-lock.json 2025-12-18 12:07:49 +01:00
Kacper
a860b3cf45 Merge main into refactor/frontend
Merge latest features from main including:
- PR #161 (worktree-confusion): Clarified branch handling in dialogs
- PR #160 (speckits-rebase): Planning mode functionality

Resolved conflicts:
- add-feature-dialog.tsx: Combined TanStack Router navigation with branch selection state
- worktree-integration.spec.ts: Updated tests for new worktree behavior (created at execution time)
- package-lock.json: Regenerated after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:00:45 +01:00
Auto
9fcdd899b2 feat: add cross-platform dev script (Windows/macOS/Linux support)
Replace Unix-only init.sh with cross-platform init.mjs Node.js script.

Changes:
- Add init.mjs: Cross-platform Node.js implementation of init.sh
- Update package.json: Change dev script from ./init.sh to node init.mjs
- Add tree-kill dependency for reliable cross-platform process termination

Key features of init.mjs:
- Cross-platform port detection (netstat on Windows, lsof on Unix)
- Cross-platform process killing using tree-kill package
- Uses cross-spawn for reliable npm/npx command execution on Windows
- Interactive prompts via Node.js readline module
- Colored terminal output (works on modern Windows terminals)
- Proper cleanup handlers for Ctrl+C/SIGTERM

Bug fix:
- Fixed Playwright browser check to run from apps/app directory where
  @playwright/test is actually installed (was silently failing before)

The original init.sh is preserved for backward compatibility.
2025-12-18 09:33:35 +02:00
Web Dev Cody
bacafd129f Merge pull request #161 from AutoMaker-Org/worktree-confusion
feat: enhance UI components and branch management
2025-12-18 00:33:25 -05:00
Cody Seibert
20d7fb1949 refactor: update Playwright config and worktree integration tests
- Adjusted Playwright configuration to set workers to undefined for improved test execution.
- Updated comments in worktree integration tests to clarify branch creation logic and ensure accurate assertions regarding branch and worktree paths.
2025-12-18 00:22:37 -05:00
Cody Seibert
a192eaa20f test: update worktree integration tests for branch input handling
- Added functionality to select "Other branch" in the edit feature dialog, enabling the branch input field.
- Updated the locator for the branch input from 'edit-feature-branch' to 'edit-feature-input' for consistency across tests.
2025-12-18 00:00:00 -05:00
Cody Seibert
99fe6f6497 refactor: clarify branch name handling in feature dialogs
- Updated comments in AddFeatureDialog and EditFeatureDialog to better explain the logic for determining the final branch name based on the current worktree context.
- Adjusted logic to ensure that an empty string indicates "unassigned" for primary worktrees, while allowing for the use of the current branch when applicable.
- Simplified branch name handling in useBoardActions to reflect these changes.
2025-12-17 23:38:19 -05:00
Cody Seibert
35c6beca37 fix: address PR 161 review comments
- Fix unknown status bypassing worktree filtering in use-board-column-features.ts
- Remove unused props projectPath and onWorktreeCreated from use-board-drag-drop.ts
- Fix test expecting worktreePath during edit (worktrees created at execution time)
- Remove unused setAutoModeRunning from dependency array
- Remove unused imports (BranchAutocomplete, cn)
- Fix htmlFor accessibility issue in branch-selector.tsx
- Remove empty finally block in resume-feature.ts
- Remove duplicate setTimeout state reset in create-pr-dialog.tsx
- Consolidate duplicate state reset logic in context-view.tsx
- Simplify branch name defaulting logic in use-board-actions.ts
- Fix branchName reset to null when worktree is deleted
2025-12-17 23:24:54 -05:00
Cody Seibert
f7cb92fa9d test: update status management test for auto mode service
- Modified the test to check that runningCount is 0 when no features are running, ensuring accurate status reporting.
2025-12-17 23:05:39 -05:00
Cody Seibert
b403d0d570 Merge main into worktree-confusion: resolve conflicts maintaining both sets of changes 2025-12-17 23:01:48 -05:00
Cody Seibert
c80ae3367a refactor: improve dialog and auto mode service functionality
- Refactored DialogContent component to use forwardRef for better integration with refs.
- Enhanced auto mode service by introducing an auto loop for processing features concurrently.
- Updated error handling and feature management logic to streamline operations.
- Cleaned up code formatting and improved readability across various components and services.
2025-12-17 22:45:39 -05:00
Cody Seibert
0549b8085a feat: enhance UI components and branch management
- Added new RadioGroup and Switch components for better UI interaction.
- Introduced BranchSelector for improved branch selection in feature dialogs.
- Updated Autocomplete and BranchAutocomplete components to handle error states.
- Refactored feature management to archive verified features instead of deleting them.
- Enhanced worktree handling by removing worktreePath from features, relying on branchName instead.
- Improved auto mode functionality by integrating branch management and worktree updates.
- Cleaned up unused code and optimized existing logic for better performance.
2025-12-17 22:29:39 -05:00
Kacper
019ac56ceb feat: enhance suggestion generation with structured output and increased max turns
- Updated MAX_TURNS to allow for more iterations in suggestion generation: quick (5 to 50), standard (20 to 100), and extended (50 to 250).
- Introduced a JSON schema for structured output in suggestions, improving the format and consistency of generated suggestions.
- Modified the generateSuggestions function to utilize structured output when available, with a fallback to text parsing for compatibility.

This enhances the suggestion generation process, allowing for more thorough exploration and better output formatting.
2025-12-18 03:55:34 +01:00
Kacper
e1c3b7528f feat: enhance root layout with navigation and theme handling
- Added useNavigate hook to facilitate programmatic navigation.
- Implemented a useEffect to redirect to the board view if a project was previously open and the root path is accessed.
- Updated theme class application to ensure proper filtering of theme options.

This improves user experience by ensuring the correct view is displayed upon navigation and enhances theme management.
2025-12-18 01:37:33 +01:00
Kacper
e78bfc80ec feat: refactor spec-view to folder pattern and add feature count selector
Closes #151

- Refactor spec-view.tsx from 1,230 lines to ~170 lines following folder-pattern.md
- Create unified CreateSpecDialog with all features from both dialogs:
  - featureCount selector (20/50/100) - was missing in spec-view
  - analyzeProject checkbox - was missing in sidebar
- Extract components: spec-header, spec-editor, spec-empty-state
- Extract hooks: use-spec-loading, use-spec-save, use-spec-generation
- Extract dialogs: create-spec-dialog, regenerate-spec-dialog
- Update sidebar to use new CreateSpecDialog with analyzeProject state
- Delete deprecated project-setup-dialog.tsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 00:14:44 +01:00
Kacper
3eac848d4f fix: use double quotes for git branch format string on Linux
The git branch --format option needs proper quoting to work
cross-platform. Single quotes are preserved literally on Windows,
while unquoted format strings may be misinterpreted on Linux.
Using double quotes works correctly on both platforms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:03:29 +01:00
Kacper
76cb72812f fix: normalize worktree paths and update branch listing logic
- Updated the branch listing command to remove quotes around branch names, ensuring compatibility across platforms.
- Enhanced worktree path comparisons in tests to normalize path separators, improving consistency between server and client environments.
- Adjusted workspace root resolution to reflect the correct directory structure for the UI.

This addresses potential discrepancies in branch names and worktree paths, particularly on Windows systems.
2025-12-17 22:52:40 +01:00
Kacper
bfc8f9bc26 fix: use browser history in web mode for proper URL routing
The router was using memory history with initial entry "/" which caused
all routes to render the index component regardless of the browser URL.

Changes:
- Use browser history when not in Electron (for e2e tests and dev)
- Use memory history only in Electron environment
- Update test utilities to use persist version 2 to match app store

This fixes e2e tests that navigate directly to /board, /context, /spec

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:37:58 +01:00
Kacper
8f2e06bc32 fix: improve waitForBoardView to handle zustand hydration
The zustand store may not have hydrated from localStorage by the time
the board view first renders, causing board-view-no-project to appear
briefly. Use waitForFunction to poll until board-view appears.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:19:53 +01:00
Kacper
2c9f77356f fix: update e2e test navigation to use direct routes
The index route (/) now shows WelcomeView instead of auto-redirecting
to board view. Updated test utilities to navigate directly to the
correct routes:

- navigateToBoard -> /board
- navigateToContext -> /context
- navigateToSpec -> /spec
- navigateToAgent -> /agent
- navigateToSettings -> /settings
- waitForBoardView -> navigates to /board first

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:08:05 +01:00
Kacper
ea1b10fea6 fix: skip electron plugin in CI to prevent X11 display error
The vite-plugin-electron was trying to spawn Electron during the Vite
dev server startup, which fails in CI because there's no X11 display.

- Use Vite's function config to check command type (serve vs build)
- Only skip electron plugin during dev server (command=serve) in CI
- Always include electron plugin during build for dist-electron/main.js
- Add VITE_SKIP_ELECTRON env var support for explicit control
- Update playwright.config.ts to pass VITE_SKIP_ELECTRON in CI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 21:48:30 +01:00
Kacper
e6a63ccae1 chore: remove CLAUDE.md file
- Deleted the CLAUDE.md file which provided guidance for the Claude Code project.
- This file contained project overview, architecture details, development commands, code conventions, and environment variables.
2025-12-17 21:20:42 +01:00
Kacper
784188cf52 fix: improve theme handling to support all themes
- index.html: Apply actual theme class instead of only 'dark'
- __root.tsx: Use themeOptions to dynamically generate theme classes
  - Fixes missing themes: cream, sunset, gray

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 21:18:54 +01:00
Kacper
1d945f4f75 fix: update API key test to use backend endpoint
- Use /api/setup/verify-claude-auth instead of removed Next.js route
- Add placeholder for Gemini test (needs backend endpoint)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 21:14:03 +01:00
Kacper
df50cccb7b fix: regenerate package-lock.json with HTTPS URLs
Remove git+ssh:// URLs that fail CI lint check

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 21:06:14 +01:00
Kacper
b0a9c89157 chore: add directory output options for Electron builds
- Introduced new build commands for Electron in package.json to support directory output.
- Updated CI workflow to utilize the new directory-only build command for faster execution.
2025-12-17 20:57:13 +01:00
Kacper
45eaf91cb3 chore: enhance Electron build scripts in package.json
- Added new build commands for Electron to support directory output for Windows, macOS, and Linux.
- This update improves the flexibility of the build process for different deployment scenarios.
2025-12-17 20:54:26 +01:00
Kacper
c20b224f30 ci: update e2e workflow for Vite migration
- Change workspace from apps/app to apps/ui
- Update env vars from NEXT_PUBLIC_* to VITE_*
- Update artifact paths for playwright reports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 20:46:55 +01:00
Kacper
96b941b008 refactor: complete migration from Next.js to Vite and update documentation
- Finalized core migration to Vite, ensuring feature parity and functionality.
- Updated migration plan to reflect completed tasks and deferred items.
- Renamed `apps/app` to `apps/ui` and adjusted related configurations.
- Verified Zustand stores and HTTP API client functionality remain unchanged.
- Added additional tasks completed during migration, including environment variable updates and memory history configuration for Electron.

This commit marks the transition to the new build infrastructure, setting the stage for further component refactoring.
2025-12-17 20:42:24 +01:00
Kacper
ad4da23743 Merge main into refactor/frontend
- Resolved conflicts from apps/app to apps/ui migration
- Moved worktree-panel component to apps/ui
- Moved dependency-resolver.ts to apps/ui
- Removed worktree-selector.tsx (replaced by worktree-panel)
- Merged theme updates, file browser improvements, and Gemini fixes
- Merged server dependency resolver and auto-mode-service updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 20:14:19 +01:00
Kacper
5136c32b68 refactor: move from next js to vite and tanstack router 2025-12-17 20:11:26 +01:00
Kacper
9954feafd8 chore: add migraiton plan and claude md file 2025-12-17 18:32:04 +01:00
346 changed files with 21190 additions and 21632 deletions

View File

@@ -0,0 +1,66 @@
name: "Setup Project"
description: "Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules"
inputs:
node-version:
description: "Node.js version to use"
required: false
default: "22"
check-lockfile:
description: "Run lockfile lint check for SSH URLs"
required: false
default: "false"
rebuild-node-pty-path:
description: "Working directory for node-pty rebuild (empty = root)"
required: false
default: ""
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
cache-dependency-path: package-lock.json
- name: Check for SSH URLs in lockfile
if: inputs.check-lockfile == 'true'
shell: bash
run: npm run lint:lockfile
- name: Configure Git for HTTPS
shell: bash
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies
shell: bash
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
# Skip scripts to avoid electron-builder install-app-deps which uses too much memory
run: npm install --ignore-scripts
- name: Install Linux native bindings
shell: bash
# Workaround for npm optional dependencies bug (npm/cli#4828)
# Explicitly install Linux bindings needed for build tools
run: |
npm install --no-save --force --ignore-scripts \
@rollup/rollup-linux-x64-gnu@4.53.3 \
@tailwindcss/oxide-linux-x64-gnu@4.1.17
- name: Rebuild native modules (root)
if: inputs.rebuild-node-pty-path == ''
shell: bash
# Rebuild node-pty and other native modules for Electron
run: npm rebuild node-pty
- name: Rebuild native modules (workspace)
if: inputs.rebuild-node-pty-path != ''
shell: bash
# Rebuild node-pty and other native modules needed for server
run: npm rebuild node-pty
working-directory: ${{ inputs.rebuild-node-pty-path }}

View File

@@ -18,34 +18,15 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Setup project
uses: ./.github/actions/setup-project
with:
node-version: "22"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
run: npm install
- name: Install Linux native bindings
# Workaround for npm optional dependencies bug (npm/cli#4828)
# Explicitly install Linux bindings needed for build tools
run: |
npm install --no-save --force \
@rollup/rollup-linux-x64-gnu@4.53.3 \
@tailwindcss/oxide-linux-x64-gnu@4.1.17
check-lockfile: "true"
rebuild-node-pty-path: "apps/server"
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
working-directory: apps/app
working-directory: apps/ui
- name: Build server
run: npm run build --workspace=apps/server
@@ -71,20 +52,20 @@ jobs:
exit 1
- name: Run E2E tests
# Playwright automatically starts the Next.js frontend via webServer config
# (see apps/app/playwright.config.ts) - no need to start it manually
run: npm run test --workspace=apps/app
# Playwright automatically starts the Vite frontend via webServer config
# (see apps/ui/playwright.config.ts) - no need to start it manually
run: npm run test --workspace=apps/ui
env:
CI: true
NEXT_PUBLIC_SERVER_URL: http://localhost:3008
NEXT_PUBLIC_SKIP_SETUP: "true"
VITE_SERVER_URL: http://localhost:3008
VITE_SKIP_SETUP: "true"
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: apps/app/playwright-report/
path: apps/ui/playwright-report/
retention-days: 7
- name: Upload test results
@@ -92,5 +73,5 @@ jobs:
if: failure()
with:
name: test-results
path: apps/app/test-results/
path: apps/ui/test-results/
retention-days: 7

View File

@@ -17,33 +17,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Setup project
uses: ./.github/actions/setup-project
with:
node-version: "22"
cache: "npm"
cache-dependency-path: package-lock.json
check-lockfile: "true"
- name: Check for SSH URLs in lockfile
run: npm run lint:lockfile
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
run: npm install
- name: Install Linux native bindings
# Workaround for npm optional dependencies bug (npm/cli#4828)
# Explicitly install Linux bindings needed for build tools
run: |
npm install --no-save --force \
@rollup/rollup-linux-x64-gnu@4.53.3 \
@tailwindcss/oxide-linux-x64-gnu@4.1.17
- name: Run build:electron
run: npm run build:electron
- name: Run build:electron (dir only - faster CI)
run: npm run build:electron:dir

View File

@@ -1,180 +0,0 @@
name: Build and Release Electron App
on:
push:
tags:
- "v*.*.*" # Triggers on version tags like v1.0.0
workflow_dispatch: # Allows manual triggering
inputs:
version:
description: "Version to release (e.g., v1.0.0)"
required: true
default: "v0.1.0"
jobs:
build-and-release:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
name: macOS
artifact-name: macos-builds
- os: windows-latest
name: Windows
artifact-name: windows-builds
- os: ubuntu-latest
name: Linux
artifact-name: linux-builds
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
run: npm install
- name: Install Linux native bindings
# Workaround for npm optional dependencies bug (npm/cli#4828)
# Only needed on Linux - macOS and Windows get their bindings automatically
if: matrix.os == 'ubuntu-latest'
run: |
npm install --no-save --force \
@rollup/rollup-linux-x64-gnu@4.53.3 \
@tailwindcss/oxide-linux-x64-gnu@4.1.17
- name: Extract and set version
id: version
shell: bash
run: |
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
VERSION="${VERSION_TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
# Update the app's package.json version
cd apps/app
npm version $VERSION --no-git-tag-version
cd ../..
echo "Updated apps/app/package.json to version $VERSION"
- name: Build Electron App (macOS)
if: matrix.os == 'macos-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --mac --x64 --arm64
- name: Build Electron App (Windows)
if: matrix.os == 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --win --x64
- name: Build Electron App (Linux)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --linux --x64
- name: Upload Release Assets
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.event.inputs.version || github.ref_name }}
files: |
apps/app/dist/*.exe
apps/app/dist/*.dmg
apps/app/dist/*.AppImage
apps/app/dist/*.zip
apps/app/dist/*.deb
apps/app/dist/*.rpm
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload macOS artifacts for R2
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: apps/app/dist/*.dmg
retention-days: 1
- name: Upload Windows artifacts for R2
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: apps/app/dist/*.exe
retention-days: 1
- name: Upload Linux artifacts for R2
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: apps/app/dist/*.AppImage
retention-days: 1
upload-to-r2:
needs: build-and-release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Install AWS SDK
run: npm install @aws-sdk/client-s3
- name: Extract version
id: version
shell: bash
run: |
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
VERSION="${VERSION_TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
- name: Upload to R2 and update releases.json
env:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
RELEASE_VERSION: ${{ steps.version.outputs.version }}
RELEASE_TAG: ${{ steps.version.outputs.version_tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: node .github/scripts/upload-to-r2.js

View File

@@ -17,30 +17,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Setup project
uses: ./.github/actions/setup-project
with:
node-version: "22"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Configure Git for HTTPS
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"
- name: Install dependencies
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
run: npm install
- name: Install Linux native bindings
# Workaround for npm optional dependencies bug (npm/cli#4828)
# Explicitly install Linux bindings needed for build tools
run: |
npm install --no-save --force \
@rollup/rollup-linux-x64-gnu@4.53.3 \
@tailwindcss/oxide-linux-x64-gnu@4.1.17
check-lockfile: "true"
rebuild-node-pty-path: "apps/server"
- name: Run server tests with coverage
run: npm run test:server:coverage

View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="apps/app/public/readme_logo.png" alt="Automaker Logo" height="80" />
<img src="apps/ui/public/readme_logo.png" alt="Automaker Logo" height="80" />
</p>
> **[!TIP]**
@@ -88,6 +88,7 @@ The future of software development is **agentic coding**—where developers beco
Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows.
In the Discord, you can:
- 💬 Discuss agentic coding patterns and best practices
- 🧠 Share ideas for AI-driven development workflows
- 🛠️ Get help setting up or extending Automaker
@@ -252,19 +253,16 @@ This project is licensed under the **Automaker License Agreement**. See [LICENSE
**Summary of Terms:**
- **Allowed:**
- **Build Anything:** You can clone and use Automaker locally or in your organization to build ANY product (commercial or free).
- **Internal Use:** You can use it internally within your company (commercial or non-profit) without restriction.
- **Modify:** You can modify the code for internal use within your organization (commercial or non-profit).
- **Restricted (The "No Monetization of the Tool" Rule):**
- **No Resale:** You cannot resell Automaker itself.
- **No SaaS:** You cannot host Automaker as a service for others.
- **No Monetizing Mods:** You cannot distribute modified versions of Automaker for money.
- **Liability:**
- **Use at Own Risk:** This tool uses AI. We are **NOT** responsible if it breaks your computer, deletes your files, or generates bad code. You assume all risk.
- **Contributing:**

View File

@@ -1,310 +0,0 @@
# Large Files - Refactoring Candidates
This document tracks files in the AutoMaker codebase that exceed 3000 lines or are significantly large (1000+ lines) and should be considered for refactoring into smaller, more maintainable components.
**Last Updated:** 2025-12-15
**Total Large Files:** 8
**Combined Size:** 15,027 lines
---
## 🔴 CRITICAL - Over 3000 Lines
### 1. board-view.tsx - 3,325 lines
**Path:** `apps/app/src/components/views/board-view.tsx`
**Type:** React Component (TSX)
**Priority:** VERY HIGH
**Description:**
Main Kanban board view component that serves as the centerpiece of the application.
**Current Responsibilities:**
- Feature/task card management and drag-and-drop operations using @dnd-kit
- Adding, editing, and deleting features
- Running autonomous agents to implement features
- Displaying feature status across multiple columns (Backlog, In Progress, Waiting Approval, Verified)
- Model/AI profile selection for feature implementation
- Advanced options configuration (thinking level, model selection, skip tests)
- Search/filtering functionality for cards
- Output modal for viewing agent results
- Feature suggestions dialog
- Board background customization
- Integration with Electron APIs for IPC communication
- Keyboard shortcuts support
- 40+ state variables for managing UI state
**Refactoring Recommendations:**
Extract into smaller components:
- `AddFeatureDialog.tsx` - Feature creation dialog with image upload
- `EditFeatureDialog.tsx` - Feature editing dialog
- `AgentOutputModal.tsx` - Already exists, verify separation
- `FeatureSuggestionsDialog.tsx` - Already exists, verify separation
- `BoardHeader.tsx` - Header with controls and search
- `BoardSearchBar.tsx` - Search and filter functionality
- `ConcurrencyControl.tsx` - Concurrency slider component
- `BoardActions.tsx` - Action buttons (add feature, auto mode, etc.)
- `DragDropContext.tsx` - Wrap drag-and-drop logic
- Custom hooks:
- `useBoardFeatures.ts` - Feature loading and management
- `useBoardDragDrop.ts` - Drag and drop handlers
- `useBoardActions.ts` - Feature action handlers (run, verify, delete, etc.)
- `useBoardKeyboardShortcuts.ts` - Keyboard shortcut logic
---
## 🟡 HIGH PRIORITY - 2000+ Lines
### 2. sidebar.tsx - 2,396 lines
**Path:** `apps/app/src/components/layout/sidebar.tsx`
**Type:** React Component (TSX)
**Priority:** HIGH
**Description:**
Main navigation sidebar with comprehensive project management.
**Current Responsibilities:**
- Project folder navigation and selection
- View mode switching (Board, Agent, Settings, etc.)
- Project operations (create, delete, rename)
- Theme and appearance controls
- Terminal, Wiki, and other view launchers
- Drag-and-drop project reordering
- Settings and configuration access
**Refactoring Recommendations:**
Split into focused components:
- `ProjectSelector.tsx` - Project list and selection
- `NavigationTabs.tsx` - View mode tabs
- `ProjectActions.tsx` - Create, delete, rename operations
- `SettingsMenu.tsx` - Settings dropdown
- `ThemeSelector.tsx` - Theme controls
- `ViewLaunchers.tsx` - Terminal, Wiki launchers
- Custom hooks:
- `useProjectManagement.ts` - Project CRUD operations
- `useSidebarState.ts` - Sidebar state management
---
### 3. electron.ts - 2,356 lines
**Path:** `apps/app/src/lib/electron.ts`
**Type:** TypeScript Utility/API Bridge
**Priority:** HIGH
**Description:**
Electron IPC bridge and type definitions for frontend-backend communication.
**Current Responsibilities:**
- File system operations (read, write, directory listing)
- Project management APIs
- Feature management APIs
- Terminal/shell execution
- Auto mode and agent execution APIs
- Worktree management
- Provider status APIs
- Event handling and subscriptions
**Refactoring Recommendations:**
Modularize into domain-specific API modules:
- `api/file-system-api.ts` - File operations
- `api/project-api.ts` - Project CRUD
- `api/feature-api.ts` - Feature management
- `api/execution-api.ts` - Auto mode and agent execution
- `api/provider-api.ts` - Provider status and management
- `api/worktree-api.ts` - Git worktree operations
- `api/terminal-api.ts` - Terminal/shell APIs
- `types/electron-types.ts` - Shared type definitions
- `electron.ts` - Main export aggregator
---
### 4. app-store.ts - 2,174 lines
**Path:** `apps/app/src/store/app-store.ts`
**Type:** TypeScript State Management (Zustand Store)
**Priority:** HIGH
**Description:**
Centralized application state store using Zustand.
**Current Responsibilities:**
- Global app state types and interfaces
- Project and feature management state
- Theme and appearance settings
- API keys configuration
- Keyboard shortcuts configuration
- Terminal themes configuration
- Auto mode settings
- All store mutations and selectors
**Refactoring Recommendations:**
Split into domain-specific stores:
- `stores/projects-store.ts` - Project state and actions
- `stores/features-store.ts` - Feature state and actions
- `stores/ui-store.ts` - UI state (theme, sidebar, modals)
- `stores/settings-store.ts` - User settings and preferences
- `stores/execution-store.ts` - Auto mode and running tasks
- `stores/provider-store.ts` - Provider configuration
- `types/store-types.ts` - Shared type definitions
- `app-store.ts` - Main store aggregator with combined selectors
---
## 🟢 MEDIUM PRIORITY - 1000-2000 Lines
### 5. auto-mode-service.ts - 1,232 lines
**Path:** `apps/server/src/services/auto-mode-service.ts`
**Type:** TypeScript Service (Backend)
**Priority:** MEDIUM-HIGH
**Description:**
Core autonomous feature implementation service.
**Current Responsibilities:**
- Worktree creation and management
- Feature execution with Claude Agent SDK
- Concurrent execution with concurrency limits
- Progress streaming via events
- Verification and merge workflows
- Provider management
- Error handling and classification
**Refactoring Recommendations:**
Extract into service modules:
- `services/worktree-manager.ts` - Worktree operations
- `services/feature-executor.ts` - Feature execution logic
- `services/concurrency-manager.ts` - Concurrency control
- `services/verification-service.ts` - Verification workflows
- `utils/error-classifier.ts` - Error handling utilities
---
### 6. spec-view.tsx - 1,230 lines
**Path:** `apps/app/src/components/views/spec-view.tsx`
**Type:** React Component (TSX)
**Priority:** MEDIUM
**Description:**
Specification editor view component for feature specification management.
**Refactoring Recommendations:**
Extract editor components and hooks:
- `SpecEditor.tsx` - Main editor component
- `SpecToolbar.tsx` - Editor toolbar
- `SpecSidebar.tsx` - Spec navigation sidebar
- `useSpecEditor.ts` - Editor state management
---
### 7. kanban-card.tsx - 1,180 lines
**Path:** `apps/app/src/components/views/kanban-card.tsx`
**Type:** React Component (TSX)
**Priority:** MEDIUM
**Description:**
Individual Kanban card component with rich feature display and interaction.
**Refactoring Recommendations:**
Split into smaller card components:
- `KanbanCardHeader.tsx` - Card title and metadata
- `KanbanCardBody.tsx` - Card content
- `KanbanCardActions.tsx` - Action buttons
- `KanbanCardStatus.tsx` - Status indicators
- `useKanbanCard.ts` - Card interaction logic
---
### 8. analysis-view.tsx - 1,134 lines
**Path:** `apps/app/src/components/views/analysis-view.tsx`
**Type:** React Component (TSX)
**Priority:** MEDIUM
**Description:**
Analysis view component for displaying and managing feature analysis data.
**Refactoring Recommendations:**
Extract visualization and data components:
- `AnalysisChart.tsx` - Chart/graph components
- `AnalysisTable.tsx` - Data table
- `AnalysisFilters.tsx` - Filter controls
- `useAnalysisData.ts` - Data fetching and processing
---
## Refactoring Strategy
### Phase 1: Critical (Immediate)
1. **board-view.tsx** - Break into dialogs, header, and custom hooks
- Extract all dialogs first (AddFeature, EditFeature)
- Move to custom hooks for business logic
- Split remaining UI into smaller components
### Phase 2: High Priority (Next Sprint)
2. **sidebar.tsx** - Componentize navigation and project management
3. **electron.ts** - Modularize into API domains
4. **app-store.ts** - Split into domain stores
### Phase 3: Medium Priority (Future)
5. **auto-mode-service.ts** - Extract service modules
6. **spec-view.tsx** - Break into editor components
7. **kanban-card.tsx** - Split card into sub-components
8. **analysis-view.tsx** - Extract visualization components
---
## General Refactoring Guidelines
### When Refactoring Large Components:
1. **Extract Dialogs/Modals First**
- Move dialog components to separate files
- Keep dialog state management in parent initially
- Later extract to custom hooks if complex
2. **Create Custom Hooks for Business Logic**
- Move data fetching to `useFetch*` hooks
- Move complex state logic to `use*State` hooks
- Move side effects to `use*Effect` hooks
3. **Split UI into Presentational Components**
- Header/toolbar components
- Content area components
- Footer/action components
4. **Move Utils and Helpers**
- Extract pure functions to utility files
- Move constants to separate constant files
- Create type files for shared interfaces
### When Refactoring Large Files:
1. **Identify Domains/Concerns**
- Group related functionality
- Find natural boundaries
2. **Extract Gradually**
- Start with least coupled code
- Work towards core functionality
- Test after each extraction
3. **Maintain Type Safety**
- Export types from extracted modules
- Use shared type files for common interfaces
- Ensure no type errors after refactoring
---
## Progress Tracking
- [ ] board-view.tsx (3,325 lines)
- [ ] sidebar.tsx (2,396 lines)
- [ ] electron.ts (2,356 lines)
- [ ] app-store.ts (2,174 lines)
- [ ] auto-mode-service.ts (1,232 lines)
- [ ] spec-view.tsx (1,230 lines)
- [ ] kanban-card.tsx (1,180 lines)
- [ ] analysis-view.tsx (1,134 lines)
**Target:** All files under 500 lines, most under 300 lines
---
*Generated: 2025-12-15*

View File

@@ -1,5 +0,0 @@
module.exports = {
rules: {
"@typescript-eslint/no-require-imports": "off",
},
};

View File

@@ -1,37 +0,0 @@
/**
* Simplified Electron preload script
*
* Only exposes native features (dialogs, shell) and server URL.
* All other operations go through HTTP API.
*/
const { contextBridge, ipcRenderer } = require("electron");
// Expose minimal API for native features
contextBridge.exposeInMainWorld("electronAPI", {
// Platform info
platform: process.platform,
isElectron: true,
// Connection check
ping: () => ipcRenderer.invoke("ping"),
// Get server URL for HTTP client
getServerUrl: () => ipcRenderer.invoke("server:getUrl"),
// Native dialogs - better UX than prompt()
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options),
// Shell operations
openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath),
// App info
getPath: (name) => ipcRenderer.invoke("app:getPath", name),
getVersion: () => ipcRenderer.invoke("app:getVersion"),
isPackaged: () => ipcRenderer.invoke("app:isPackaged"),
});
console.log("[Preload] Electron API exposed (simplified mode)");

View File

@@ -1,20 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
// Electron files use CommonJS
"electron/**",
]),
]);
export default eslintConfig;

View File

@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -1,97 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
interface AnthropicResponse {
content?: Array<{ type: string; text?: string }>;
model?: string;
error?: { message?: string };
}
export async function POST(request: NextRequest) {
try {
const { apiKey } = await request.json();
// Use provided API key or fall back to environment variable
const effectiveApiKey = apiKey || process.env.ANTHROPIC_API_KEY;
if (!effectiveApiKey) {
return NextResponse.json(
{ success: false, error: "No API key provided or configured in environment" },
{ status: 400 }
);
}
// Send a simple test prompt to the Anthropic API
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": effectiveApiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 100,
messages: [
{
role: "user",
content: "Respond with exactly: 'Claude API connection successful!' and nothing else.",
},
],
}),
});
if (!response.ok) {
const errorData = (await response.json()) as AnthropicResponse;
const errorMessage = errorData.error?.message || `HTTP ${response.status}`;
if (response.status === 401) {
return NextResponse.json(
{ success: false, error: "Invalid API key. Please check your Anthropic API key." },
{ status: 401 }
);
}
if (response.status === 429) {
return NextResponse.json(
{ success: false, error: "Rate limit exceeded. Please try again later." },
{ status: 429 }
);
}
return NextResponse.json(
{ success: false, error: `API error: ${errorMessage}` },
{ status: response.status }
);
}
const data = (await response.json()) as AnthropicResponse;
// Check if we got a valid response
if (data.content && data.content.length > 0) {
const textContent = data.content.find((block) => block.type === "text");
if (textContent && textContent.type === "text" && textContent.text) {
return NextResponse.json({
success: true,
message: `Connection successful! Response: "${textContent.text}"`,
model: data.model,
});
}
}
return NextResponse.json({
success: true,
message: "Connection successful! Claude responded.",
model: data.model,
});
} catch (error: unknown) {
console.error("Claude API test error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to connect to Claude API";
return NextResponse.json(
{ success: false, error: errorMessage },
{ status: 500 }
);
}
}

View File

@@ -1,191 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
interface GeminiContent {
parts: Array<{
text?: string;
inlineData?: {
mimeType: string;
data: string;
};
}>;
role?: string;
}
interface GeminiRequest {
contents: GeminiContent[];
generationConfig?: {
maxOutputTokens?: number;
temperature?: number;
};
}
interface GeminiResponse {
candidates?: Array<{
content: {
parts: Array<{
text: string;
}>;
role: string;
};
finishReason: string;
safetyRatings?: Array<{
category: string;
probability: string;
}>;
}>;
promptFeedback?: {
safetyRatings?: Array<{
category: string;
probability: string;
}>;
};
error?: {
code: number;
message: string;
status: string;
};
}
export async function POST(request: NextRequest) {
try {
const { apiKey, imageData, mimeType, prompt } = await request.json();
// Use provided API key or fall back to environment variable
const effectiveApiKey = apiKey || process.env.GOOGLE_API_KEY;
if (!effectiveApiKey) {
return NextResponse.json(
{ success: false, error: "No API key provided or configured in environment" },
{ status: 400 }
);
}
// Build the request body
const requestBody: GeminiRequest = {
contents: [
{
parts: [],
},
],
generationConfig: {
maxOutputTokens: 150,
temperature: 0.4,
},
};
// Add image if provided
if (imageData && mimeType) {
requestBody.contents[0].parts.push({
inlineData: {
mimeType: mimeType,
data: imageData,
},
});
}
// Add text prompt
const textPrompt = prompt || (imageData
? "Describe what you see in this image briefly."
: "Respond with exactly: 'Gemini SDK connection successful!' and nothing else.");
requestBody.contents[0].parts.push({
text: textPrompt,
});
// Call Gemini API - using gemini-1.5-flash as it supports both text and vision
const model = imageData ? "gemini-1.5-flash" : "gemini-1.5-flash";
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${effectiveApiKey}`;
const response = await fetch(geminiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
const data: GeminiResponse = await response.json();
// Check for API errors
if (data.error) {
const errorMessage = data.error.message || "Unknown Gemini API error";
const statusCode = data.error.code || 500;
if (statusCode === 400 && errorMessage.includes("API key")) {
return NextResponse.json(
{ success: false, error: "Invalid API key. Please check your Google API key." },
{ status: 401 }
);
}
if (statusCode === 429) {
return NextResponse.json(
{ success: false, error: "Rate limit exceeded. Please try again later." },
{ status: 429 }
);
}
return NextResponse.json(
{ success: false, error: `API error: ${errorMessage}` },
{ status: statusCode }
);
}
// Check for valid response
if (!response.ok) {
return NextResponse.json(
{ success: false, error: `HTTP error: ${response.status} ${response.statusText}` },
{ status: response.status }
);
}
// Extract response text
if (data.candidates && data.candidates.length > 0 && data.candidates[0].content?.parts?.length > 0) {
const responseText = data.candidates[0].content.parts
.filter((part) => part.text)
.map((part) => part.text)
.join("");
return NextResponse.json({
success: true,
message: `Connection successful! Response: "${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}"`,
model: model,
hasImage: !!imageData,
});
}
// Handle blocked responses
if (data.promptFeedback?.safetyRatings) {
return NextResponse.json({
success: true,
message: "Connection successful! Gemini responded (response may have been filtered).",
model: model,
hasImage: !!imageData,
});
}
return NextResponse.json({
success: true,
message: "Connection successful! Gemini responded.",
model: model,
hasImage: !!imageData,
});
} catch (error: unknown) {
console.error("Gemini API test error:", error);
if (error instanceof TypeError && error.message.includes("fetch")) {
return NextResponse.json(
{ success: false, error: "Network error. Unable to reach Gemini API." },
{ status: 503 }
);
}
const errorMessage =
error instanceof Error ? error.message : "Failed to connect to Gemini API";
return NextResponse.json(
{ success: false, error: errorMessage },
{ status: 500 }
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Inter, JetBrains_Mono } from "next/font/google";
import { Toaster } from "sonner";
import "./globals.css";
// Inter font for clean theme
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
// JetBrains Mono for clean theme
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
display: "swap",
});
export const metadata: Metadata = {
title: "Automaker - Autonomous AI Development Studio",
description: "Build software autonomously with intelligent orchestration",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${GeistSans.variable} ${GeistMono.variable} ${inter.variable} ${jetbrainsMono.variable} antialiased`}
>
{children}
<Toaster richColors position="bottom-right" />
</body>
</html>
);
}

View File

@@ -1,235 +0,0 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { Sidebar } from "@/components/layout/sidebar";
import { WelcomeView } from "@/components/views/welcome-view";
import { BoardView } from "@/components/views/board-view";
import { SpecView } from "@/components/views/spec-view";
import { AgentView } from "@/components/views/agent-view";
import { SettingsView } from "@/components/views/settings-view";
import { InterviewView } from "@/components/views/interview-view";
import { ContextView } from "@/components/views/context-view";
import { ProfilesView } from "@/components/views/profiles-view";
import { SetupView } from "@/components/views/setup-view";
import { RunningAgentsView } from "@/components/views/running-agents-view";
import { TerminalView } from "@/components/views/terminal-view";
import { WikiView } from "@/components/views/wiki-view";
import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI, isElectron } from "@/lib/electron";
import {
FileBrowserProvider,
useFileBrowser,
setGlobalFileBrowser,
} from "@/contexts/file-browser-context";
function HomeContent() {
const {
currentView,
setCurrentView,
setIpcConnected,
theme,
currentProject,
previewTheme,
getEffectiveTheme,
} = useAppStore();
const { isFirstRun, setupComplete } = useSetupStore();
const [isMounted, setIsMounted] = useState(false);
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
const { openFileBrowser } = useFileBrowser();
// Hidden streamer panel - opens with "\" key
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
// Don't trigger when typing in inputs
const activeElement = document.activeElement;
if (activeElement) {
const tagName = activeElement.tagName.toLowerCase();
if (
tagName === "input" ||
tagName === "textarea" ||
tagName === "select"
) {
return;
}
if (activeElement.getAttribute("contenteditable") === "true") {
return;
}
const role = activeElement.getAttribute("role");
if (role === "textbox" || role === "searchbox" || role === "combobox") {
return;
}
}
// Don't trigger with modifier keys
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
// Check for "\" key (backslash)
if (event.key === "\\") {
event.preventDefault();
setStreamerPanelOpen((prev) => !prev);
}
}, []);
// Register the "\" shortcut for streamer panel
useEffect(() => {
window.addEventListener("keydown", handleStreamerPanelShortcut);
return () => {
window.removeEventListener("keydown", handleStreamerPanelShortcut);
};
}, [handleStreamerPanelShortcut]);
// Compute the effective theme: previewTheme takes priority, then project theme, then global theme
// This is reactive because it depends on previewTheme, currentProject, and theme from the store
const effectiveTheme = getEffectiveTheme();
// Prevent hydration issues
useEffect(() => {
setIsMounted(true);
}, []);
// Initialize global file browser for HttpApiClient
useEffect(() => {
setGlobalFileBrowser(openFileBrowser);
}, [openFileBrowser]);
// Check if this is first run and redirect to setup if needed
useEffect(() => {
console.log("[Setup Flow] Checking setup state:", {
isMounted,
isFirstRun,
setupComplete,
currentView,
shouldShowSetup: isMounted && isFirstRun && !setupComplete,
});
if (isMounted && isFirstRun && !setupComplete) {
console.log(
"[Setup Flow] Redirecting to setup wizard (first run, not complete)"
);
setCurrentView("setup");
} else if (isMounted && setupComplete) {
console.log("[Setup Flow] Setup already complete, showing normal view");
}
}, [isMounted, isFirstRun, setupComplete, setCurrentView, currentView]);
// Test IPC connection on mount
useEffect(() => {
const testConnection = async () => {
try {
const api = getElectronAPI();
const result = await api.ping();
setIpcConnected(result === "pong");
} catch (error) {
console.error("IPC connection failed:", error);
setIpcConnected(false);
}
};
testConnection();
}, [setIpcConnected]);
// Apply theme class to document (uses effective theme - preview, project-specific, or global)
useEffect(() => {
const root = document.documentElement;
const themeClasses = [
"dark",
"light",
"retro",
"dracula",
"nord",
"monokai",
"tokyonight",
"solarized",
"gruvbox",
"catppuccin",
"onedark",
"synthwave",
"red",
"cream",
"sunset",
"gray",
"clean",
];
// Remove all theme classes
root.classList.remove(...themeClasses);
// Apply the effective theme
if (themeClasses.includes(effectiveTheme)) {
root.classList.add(effectiveTheme);
} else if (effectiveTheme === "system") {
// System theme - detect OS preference
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
root.classList.add(isDark ? "dark" : "light");
}
}, [effectiveTheme, previewTheme, currentProject, theme]);
const renderView = () => {
switch (currentView) {
case "welcome":
return <WelcomeView />;
case "setup":
return <SetupView />;
case "board":
return <BoardView />;
case "spec":
return <SpecView />;
case "agent":
return <AgentView />;
case "settings":
return <SettingsView />;
case "interview":
return <InterviewView />;
case "context":
return <ContextView />;
case "profiles":
return <ProfilesView />;
case "running-agents":
return <RunningAgentsView />;
case "terminal":
return <TerminalView />;
case "wiki":
return <WikiView />;
default:
return <WelcomeView />;
}
};
// Setup view is full-screen without sidebar
if (currentView === "setup") {
return (
<main className="h-screen overflow-hidden" data-testid="app-container">
<SetupView />
</main>
);
}
return (
<main className="flex h-screen overflow-hidden" data-testid="app-container">
<Sidebar />
<div
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
style={{ marginRight: streamerPanelOpen ? "250px" : "0" }}
>
{renderView()}
</div>
{/* Hidden streamer panel - opens with "\" key, pushes content */}
<div
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
streamerPanelOpen ? "translate-x-0" : "translate-x-full"
}`}
/>
</main>
);
}
export default function Home() {
return (
<FileBrowserProvider>
<HomeContent />
</FileBrowserProvider>
);
}

View File

@@ -1,88 +0,0 @@
"use client";
import * as React from "react";
import { Sparkles, X } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface CoursePromoBadgeProps {
sidebarOpen?: boolean;
}
export function CoursePromoBadge({ sidebarOpen = true }: CoursePromoBadgeProps) {
const [dismissed, setDismissed] = React.useState(false);
if (dismissed) {
return null;
}
// Collapsed state - show only icon with tooltip
if (!sidebarOpen) {
return (
<div className="p-2 pb-0 flex justify-center">
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://agenticjumpstart.com"
target="_blank"
rel="noopener noreferrer"
className="group cursor-pointer flex items-center justify-center w-10 h-10 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-all border border-primary/30"
data-testid="course-promo-badge-collapsed"
>
<Sparkles className="size-4 shrink-0" />
</a>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-2">
<span>Become a 10x Dev</span>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDismissed(true);
}}
className="p-0.5 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
aria-label="Dismiss"
>
<X className="size-3" />
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
// Expanded state - show full badge
return (
<div className="p-2 pb-0">
<a
href="https://agenticjumpstart.com"
target="_blank"
rel="noopener noreferrer"
className="group cursor-pointer flex items-center justify-between w-full px-2 lg:px-3 py-2.5 bg-primary/10 text-primary rounded-lg font-medium text-sm hover:bg-primary/20 transition-all border border-primary/30"
data-testid="course-promo-badge"
>
<div className="flex items-center gap-2">
<Sparkles className="size-4 shrink-0" />
<span className="hidden lg:block">Become a 10x Dev</span>
</div>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDismissed(true);
}}
className="hidden lg:block p-1 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
aria-label="Dismiss"
>
<X className="size-3.5" />
</span>
</a>
</div>
);
}

View File

@@ -1,158 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Slider } from "@/components/ui/slider";
import { Play, StopCircle, Plus, Users } from "lucide-react";
import { KeyboardShortcut } from "@/hooks/use-keyboard-shortcuts";
import { useAppStore } from "@/store/app-store";
interface BoardHeaderProps {
projectName: string;
maxConcurrency: number;
onConcurrencyChange: (value: number) => void;
isAutoModeRunning: boolean;
onStartAutoMode: () => void;
onStopAutoMode: () => void;
onAddFeature: () => void;
addFeatureShortcut: KeyboardShortcut;
isMounted: boolean;
}
export function BoardHeader({
projectName,
maxConcurrency,
onConcurrencyChange,
isAutoModeRunning,
onStartAutoMode,
onStopAutoMode,
onAddFeature,
addFeatureShortcut,
isMounted,
}: BoardHeaderProps) {
const { getEffectiveTheme } = useAppStore();
const effectiveTheme = getEffectiveTheme();
const isCleanTheme = effectiveTheme === "clean";
if (isCleanTheme) {
return (
<header className="h-16 flex items-center justify-between px-8 border-b border-white/5 bg-[#0b101a]/40 backdrop-blur-md z-20 shrink-0">
<div>
<h2 className="text-lg font-bold text-white tracking-tight">Kanban Board</h2>
<p className="text-[10px] text-slate-500 uppercase tracking-[0.2em] font-bold mono">
{projectName}
</p>
</div>
<div className="flex items-center gap-5">
{/* Concurrency Display (Visual only to match mockup for now, or interactive if needed) */}
<div className="flex items-center bg-white/5 border border-white/10 rounded-full px-4 py-1.5 gap-3">
<Users className="w-4 h-4 text-slate-500" />
<div className="toggle-track">
<div className="toggle-thumb"></div>
</div>
<span className="mono text-xs font-bold text-slate-400">{maxConcurrency}</span>
</div>
{/* Auto Mode Button */}
{isAutoModeRunning ? (
<button
className="flex items-center gap-2 glass px-5 py-2 rounded-xl text-xs font-bold hover:bg-white/10 transition text-rose-400 border-rose-500/30"
onClick={onStopAutoMode}
>
<StopCircle className="w-3.5 h-3.5" /> Stop
</button>
) : (
<button
className="flex items-center gap-2 glass px-5 py-2 rounded-xl text-xs font-bold hover:bg-white/10 transition"
onClick={onStartAutoMode}
>
<Play className="w-3.5 h-3.5 text-cyan-400 fill-cyan-400" /> Auto Mode
</button>
)}
{/* Add Feature Button */}
<button
className="btn-cyan px-6 py-2 rounded-xl text-xs font-black flex items-center gap-2 shadow-lg shadow-cyan-500/20"
onClick={onAddFeature}
>
<Plus className="w-4 h-4 stroke-[3.5px]" /> ADD FEATURE
</button>
</div>
</header>
);
}
return (
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div>
<h1 className="text-xl font-bold">Kanban Board</h1>
<p className="text-sm text-muted-foreground">{projectName}</p>
</div>
<div className="flex gap-2 items-center">
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
{isMounted && (
<div
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border"
data-testid="concurrency-slider-container"
>
<Users className="w-4 h-4 text-muted-foreground" />
<Slider
value={[maxConcurrency]}
onValueChange={(value) => onConcurrencyChange(value[0])}
min={1}
max={10}
step={1}
className="w-20"
data-testid="concurrency-slider"
/>
<span
className="text-sm text-muted-foreground min-w-[2ch] text-center"
data-testid="concurrency-value"
>
{maxConcurrency}
</span>
</div>
)}
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
{isMounted && (
<>
{isAutoModeRunning ? (
<Button
variant="destructive"
size="sm"
onClick={onStopAutoMode}
data-testid="stop-auto-mode"
>
<StopCircle className="w-4 h-4 mr-2" />
Stop Auto Mode
</Button>
) : (
<Button
variant="secondary"
size="sm"
onClick={onStartAutoMode}
data-testid="start-auto-mode"
>
<Play className="w-4 h-4 mr-2" />
Auto Mode
</Button>
)}
</>
)}
<HotkeyButton
size="sm"
onClick={onAddFeature}
hotkey={addFeatureShortcut}
hotkeyActive={false}
data-testid="add-feature-button"
>
<Plus className="w-4 h-4 mr-2" />
Add Feature
</HotkeyButton>
</div>
</div>
);
}

View File

@@ -1,198 +0,0 @@
"use client";
import { memo } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
import { useAppStore } from "@/store/app-store";
interface KanbanColumnProps {
id: string;
title: string;
colorClass: string;
count: number;
children: ReactNode;
headerAction?: ReactNode;
opacity?: number;
showBorder?: boolean;
hideScrollbar?: boolean;
}
export const KanbanColumn = memo(function KanbanColumn({
id,
title,
colorClass,
count,
children,
headerAction,
opacity = 100,
showBorder = true,
hideScrollbar = false,
}: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({ id });
const { getEffectiveTheme } = useAppStore();
const effectiveTheme = getEffectiveTheme();
const isCleanTheme = effectiveTheme === "clean";
// Map column IDs to clean theme classes
const getColumnClasses = () => {
switch (id) {
case "in_progress":
return "col-in-progress";
case "waiting_approval":
return "col-waiting";
case "verified":
return "col-verified";
default:
return "";
}
};
// Map column IDs to status dot glow classes
const getStatusDotClasses = () => {
switch (id) {
case "in_progress":
return "status-dot-in-progress glow-cyan";
case "waiting_approval":
return "status-dot-waiting glow-orange";
case "verified":
return "status-dot-verified glow-green";
default:
return "";
}
};
// Clean theme column styles
if (isCleanTheme) {
const isBacklog = id === "backlog";
// Explicitly match mockup classes for status dots
const getCleanStatusDotClass = () => {
switch (id) {
case "backlog":
return "status-dot bg-slate-600";
case "in_progress":
return "status-dot bg-cyan-400 glow-cyan";
case "waiting_approval":
return "status-dot bg-orange-500 glow-orange";
case "verified":
return "status-dot bg-emerald-500 glow-green";
default:
return "status-dot bg-slate-600";
}
};
// Explicitly match mockup classes for badges
const getBadgeClass = () => {
switch (id) {
case "in_progress":
return "mono text-[10px] bg-cyan-500/10 px-2.5 py-0.5 rounded-full text-cyan-400 border border-cyan-500/20";
case "verified":
return "mono text-[10px] bg-emerald-500/10 px-2.5 py-0.5 rounded-full text-emerald-500 border border-emerald-500/20";
case "backlog":
case "waiting_approval":
default:
return "mono text-[10px] bg-white/5 px-2.5 py-0.5 rounded-full text-slate-500 border border-white/5";
}
};
return (
<div
ref={setNodeRef}
className={cn(
"flex flex-col h-full w-80 gap-5",
!isBacklog && "rounded-[2.5rem] p-3",
getColumnClasses()
)}
data-testid={`kanban-column-${id}`}
data-column-id={id}
>
{/* Header */}
<div className="flex items-center justify-between px-2 shrink-0">
<div className="flex items-center gap-3">
<span className={getCleanStatusDotClass()} />
<h3 className={cn(
"text-[11px] font-black uppercase tracking-widest",
id === "backlog" ? "text-slate-400" :
id === "in_progress" ? "text-slate-200" : "text-slate-300"
)}>
{title}
</h3>
{headerAction}
</div>
<span className={getBadgeClass()}>
{count}
</span>
</div>
{/* Content */}
<div
className={cn(
"flex-1 overflow-y-auto custom-scrollbar space-y-4",
isBacklog ? "pr-2" : "pr-1",
hideScrollbar && "scrollbar-hide"
)}
>
{children}
</div>
</div>
);
}
return (
<div
ref={setNodeRef}
className={cn(
"relative flex flex-col h-full rounded-xl transition-all duration-200 w-72 clean:w-80",
showBorder && "border border-border/60",
isOver && "ring-2 ring-primary/30 ring-offset-1 ring-offset-background",
getColumnClasses()
)}
data-testid={`kanban-column-${id}`}
data-column-id={id}
>
{/* Background layer with opacity */}
<div
className={cn(
"absolute inset-0 rounded-xl backdrop-blur-sm transition-colors duration-200",
isOver ? "bg-accent/80" : "bg-card/80"
)}
style={{ opacity: opacity / 100 }}
/>
{/* Column Header */}
<div
className={cn(
"relative z-10 flex items-center gap-3 px-3 py-2.5",
showBorder && "border-b border-border/40"
)}
>
<div className={cn("w-2.5 h-2.5 rounded-full shrink-0 status-dot", colorClass, getStatusDotClasses())} />
<h3 className="font-semibold text-sm text-foreground/90 flex-1 tracking-tight">{title}</h3>
{headerAction}
<span className="text-xs font-medium text-muted-foreground/80 bg-muted/50 px-2 py-0.5 rounded-md tabular-nums">
{count}
</span>
</div>
{/* Column Content */}
<div
className={cn(
"relative z-10 flex-1 overflow-y-auto p-2 space-y-2.5",
hideScrollbar &&
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
// Smooth scrolling
"scroll-smooth"
)}
>
{children}
</div>
{/* Drop zone indicator when dragging over */}
{isOver && (
<div className="absolute inset-0 rounded-xl bg-primary/5 pointer-events-none z-5 border-2 border-dashed border-primary/20" />
)}
</div>
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}

View File

@@ -18,24 +18,24 @@
"test:unit": "vitest run tests/unit"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.61",
"@anthropic-ai/claude-agent-sdk": "^0.1.72",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express": "^5.2.1",
"morgan": "^1.10.1",
"node-pty": "1.1.0-beta41",
"ws": "^8.18.0"
"ws": "^8.18.3"
},
"devDependencies": {
"@types/cors": "^2.8.18",
"@types/express": "^5.0.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/node": "^20",
"@types/node": "^22",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.15",
"tsx": "^4.19.4",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
"tsx": "^4.21.0",
"typescript": "^5",
"vitest": "^4.0.15"
"vitest": "^4.0.16"
}
}

View File

@@ -4,6 +4,235 @@
* This format must be included in all prompts that generate, modify, or regenerate
* app specifications to ensure consistency across the application.
*/
/**
* TypeScript interface for structured spec output
*/
export interface SpecOutput {
project_name: string;
overview: string;
technology_stack: string[];
core_capabilities: string[];
implemented_features: Array<{
name: string;
description: string;
file_locations?: string[];
}>;
additional_requirements?: string[];
development_guidelines?: string[];
implementation_roadmap?: Array<{
phase: string;
status: "completed" | "in_progress" | "pending";
description: string;
}>;
}
/**
* JSON Schema for structured spec output
* Used with Claude's structured output feature for reliable parsing
*/
export const specOutputSchema = {
type: "object",
properties: {
project_name: {
type: "string",
description: "The name of the project",
},
overview: {
type: "string",
description:
"A comprehensive description of what the project does, its purpose, and key goals",
},
technology_stack: {
type: "array",
items: { type: "string" },
description:
"List of all technologies, frameworks, libraries, and tools used",
},
core_capabilities: {
type: "array",
items: { type: "string" },
description: "List of main features and capabilities the project provides",
},
implemented_features: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the implemented feature",
},
description: {
type: "string",
description: "Description of what the feature does",
},
file_locations: {
type: "array",
items: { type: "string" },
description: "File paths where this feature is implemented",
},
},
required: ["name", "description"],
},
description: "Features that have been implemented based on code analysis",
},
additional_requirements: {
type: "array",
items: { type: "string" },
description: "Any additional requirements or constraints",
},
development_guidelines: {
type: "array",
items: { type: "string" },
description: "Development standards and practices",
},
implementation_roadmap: {
type: "array",
items: {
type: "object",
properties: {
phase: {
type: "string",
description: "Name of the implementation phase",
},
status: {
type: "string",
enum: ["completed", "in_progress", "pending"],
description: "Current status of this phase",
},
description: {
type: "string",
description: "Description of what this phase involves",
},
},
required: ["phase", "status", "description"],
},
description: "Phases or roadmap items for implementation",
},
},
required: [
"project_name",
"overview",
"technology_stack",
"core_capabilities",
"implemented_features",
],
additionalProperties: false,
};
/**
* Escape special XML characters
*/
function escapeXml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
/**
* Convert structured spec output to XML format
*/
export function specToXml(spec: SpecOutput): string {
const indent = " ";
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<project_specification>
${indent}<project_name>${escapeXml(spec.project_name)}</project_name>
${indent}<overview>
${indent}${indent}${escapeXml(spec.overview)}
${indent}</overview>
${indent}<technology_stack>
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join("\n")}
${indent}</technology_stack>
${indent}<core_capabilities>
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join("\n")}
${indent}</core_capabilities>
${indent}<implemented_features>
${spec.implemented_features
.map(
(f) => `${indent}${indent}<feature>
${indent}${indent}${indent}<name>${escapeXml(f.name)}</name>
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
f.file_locations && f.file_locations.length > 0
? `\n${indent}${indent}${indent}<file_locations>
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join("\n")}
${indent}${indent}${indent}</file_locations>`
: ""
}
${indent}${indent}</feature>`
)
.join("\n")}
${indent}</implemented_features>`;
// Optional sections
if (spec.additional_requirements && spec.additional_requirements.length > 0) {
xml += `
${indent}<additional_requirements>
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join("\n")}
${indent}</additional_requirements>`;
}
if (spec.development_guidelines && spec.development_guidelines.length > 0) {
xml += `
${indent}<development_guidelines>
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join("\n")}
${indent}</development_guidelines>`;
}
if (spec.implementation_roadmap && spec.implementation_roadmap.length > 0) {
xml += `
${indent}<implementation_roadmap>
${spec.implementation_roadmap
.map(
(r) => `${indent}${indent}<phase>
${indent}${indent}${indent}<name>${escapeXml(r.phase)}</name>
${indent}${indent}${indent}<status>${escapeXml(r.status)}</status>
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
${indent}${indent}</phase>`
)
.join("\n")}
${indent}</implementation_roadmap>`;
}
xml += `
</project_specification>`;
return xml;
}
/**
* Get prompt instruction for structured output (simpler than XML instructions)
*/
export function getStructuredSpecPromptInstruction(): string {
return `
Analyze the project and provide a comprehensive specification with:
1. **project_name**: The name of the project
2. **overview**: A comprehensive description of what the project does, its purpose, and key goals
3. **technology_stack**: List all technologies, frameworks, libraries, and tools used
4. **core_capabilities**: List the main features and capabilities the project provides
5. **implemented_features**: For each implemented feature, provide:
- name: Feature name
- description: What it does
- file_locations: Key files where it's implemented (optional)
6. **additional_requirements**: Any system requirements, dependencies, or constraints (optional)
7. **development_guidelines**: Development standards and best practices (optional)
8. **implementation_roadmap**: Project phases with status (completed/in_progress/pending) (optional)
Be thorough in your analysis. The output will be automatically formatted as structured JSON.
`;
}
export const APP_SPEC_XML_FORMAT = `
The app_spec.txt file MUST follow this exact XML format:
@@ -63,10 +292,11 @@ export function getAppSpecFormatInstruction(): string {
${APP_SPEC_XML_FORMAT}
CRITICAL FORMATTING REQUIREMENTS:
- Do NOT use the Write, Edit, or Bash tools to create files - just OUTPUT the XML in your response
- Your ENTIRE response MUST be valid XML following the exact template structure above
- Do NOT use markdown formatting (no # headers, no **bold**, no - lists, etc.)
- Do NOT include any explanatory text, prefix, or suffix outside the XML tags
- Do NOT include phrases like "Based on my analysis..." or "I'll create..." before the XML
- Do NOT include phrases like "Based on my analysis...", "I'll create...", "Let me analyze..." before the XML
- Do NOT include any text before <project_specification> or after </project_specification>
- Your response must start IMMEDIATELY with <project_specification> with no preceding text
- Your response must end IMMEDIATELY with </project_specification> with no following text

View File

@@ -53,6 +53,13 @@ export function getImagesDir(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "images");
}
/**
* Get the context files directory for a project (user-added context files)
*/
export function getContextDir(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), "context");
}
/**
* Get the worktrees metadata directory for a project
*/

View File

@@ -58,13 +58,13 @@ export const TOOL_PRESETS = {
*/
export const MAX_TURNS = {
/** Quick operations that shouldn't need many iterations */
quick: 5,
quick: 50,
/** Standard operations */
standard: 20,
standard: 100,
/** Long-running operations like full spec generation */
extended: 50,
extended: 250,
/** Very long operations that may require extensive exploration */
maximum: 1000,
@@ -143,6 +143,12 @@ export interface CreateSdkOptionsConfig {
/** Optional abort controller for cancellation */
abortController?: AbortController;
/** Optional output format for structured outputs */
outputFormat?: {
type: "json_schema";
schema: Record<string, unknown>;
};
}
/**
@@ -158,12 +164,17 @@ export function createSpecGenerationOptions(
): Options {
return {
...getBaseOptions(),
// Override permissionMode - spec generation only needs read-only tools
// Using "acceptEdits" can cause Claude to write files to unexpected locations
// See: https://github.com/AutoMaker-Org/automaker/issues/149
permissionMode: "default",
model: getModelForUseCase("spec", config.model),
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.specGeneration],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
}
@@ -180,6 +191,8 @@ export function createFeatureGenerationOptions(
): Options {
return {
...getBaseOptions(),
// Override permissionMode - feature generation only needs read-only tools
permissionMode: "default",
model: getModelForUseCase("features", config.model),
maxTurns: MAX_TURNS.quick,
cwd: config.cwd,
@@ -194,7 +207,7 @@ export function createFeatureGenerationOptions(
*
* Configuration:
* - Uses read-only tools for analysis
* - Quick turns for focused suggestions
* - Standard turns to allow thorough codebase exploration and structured output generation
* - Opus model by default for thorough analysis
*/
export function createSuggestionsOptions(
@@ -203,11 +216,12 @@ export function createSuggestionsOptions(
return {
...getBaseOptions(),
model: getModelForUseCase("suggestions", config.model),
maxTurns: MAX_TURNS.quick,
maxTurns: MAX_TURNS.extended,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
}

View File

@@ -48,6 +48,29 @@ export interface ContentBlock {
content?: string;
}
/**
* Token usage statistics from SDK execution
*/
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
cacheReadInputTokens: number;
cacheCreationInputTokens: number;
totalTokens: number;
costUSD: number;
}
/**
* Per-model usage breakdown from SDK result
*/
export interface ModelUsageData {
inputTokens: number;
outputTokens: number;
cacheReadInputTokens: number;
cacheCreationInputTokens: number;
costUSD: number;
}
/**
* Message returned by a provider (matches Claude SDK streaming format)
*/
@@ -62,6 +85,15 @@ export interface ProviderMessage {
result?: string;
error?: string;
parent_tool_use_id?: string | null;
// Token usage fields (present in result messages)
usage?: {
input_tokens: number;
output_tokens: number;
cache_read_input_tokens?: number;
cache_creation_input_tokens?: number;
};
total_cost_usd?: number;
modelUsage?: Record<string, ModelUsageData>;
}
/**

View File

@@ -6,7 +6,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
import path from "path";
import fs from "fs/promises";
import type { EventEmitter } from "../../lib/events.js";
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
import {
specOutputSchema,
specToXml,
getStructuredSpecPromptInstruction,
type SpecOutput,
} from "../../lib/app-spec-format.js";
import { createLogger } from "../../lib/logger.js";
import { createSpecGenerationOptions } from "../../lib/sdk-options.js";
import { logAuthStatus } from "./common.js";
@@ -38,7 +43,7 @@ export async function generateSpec(
if (analyzeProject !== false) {
// Default to true - analyze the project
analysisInstructions = `Based on this overview, analyze the project directory (if it exists) and create a comprehensive specification. Use the Read, Glob, and Grep tools to explore the codebase and understand:
analysisInstructions = `Based on this overview, analyze the project directory (if it exists) using the Read, Glob, and Grep tools to understand:
- Existing technologies and frameworks
- Project structure and architecture
- Current features and capabilities
@@ -66,7 +71,7 @@ ${techStackDefaults}
${analysisInstructions}
${getAppSpecFormatInstruction()}`;
${getStructuredSpecPromptInstruction()}`;
logger.info("========== PROMPT BEING SENT ==========");
logger.info(`Prompt length: ${prompt.length} chars`);
@@ -81,6 +86,10 @@ ${getAppSpecFormatInstruction()}`;
const options = createSpecGenerationOptions({
cwd: projectPath,
abortController,
outputFormat: {
type: "json_schema",
schema: specOutputSchema,
},
});
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
@@ -101,6 +110,7 @@ ${getAppSpecFormatInstruction()}`;
let responseText = "";
let messageCount = 0;
let structuredOutput: SpecOutput | null = null;
logger.info("Starting to iterate over stream...");
@@ -114,75 +124,49 @@ ${getAppSpecFormatInstruction()}`;
);
if (msg.type === "assistant") {
// Log the full message structure to debug
logger.info(`Assistant msg keys: ${Object.keys(msg).join(", ")}`);
const msgAny = msg as any;
if (msgAny.message) {
logger.info(
`msg.message keys: ${Object.keys(msgAny.message).join(", ")}`
);
if (msgAny.message.content) {
logger.info(
`msg.message.content length: ${msgAny.message.content.length}`
);
for (const block of msgAny.message.content) {
if (msgAny.message?.content) {
for (const block of msgAny.message.content) {
if (block.type === "text") {
responseText += block.text;
logger.info(
`Block keys: ${Object.keys(block).join(", ")}, type: ${
block.type
}`
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
);
if (block.type === "text") {
responseText += block.text;
logger.info(
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
);
logger.info(`Text preview: ${block.text.substring(0, 200)}...`);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
} else if (block.type === "tool_use") {
logger.info("Tool use:", block.name);
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
}
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
} else if (block.type === "tool_use") {
logger.info("Tool use:", block.name);
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
}
} else {
logger.warn("msg.message.content is falsy");
}
} else {
logger.warn("msg.message is falsy");
// Log full message to see structure
logger.info(
`Full assistant msg: ${JSON.stringify(msg).substring(0, 1000)}`
);
}
} else if (msg.type === "result" && (msg as any).subtype === "success") {
logger.info("Received success result");
logger.info(`Result value: "${(msg as any).result}"`);
logger.info(
`Current responseText length before result: ${responseText.length}`
);
// Only use result if it has content, otherwise keep accumulated text
if ((msg as any).result && (msg as any).result.length > 0) {
logger.info("Using result value as responseText");
responseText = (msg as any).result;
// Check for structured output - this is the reliable way to get spec data
const resultMsg = msg as any;
if (resultMsg.structured_output) {
structuredOutput = resultMsg.structured_output as SpecOutput;
logger.info("✅ Received structured output");
logger.debug("Structured output:", JSON.stringify(structuredOutput, null, 2));
} else {
logger.info("Result is empty, keeping accumulated responseText");
logger.warn("⚠️ No structured output in result, will fall back to text parsing");
}
} else if (msg.type === "result") {
// Handle all result types
// Handle error result types
const subtype = (msg as any).subtype;
logger.info(`Result message: subtype=${subtype}`);
if (subtype === "error_max_turns") {
logger.error(
"❌ Hit max turns limit! Claude used too many tool calls."
);
logger.info(`responseText so far: ${responseText.length} chars`);
logger.error("❌ Hit max turns limit!");
} else if (subtype === "error_max_structured_output_retries") {
logger.error("❌ Failed to produce valid structured output after retries");
throw new Error("Could not produce valid spec output");
}
} else if ((msg as { type: string }).type === "error") {
logger.error("❌ Received error message from stream:");
@@ -202,22 +186,58 @@ ${getAppSpecFormatInstruction()}`;
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
logger.info(`Response text length: ${responseText.length} chars`);
logger.info("========== FINAL RESPONSE TEXT ==========");
logger.info(responseText || "(empty)");
logger.info("========== END RESPONSE TEXT ==========");
if (!responseText || responseText.trim().length === 0) {
logger.error("❌ WARNING: responseText is empty! Nothing to save.");
// Determine XML content to save
let xmlContent: string;
if (structuredOutput) {
// Use structured output - convert JSON to XML
logger.info("✅ Using structured output for XML generation");
xmlContent = specToXml(structuredOutput);
logger.info(`Generated XML from structured output: ${xmlContent.length} chars`);
} else {
// Fallback: Extract XML content from response text
// Claude might include conversational text before/after
// See: https://github.com/AutoMaker-Org/automaker/issues/149
logger.warn("⚠️ No structured output, falling back to text parsing");
logger.info("========== FINAL RESPONSE TEXT ==========");
logger.info(responseText || "(empty)");
logger.info("========== END RESPONSE TEXT ==========");
if (!responseText || responseText.trim().length === 0) {
throw new Error("No response text and no structured output - cannot generate spec");
}
const xmlStart = responseText.indexOf("<project_specification>");
const xmlEnd = responseText.lastIndexOf("</project_specification>");
if (xmlStart !== -1 && xmlEnd !== -1) {
// Extract just the XML content, discarding any conversational text before/after
xmlContent = responseText.substring(xmlStart, xmlEnd + "</project_specification>".length);
logger.info(`Extracted XML content: ${xmlContent.length} chars (from position ${xmlStart})`);
} else {
// No valid XML structure found in the response text
// This happens when structured output was expected but not received, and the agent
// output conversational text instead of XML (e.g., "The project directory appears to be empty...")
// We should NOT save this conversational text as it's not a valid spec
logger.error("❌ Response does not contain valid <project_specification> XML structure");
logger.error("This typically happens when structured output failed and the agent produced conversational text instead of XML");
throw new Error(
"Failed to generate spec: No valid XML structure found in response. " +
"The response contained conversational text but no <project_specification> tags. " +
"Please try again."
);
}
}
// Save spec to .automaker directory
const specDir = await ensureAutomakerDir(projectPath);
await ensureAutomakerDir(projectPath);
const specPath = getAppSpecPath(projectPath);
logger.info("Saving spec to:", specPath);
logger.info(`Content to save (${responseText.length} chars)`);
logger.info(`Content to save (${xmlContent.length} chars)`);
await fs.writeFile(specPath, responseText);
await fs.writeFile(specPath, xmlContent);
// Verify the file was written
const savedContent = await fs.readFile(specPath, "utf-8");

View File

@@ -23,3 +23,4 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
}

View File

@@ -6,8 +6,6 @@
import { Router } from "express";
import type { AutoModeService } from "../../services/auto-mode-service.js";
import { createStartHandler } from "./routes/start.js";
import { createStopHandler } from "./routes/stop.js";
import { createStopFeatureHandler } from "./routes/stop-feature.js";
import { createStatusHandler } from "./routes/status.js";
import { createRunFeatureHandler } from "./routes/run-feature.js";
@@ -22,8 +20,6 @@ import { createApprovePlanHandler } from "./routes/approve-plan.js";
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router();
router.post("/start", createStartHandler(autoModeService));
router.post("/stop", createStopHandler(autoModeService));
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
router.post("/status", createStatusHandler(autoModeService));
router.post("/run-feature", createRunFeatureHandler(autoModeService));

View File

@@ -12,13 +12,14 @@ const logger = createLogger("AutoMode");
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, prompt, imagePaths, worktreePath } = req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
worktreePath?: string;
};
const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
useWorktrees?: boolean;
};
if (!projectPath || !featureId || !prompt) {
res.status(400).json({
@@ -28,14 +29,25 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return;
}
// Start follow-up in background, using the feature's worktreePath for correct branch
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
autoModeService
.followUpFeature(projectPath, featureId, prompt, imagePaths, worktreePath)
.followUpFeature(
projectPath,
featureId,
prompt,
imagePaths,
useWorktrees ?? true
)
.catch((error) => {
logger.error(
`[AutoMode] Follow up feature ${featureId} error:`,
error
);
})
.finally(() => {
// Release the starting slot when follow-up completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });

View File

@@ -19,12 +19,10 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: "projectPath and featureId are required",
});
return;
}

View File

@@ -12,30 +12,30 @@ const logger = createLogger("AutoMode");
export function createRunFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, useWorktrees, worktreePath } = req.body as {
const { projectPath, featureId, useWorktrees } = req.body as {
projectPath: string;
featureId: string;
useWorktrees?: boolean;
worktreePath?: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: "projectPath and featureId are required",
});
return;
}
// Start execution in background
// If worktreePath is provided, use it directly; otherwise let the service decide
// Default to false - worktrees should only be used when explicitly enabled
// executeFeature derives workDir from feature.branchName
autoModeService
.executeFeature(projectPath, featureId, useWorktrees ?? false, false, worktreePath)
.executeFeature(projectPath, featureId, useWorktrees ?? false, false)
.catch((error) => {
logger.error(`[AutoMode] Feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when execution completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });

View File

@@ -1,31 +0,0 @@
/**
* POST /start endpoint - Start auto mode loop
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createStartHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, maxConcurrency } = req.body as {
projectPath: string;
maxConcurrency?: number;
};
if (!projectPath) {
res
.status(400)
.json({ success: false, error: "projectPath is required" });
return;
}
await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
res.json({ success: true });
} catch (error) {
logError(error, "Start auto loop failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -1,19 +0,0 @@
/**
* POST /stop endpoint - Stop auto mode loop
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createStopHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const runningCount = await autoModeService.stopAutoLoop();
res.json({ success: true, runningFeatures: runningCount });
} catch (error) {
logError(error, "Stop auto loop failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -29,6 +29,7 @@ const BINARY_EXTENSIONS = new Set([
]);
// Status map for git status codes
// Git porcelain format uses XY where X=staging area, Y=working tree
const GIT_STATUS_MAP: Record<string, string> = {
M: "Modified",
A: "Added",
@@ -37,8 +38,42 @@ const GIT_STATUS_MAP: Record<string, string> = {
C: "Copied",
U: "Updated",
"?": "Untracked",
"!": "Ignored",
" ": "Unmodified",
};
/**
* Get a readable status text from git status codes
* Handles both single character and XY format status codes
*/
function getStatusText(indexStatus: string, workTreeStatus: string): string {
// Untracked files
if (indexStatus === "?" && workTreeStatus === "?") {
return "Untracked";
}
// Ignored files
if (indexStatus === "!" && workTreeStatus === "!") {
return "Ignored";
}
// Prioritize staging area status, then working tree
const primaryStatus = indexStatus !== " " && indexStatus !== "?" ? indexStatus : workTreeStatus;
// Handle combined statuses
if (indexStatus !== " " && indexStatus !== "?" && workTreeStatus !== " " && workTreeStatus !== "?") {
// Both staging and working tree have changes
const indexText = GIT_STATUS_MAP[indexStatus] || "Changed";
const workText = GIT_STATUS_MAP[workTreeStatus] || "Changed";
if (indexText === workText) {
return indexText;
}
return `${indexText} (staged), ${workText} (unstaged)`;
}
return GIT_STATUS_MAP[primaryStatus] || "Changed";
}
/**
* File status interface for git status results
*/
@@ -70,18 +105,46 @@ export async function isGitRepo(repoPath: string): Promise<boolean> {
/**
* Parse the output of `git status --porcelain` into FileStatus array
* Git porcelain format: XY PATH where X=staging area status, Y=working tree status
* For renamed files: XY ORIG_PATH -> NEW_PATH
*/
export function parseGitStatus(statusOutput: string): FileStatus[] {
return statusOutput
.split("\n")
.filter(Boolean)
.map((line) => {
const statusChar = line[0];
const filePath = line.slice(3);
// Git porcelain format uses two status characters: XY
// X = status in staging area (index)
// Y = status in working tree
const indexStatus = line[0] || " ";
const workTreeStatus = line[1] || " ";
// File path starts at position 3 (after "XY ")
let filePath = line.slice(3);
// Handle renamed files (format: "R old_path -> new_path")
if (indexStatus === "R" || workTreeStatus === "R") {
const arrowIndex = filePath.indexOf(" -> ");
if (arrowIndex !== -1) {
filePath = filePath.slice(arrowIndex + 4); // Use new path
}
}
// Determine the primary status character for backwards compatibility
// Prioritize staging area status, then working tree
let primaryStatus: string;
if (indexStatus === "?" && workTreeStatus === "?") {
primaryStatus = "?"; // Untracked
} else if (indexStatus !== " " && indexStatus !== "?") {
primaryStatus = indexStatus; // Staged change
} else {
primaryStatus = workTreeStatus; // Working tree change
}
return {
status: statusChar,
status: primaryStatus,
path: filePath,
statusText: GIT_STATUS_MAP[statusChar] || "Unknown",
statusText: getStatusText(indexStatus, workTreeStatus),
};
});
}

View File

@@ -9,7 +9,7 @@ import { getErrorMessage, logError } from "../common.js";
// Optional files that are expected to not exist in new projects
// Don't log ENOENT errors for these to reduce noise
const OPTIONAL_FILES = ["categories.json"];
const OPTIONAL_FILES = ["categories.json", "app_spec.txt"];
function isOptionalFile(filePath: string): boolean {
return OPTIONAL_FILES.some((optionalFile) => filePath.endsWith(optionalFile));

View File

@@ -16,7 +16,6 @@ export function createIndexHandler(autoModeService: AutoModeService) {
success: true,
runningAgents,
totalCount: runningAgents.length,
autoLoopRunning: status.autoLoopRunning,
});
} catch (error) {
logError(error, "Get running agents failed");

View File

@@ -103,3 +103,4 @@ export function createDeleteApiKeyHandler() {
}

View File

@@ -9,6 +9,39 @@ import { createSuggestionsOptions } from "../../lib/sdk-options.js";
const logger = createLogger("Suggestions");
/**
* JSON Schema for suggestions output
*/
const suggestionsSchema = {
type: "object",
properties: {
suggestions: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
category: { type: "string" },
description: { type: "string" },
steps: {
type: "array",
items: { type: "string" },
},
priority: {
type: "number",
minimum: 1,
maximum: 3,
},
reasoning: { type: "string" },
},
required: ["category", "description", "steps", "priority", "reasoning"],
},
},
},
required: ["suggestions"],
additionalProperties: false,
};
export async function generateSuggestions(
projectPath: string,
suggestionType: string,
@@ -36,19 +69,7 @@ For each suggestion, provide:
4. Priority (1=high, 2=medium, 3=low)
5. Brief reasoning for why this would help
Format your response as JSON:
{
"suggestions": [
{
"id": "suggestion-123",
"category": "Category",
"description": "What to implement",
"steps": ["Step 1", "Step 2"],
"priority": 1,
"reasoning": "Why this helps"
}
]
}`;
The response will be automatically formatted as structured JSON.`;
events.emit("suggestions:event", {
type: "suggestions_progress",
@@ -58,16 +79,21 @@ Format your response as JSON:
const options = createSuggestionsOptions({
cwd: projectPath,
abortController,
outputFormat: {
type: "json_schema",
schema: suggestionsSchema,
},
});
const stream = query({ prompt, options });
let responseText = "";
let structuredOutput: { suggestions: Array<Record<string, unknown>> } | null = null;
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
responseText += block.text;
events.emit("suggestions:event", {
type: "suggestions_progress",
content: block.text,
@@ -81,18 +107,34 @@ Format your response as JSON:
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
responseText = msg.result || responseText;
// Check for structured output
const resultMsg = msg as any;
if (resultMsg.structured_output) {
structuredOutput = resultMsg.structured_output as {
suggestions: Array<Record<string, unknown>>;
};
logger.debug("Received structured output:", structuredOutput);
}
} else if (msg.type === "result") {
const resultMsg = msg as any;
if (resultMsg.subtype === "error_max_structured_output_retries") {
logger.error("Failed to produce valid structured output after retries");
throw new Error("Could not produce valid suggestions output");
} else if (resultMsg.subtype === "error_max_turns") {
logger.error("Hit max turns limit before completing suggestions generation");
logger.warn(`Response text length: ${responseText.length} chars`);
// Still try to parse what we have
}
}
}
// Parse suggestions from response
// Use structured output if available, otherwise fall back to parsing text
try {
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
if (structuredOutput && structuredOutput.suggestions) {
// Use structured output directly
events.emit("suggestions:event", {
type: "suggestions_complete",
suggestions: parsed.suggestions.map(
suggestions: structuredOutput.suggestions.map(
(s: Record<string, unknown>, i: number) => ({
...s,
id: s.id || `suggestion-${Date.now()}-${i}`,
@@ -100,7 +142,23 @@ Format your response as JSON:
),
});
} else {
throw new Error("No valid JSON found in response");
// Fallback: try to parse from text (for backwards compatibility)
logger.warn("No structured output received, attempting to parse from text");
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
events.emit("suggestions:event", {
type: "suggestions_complete",
suggestions: parsed.suggestions.map(
(s: Record<string, unknown>, i: number) => ({
...s,
id: s.id || `suggestion-${Date.now()}-${i}`,
})
),
});
} else {
throw new Error("No valid JSON found in response");
}
}
} catch (error) {
// Log the parsing error for debugging

View File

@@ -5,13 +5,20 @@
import { createLogger } from "../../lib/logger.js";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { FeatureLoader } from "../../services/feature-loader.js";
const logger = createLogger("Worktree");
const execAsync = promisify(exec);
const featureLoader = new FeatureLoader();
export const AUTOMAKER_INITIAL_COMMIT_MESSAGE =
"chore: automaker initial commit";
/**
* Normalize path separators to forward slashes for cross-platform consistency.
@@ -73,3 +80,30 @@ export function logWorktreeError(
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
export const logError = createLogError(logger);
/**
* Ensure the repository has at least one commit so git commands that rely on HEAD work.
* Returns true if an empty commit was created, false if the repo already had commits.
*/
export async function ensureInitialCommit(repoPath: string): Promise<boolean> {
try {
await execAsync("git rev-parse --verify HEAD", { cwd: repoPath });
return false;
} catch {
try {
await execAsync(
`git commit --allow-empty -m "${AUTOMAKER_INITIAL_COMMIT_MESSAGE}"`,
{ cwd: repoPath }
);
logger.info(
`[Worktree] Created initial empty commit to enable worktrees in ${repoPath}`
);
return true;
} catch (error) {
const reason = getErrorMessageShared(error);
throw new Error(
`Failed to create initial git commit. Please commit manually and retry. ${reason}`
);
}
}
}

View File

@@ -12,7 +12,13 @@ import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import { mkdir } from "fs/promises";
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
import {
isGitRepo,
getErrorMessage,
logError,
normalizePath,
ensureInitialCommit,
} from "../common.js";
import { trackBranch } from "./branch-tracking.js";
const execAsync = promisify(exec);
@@ -93,6 +99,9 @@ export function createCreateHandler() {
return;
}
// Ensure the repository has at least one commit so worktree commands referencing HEAD succeed
await ensureInitialCommit(projectPath);
// First, check if git already has a worktree for this branch (anywhere)
const existingWorktree = await findExistingWorktreeForBranch(projectPath, branchName);
if (existingWorktree) {

View File

@@ -38,8 +38,10 @@ export function createListBranchesHandler() {
const currentBranch = currentBranchOutput.trim();
// List all local branches
// Use double quotes around the format string for cross-platform compatibility
// Single quotes are preserved literally on Windows; double quotes work on both
const { stdout: branchesOutput } = await execAsync(
"git branch --format='%(refname:short)'",
'git branch --format="%(refname:short)"',
{ cwd: worktreePath }
);
@@ -47,11 +49,15 @@ export function createListBranchesHandler() {
.trim()
.split("\n")
.filter((b) => b.trim())
.map((name) => ({
name: name.trim(),
isCurrent: name.trim() === currentBranch,
isRemote: false,
}));
.map((name) => {
// Remove any surrounding quotes (Windows git may preserve them)
const cleanName = name.trim().replace(/^['"]|['"]$/g, "");
return {
name: cleanName,
isCurrent: cleanName === currentBranch,
isRemote: false,
};
});
// Get ahead/behind count for current branch
let aheadCount = 0;

View File

@@ -8,6 +8,7 @@
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import { existsSync } from "fs";
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
const execAsync = promisify(exec);
@@ -58,10 +59,12 @@ export function createListHandler() {
});
const worktrees: WorktreeInfo[] = [];
const removedWorktrees: Array<{ path: string; branch: string }> = [];
const lines = stdout.split("\n");
let current: { path?: string; branch?: string } = {};
let isFirst = true;
// First pass: detect removed worktrees
for (const line of lines) {
if (line.startsWith("worktree ")) {
current.path = normalizePath(line.slice(9));
@@ -69,19 +72,40 @@ export function createListHandler() {
current.branch = line.slice(7).replace("refs/heads/", "");
} else if (line === "") {
if (current.path && current.branch) {
worktrees.push({
path: current.path,
branch: current.branch,
isMain: isFirst,
isCurrent: current.branch === currentBranch,
hasWorktree: true,
});
isFirst = false;
const isMainWorktree = isFirst;
// Check if the worktree directory actually exists
// Skip checking/pruning the main worktree (projectPath itself)
if (!isMainWorktree && !existsSync(current.path)) {
// Worktree directory doesn't exist - it was manually deleted
removedWorktrees.push({
path: current.path,
branch: current.branch,
});
} else {
// Worktree exists (or is main worktree), add it to the list
worktrees.push({
path: current.path,
branch: current.branch,
isMain: isMainWorktree,
isCurrent: current.branch === currentBranch,
hasWorktree: true,
});
isFirst = false;
}
}
current = {};
}
}
// Prune removed worktrees from git (only if any were detected)
if (removedWorktrees.length > 0) {
try {
await execAsync("git worktree prune", { cwd: projectPath });
} catch {
// Prune failed, but we'll still report the removed worktrees
}
}
// If includeDetails is requested, fetch change status for each worktree
if (includeDetails) {
for (const worktree of worktrees) {
@@ -103,7 +127,11 @@ export function createListHandler() {
}
}
res.json({ success: true, worktrees });
res.json({
success: true,
worktrees,
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
});
} catch (error) {
logError(error, "List worktrees failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });

View File

@@ -10,7 +10,7 @@
*/
import { ProviderFactory } from "../providers/provider-factory.js";
import type { ExecuteOptions } from "../providers/types.js";
import type { ExecuteOptions, TokenUsage } from "../providers/types.js";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
@@ -19,15 +19,11 @@ import type { EventEmitter } from "../lib/events.js";
import { buildPromptWithImages } from "../lib/prompt-builder.js";
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
import { createAutoModeOptions } from "../lib/sdk-options.js";
import { classifyError } from "../lib/error-handler.js";
import { isAbortError, classifyError } from "../lib/error-handler.js";
import { resolveDependencies, areDependenciesSatisfied } from "../lib/dependency-resolver.js";
import type { Feature } from "./feature-loader.js";
import {
getFeatureDir,
getFeaturesDir,
getAutomakerDir,
getWorktreesDir,
} from "../lib/automaker-paths.js";
import { FeatureLoader } from "./feature-loader.js";
import { getFeatureDir, getAutomakerDir, getFeaturesDir, getContextDir } from "../lib/automaker-paths.js";
const execAsync = promisify(exec);
@@ -313,10 +309,11 @@ interface RunningFeature {
startTime: number;
}
interface AutoModeConfig {
maxConcurrency: number;
useWorktrees: boolean;
interface AutoLoopState {
projectPath: string;
maxConcurrency: number;
abortController: AbortController;
isRunning: boolean;
}
interface PendingApproval {
@@ -326,9 +323,17 @@ interface PendingApproval {
projectPath: string;
}
interface AutoModeConfig {
maxConcurrency: number;
useWorktrees: boolean;
projectPath: string;
}
export class AutoModeService {
private events: EventEmitter;
private runningFeatures = new Map<string, RunningFeature>();
private autoLoop: AutoLoopState | null = null;
private featureLoader = new FeatureLoader();
private autoLoopRunning = false;
private autoLoopAbortController: AbortController | null = null;
private config: AutoModeConfig | null = null;
@@ -452,7 +457,6 @@ export class AutoModeService {
* @param featureId - The feature ID to execute
* @param useWorktrees - Whether to use worktrees for isolation
* @param isAutoMode - Whether this is running in auto mode
* @param providedWorktreePath - Optional: use this worktree path instead of creating a new one
*/
async executeFeature(
projectPath: string,
@@ -465,79 +469,98 @@ export class AutoModeService {
}
): Promise<void> {
if (this.runningFeatures.has(featureId)) {
throw new Error(`Feature ${featureId} is already running`);
throw new Error("already running");
}
// Add to running features immediately to prevent race conditions
const abortController = new AbortController();
const branchName = `feature/${featureId}`;
let worktreePath: string | null = null;
// Use provided worktree path if given, otherwise setup new worktree if enabled
if (providedWorktreePath) {
// Resolve to absolute path - critical for cross-platform compatibility
// On Windows, relative paths or paths with forward slashes may not work correctly with cwd
// On all platforms, absolute paths ensure commands execute in the correct directory
try {
// Resolve relative paths relative to projectPath, absolute paths as-is
const resolvedPath = path.isAbsolute(providedWorktreePath)
? path.resolve(providedWorktreePath)
: path.resolve(projectPath, providedWorktreePath);
// Verify the path exists before using it
await fs.access(resolvedPath);
worktreePath = resolvedPath;
console.log(`[AutoMode] Using provided worktree path (resolved): ${worktreePath}`);
} catch (error) {
console.error(`[AutoMode] Provided worktree path invalid or doesn't exist: ${providedWorktreePath}`, error);
// Fall through to create new worktree or use project path
}
}
if (!worktreePath && useWorktrees) {
// No specific worktree provided, create a new one for this feature
worktreePath = await this.setupWorktree(
projectPath,
featureId,
branchName
);
}
// Ensure workDir is always an absolute path for cross-platform compatibility
const workDir = worktreePath ? path.resolve(worktreePath) : path.resolve(projectPath);
this.runningFeatures.set(featureId, {
const tempRunningFeature: RunningFeature = {
featureId,
projectPath,
worktreePath,
branchName,
worktreePath: null,
branchName: null,
abortController,
isAutoMode,
startTime: Date.now(),
});
// Emit feature start event
this.emitAutoModeEvent("auto_mode_feature_start", {
featureId,
projectPath,
feature: {
id: featureId,
title: "Loading...",
description: "Feature is starting",
},
});
};
this.runningFeatures.set(featureId, tempRunningFeature);
try {
// Load feature details
// Check if feature has existing context - if so, resume instead of starting fresh
// Skip this check if we're already being called with a continuation prompt (from resumeFeature)
if (!options?.continuationPrompt) {
const hasExistingContext = await this.contextExists(
projectPath,
featureId
);
if (hasExistingContext) {
console.log(
`[AutoMode] Feature ${featureId} has existing context, resuming instead of starting fresh`
);
// Remove from running features temporarily, resumeFeature will add it back
this.runningFeatures.delete(featureId);
return this.resumeFeature(projectPath, featureId, useWorktrees);
}
}
// Emit feature start event early
this.emitAutoModeEvent("auto_mode_feature_start", {
featureId,
projectPath,
feature: {
id: featureId,
title: "Loading...",
description: "Feature is starting",
},
});
// Load feature details FIRST to get branchName
const feature = await this.loadFeature(projectPath, featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
// Derive workDir from feature.branchName
// Worktrees should already be created when the feature is added/edited
let worktreePath: string | null = null;
const branchName = feature.branchName;
if (useWorktrees && branchName) {
// Try to find existing worktree for this branch
// Worktree should already exist (created when feature was added/edited)
worktreePath = await this.findExistingWorktreeForBranch(
projectPath,
branchName
);
if (worktreePath) {
console.log(
`[AutoMode] Using worktree for branch "${branchName}": ${worktreePath}`
);
} else {
// Worktree doesn't exist - log warning and continue with project path
console.warn(
`[AutoMode] Worktree for branch "${branchName}" not found, using project path`
);
}
}
// Ensure workDir is always an absolute path for cross-platform compatibility
const workDir = worktreePath
? path.resolve(worktreePath)
: path.resolve(projectPath);
// Update running feature with actual worktree info
tempRunningFeature.worktreePath = worktreePath;
tempRunningFeature.branchName = branchName ?? null;
// Update feature status to in_progress
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
// Build the prompt - use continuation prompt if provided (for recovery after plan approval)
let prompt: string;
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
const contextFiles = await this.loadContextFiles(projectPath);
if (options?.continuationPrompt) {
// Continuation prompt is used when recovering from a plan approval
// The plan was already approved, so skip the planning phase
@@ -567,21 +590,24 @@ export class AutoModeService {
// Get model from feature
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
console.log(
`[AutoMode] Executing feature ${featureId} with model: ${model}`
`[AutoMode] Executing feature ${featureId} with model: ${model} in ${workDir}`
);
// Run the agent with the feature's model and images
// Context files are passed as system prompt for higher priority
await this.runAgent(
workDir,
featureId,
prompt,
abortController,
projectPath,
imagePaths,
model,
{
projectPath,
planningMode: feature.planningMode,
requirePlanApproval: feature.requirePlanApproval,
systemPrompt: contextFiles || undefined,
}
);
@@ -596,7 +622,7 @@ export class AutoModeService {
featureId,
passes: true,
message: `Feature completed in ${Math.round(
(Date.now() - this.runningFeatures.get(featureId)!.startTime) / 1000
(Date.now() - tempRunningFeature.startTime) / 1000
)}s`,
projectPath,
});
@@ -651,6 +677,10 @@ export class AutoModeService {
featureId: string,
useWorktrees = false
): Promise<void> {
if (this.runningFeatures.has(featureId)) {
throw new Error("already running");
}
// Check if context exists in .automaker directory
const featureDir = getFeatureDir(projectPath, featureId);
const contextPath = path.join(featureDir, "agent-output.md");
@@ -674,7 +704,9 @@ export class AutoModeService {
);
}
// No context, start fresh
// No context, start fresh - executeFeature will handle adding to runningFeatures
// Remove the temporary entry we added
this.runningFeatures.delete(featureId);
return this.executeFeature(projectPath, featureId, useWorktrees, false);
}
@@ -686,7 +718,7 @@ export class AutoModeService {
featureId: string,
prompt: string,
imagePaths?: string[],
providedWorktreePath?: string
useWorktrees = true
): Promise<void> {
if (this.runningFeatures.has(featureId)) {
throw new Error(`Feature ${featureId} is already running`);
@@ -694,32 +726,30 @@ export class AutoModeService {
const abortController = new AbortController();
// Use the provided worktreePath (from the feature's assigned branch)
// Fall back to project path if not provided
// Load feature info for context FIRST to get branchName
const feature = await this.loadFeature(projectPath, featureId);
// Derive workDir from feature.branchName
// If no branchName, derive from feature ID: feature/{featureId}
let workDir = path.resolve(projectPath);
let worktreePath: string | null = null;
const branchName = feature?.branchName || `feature/${featureId}`;
if (providedWorktreePath) {
try {
// Resolve to absolute path - critical for cross-platform compatibility
// On Windows, relative paths or paths with forward slashes may not work correctly with cwd
// On all platforms, absolute paths ensure commands execute in the correct directory
const resolvedPath = path.isAbsolute(providedWorktreePath)
? path.resolve(providedWorktreePath)
: path.resolve(projectPath, providedWorktreePath);
await fs.access(resolvedPath);
workDir = resolvedPath;
worktreePath = resolvedPath;
} catch {
// Worktree path provided but doesn't exist, use project path
console.log(`[AutoMode] Provided worktreePath doesn't exist: ${providedWorktreePath}, using project path`);
if (useWorktrees && branchName) {
// Try to find existing worktree for this branch
worktreePath = await this.findExistingWorktreeForBranch(
projectPath,
branchName
);
if (worktreePath) {
workDir = worktreePath;
console.log(
`[AutoMode] Follow-up using worktree for branch "${branchName}": ${workDir}`
);
}
}
// Load feature info for context
const feature = await this.loadFeature(projectPath, featureId);
// Load previous agent output if it exists
const featureDir = getFeatureDir(projectPath, featureId);
const contextPath = path.join(featureDir, "agent-output.md");
@@ -730,6 +760,9 @@ export class AutoModeService {
// No previous context
}
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
const contextFiles = await this.loadContextFiles(projectPath);
// Build complete prompt with feature info, previous context, and follow-up instructions
let fullPrompt = `## Follow-up on Feature Implementation
@@ -756,7 +789,7 @@ Address the follow-up instructions above. Review the previous work and make the
featureId,
projectPath,
worktreePath,
branchName: worktreePath ? path.basename(worktreePath) : null,
branchName,
abortController,
isAutoMode: false,
startTime: Date.now(),
@@ -848,17 +881,20 @@ Address the follow-up instructions above. Review the previous work and make the
// Use fullPrompt (already built above) with model and all images
// Note: Follow-ups skip planning mode - they continue from previous work
// Pass previousContext so the history is preserved in the output file
// Context files are passed as system prompt for higher priority
await this.runAgent(
workDir,
featureId,
fullPrompt,
abortController,
projectPath,
allImagePaths.length > 0 ? allImagePaths : imagePaths,
model,
{
projectPath,
planningMode: 'skip', // Follow-ups don't require approval
previousContent: previousContext || undefined,
systemPrompt: contextFiles || undefined,
}
);
@@ -975,17 +1011,25 @@ Address the follow-up instructions above. Review the previous work and make the
workDir = providedWorktreePath;
console.log(`[AutoMode] Committing in provided worktree: ${workDir}`);
} catch {
console.log(`[AutoMode] Provided worktree path doesn't exist: ${providedWorktreePath}, using project path`);
console.log(
`[AutoMode] Provided worktree path doesn't exist: ${providedWorktreePath}, using project path`
);
}
} else {
// Fallback: try to find worktree at legacy location
const legacyWorktreePath = path.join(projectPath, ".worktrees", featureId);
const legacyWorktreePath = path.join(
projectPath,
".worktrees",
featureId
);
try {
await fs.access(legacyWorktreePath);
workDir = legacyWorktreePath;
console.log(`[AutoMode] Committing in legacy worktree: ${workDir}`);
} catch {
console.log(`[AutoMode] No worktree found, committing in project path: ${workDir}`);
console.log(
`[AutoMode] No worktree found, committing in project path: ${workDir}`
);
}
}
@@ -1049,6 +1093,65 @@ Address the follow-up instructions above. Review the previous work and make the
}
}
/**
* Load context files from .automaker/context/ directory
* These are user-defined context files (CLAUDE.md, CODE_QUALITY.md, etc.)
* that provide project-specific rules and guidelines for the agent.
*/
private async loadContextFiles(projectPath: string): Promise<string> {
// Use path.resolve for cross-platform absolute path handling
const contextDir = path.resolve(getContextDir(projectPath));
try {
// Check if directory exists first
await fs.access(contextDir);
const files = await fs.readdir(contextDir);
// Filter for text-based context files (case-insensitive for Windows)
const textFiles = files.filter((f) => {
const lower = f.toLowerCase();
return lower.endsWith(".md") || lower.endsWith(".txt");
});
if (textFiles.length === 0) return "";
const contents: string[] = [];
for (const file of textFiles) {
// Use path.join for cross-platform path construction
const filePath = path.join(contextDir, file);
const content = await fs.readFile(filePath, "utf-8");
contents.push(`## ${file}\n\n${content}`);
}
console.log(
`[AutoMode] Loaded ${textFiles.length} context file(s): ${textFiles.join(", ")}`
);
return `# ⚠️ CRITICAL: Project Context Files - READ AND FOLLOW STRICTLY
**IMPORTANT**: The following context files contain MANDATORY project-specific rules and conventions. You MUST:
1. Read these rules carefully before taking any action
2. Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`)
3. Follow ALL coding conventions, commit message formats, and architectural patterns specified
4. Reference these rules before running ANY shell commands or making commits
Failure to follow these rules will result in broken builds, failed CI, and rejected commits.
${contents.join("\n\n---\n\n")}
---
**REMINDER**: Before running any command, verify you are using the correct package manager and following the conventions above.
---
`;
} catch {
// Context directory doesn't exist or is empty - this is fine
return "";
}
}
/**
* Analyze project to gather context
*/
@@ -1135,18 +1238,17 @@ Format your response as a structured markdown document.`;
}
}
/**
* Get current status
*/
getStatus(): {
isRunning: boolean;
autoLoopRunning: boolean;
runningFeatures: string[];
runningCount: number;
} {
return {
isRunning: this.autoLoopRunning || this.runningFeatures.size > 0,
autoLoopRunning: this.autoLoopRunning,
isRunning: this.runningFeatures.size > 0,
runningFeatures: Array.from(this.runningFeatures.keys()),
runningCount: this.runningFeatures.size,
};
@@ -1323,6 +1425,7 @@ Format your response as a structured markdown document.`;
// Private helpers
/**
* Find an existing worktree for a given branch by checking git worktree list
*/
@@ -1375,55 +1478,6 @@ Format your response as a structured markdown document.`;
}
}
private async setupWorktree(
projectPath: string,
featureId: string,
branchName: string
): Promise<string> {
// First, check if git already has a worktree for this branch (anywhere)
const existingWorktree = await this.findExistingWorktreeForBranch(projectPath, branchName);
if (existingWorktree) {
// Path is already resolved to absolute in findExistingWorktreeForBranch
console.log(`[AutoMode] Found existing worktree for branch "${branchName}" at: ${existingWorktree}`);
return existingWorktree;
}
// Git worktrees stay in project directory
const worktreesDir = path.join(projectPath, ".worktrees");
const worktreePath = path.join(worktreesDir, featureId);
await fs.mkdir(worktreesDir, { recursive: true });
// Check if worktree directory already exists (might not be linked to branch)
try {
await fs.access(worktreePath);
// Return absolute path for cross-platform compatibility
return path.resolve(worktreePath);
} catch {
// Create new worktree
}
// Create branch if it doesn't exist
try {
await execAsync(`git branch ${branchName}`, { cwd: projectPath });
} catch {
// Branch may already exist
}
// Create worktree
try {
await execAsync(`git worktree add "${worktreePath}" ${branchName}`, {
cwd: projectPath,
});
// Return absolute path for cross-platform compatibility
return path.resolve(worktreePath);
} catch (error) {
// Worktree creation failed, fall back to direct execution
console.error(`[AutoMode] Worktree creation failed:`, error);
return path.resolve(projectPath);
}
}
private async loadFeature(
projectPath: string,
featureId: string
@@ -1683,6 +1737,7 @@ This helps parse your summary correctly in the output logs.`;
featureId: string,
prompt: string,
abortController: AbortController,
projectPath: string,
imagePaths?: string[],
model?: string,
options?: {
@@ -1690,9 +1745,10 @@ This helps parse your summary correctly in the output logs.`;
planningMode?: PlanningMode;
requirePlanApproval?: boolean;
previousContent?: string;
systemPrompt?: string;
}
): Promise<void> {
const projectPath = options?.projectPath || workDir;
const finalProjectPath = options?.projectPath || projectPath;
const planningMode = options?.planningMode || 'skip';
const previousContent = options?.previousContent;
@@ -1708,7 +1764,9 @@ This helps parse your summary correctly in the output logs.`;
// CI/CD Mock Mode: Return early with mock response when AUTOMAKER_MOCK_AGENT is set
// This prevents actual API calls during automated testing
if (process.env.AUTOMAKER_MOCK_AGENT === "true") {
console.log(`[AutoMode] MOCK MODE: Skipping real agent execution for feature ${featureId}`);
console.log(
`[AutoMode] MOCK MODE: Skipping real agent execution for feature ${featureId}`
);
// Simulate some work being done
await this.sleep(500);
@@ -1740,8 +1798,7 @@ This helps parse your summary correctly in the output logs.`;
await this.sleep(200);
// Save mock agent output
const configProjectPath = this.config?.projectPath || workDir;
const featureDirForOutput = getFeatureDir(configProjectPath, featureId);
const featureDirForOutput = getFeatureDir(projectPath, featureId);
const outputPath = path.join(featureDirForOutput, "agent-output.md");
const mockOutput = `# Mock Agent Output
@@ -1759,7 +1816,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, mockOutput);
console.log(`[AutoMode] MOCK MODE: Completed mock execution for feature ${featureId}`);
console.log(
`[AutoMode] MOCK MODE: Completed mock execution for feature ${featureId}`
);
return;
}
@@ -1794,6 +1853,13 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
false // don't duplicate paths in text
);
// Debug: Log if system prompt is provided
if (options?.systemPrompt) {
console.log(
`[AutoMode] System prompt provided (${options.systemPrompt.length} chars), first 200 chars:\n${options.systemPrompt.substring(0, 200)}...`
);
}
const executeOptions: ExecuteOptions = {
prompt: promptContent,
model: finalModel,
@@ -1801,6 +1867,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
cwd: workDir,
allowedTools: allowedTools,
abortController,
systemPrompt: options?.systemPrompt,
};
// Execute via provider
@@ -1811,11 +1878,33 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
: "";
let specDetected = false;
// Token usage accumulator - tracks total usage across all streams
const accumulatedUsage: TokenUsage = {
inputTokens: 0,
outputTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalTokens: 0,
costUSD: 0,
};
// Helper to accumulate usage from a result message
const accumulateUsage = (msg: { usage?: { input_tokens: number; output_tokens: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number }; total_cost_usd?: number }): void => {
if (msg.usage) {
accumulatedUsage.inputTokens += msg.usage.input_tokens || 0;
accumulatedUsage.outputTokens += msg.usage.output_tokens || 0;
accumulatedUsage.cacheReadInputTokens += msg.usage.cache_read_input_tokens || 0;
accumulatedUsage.cacheCreationInputTokens += msg.usage.cache_creation_input_tokens || 0;
accumulatedUsage.totalTokens = accumulatedUsage.inputTokens + accumulatedUsage.outputTokens;
}
if (msg.total_cost_usd !== undefined) {
accumulatedUsage.costUSD = msg.total_cost_usd;
}
};
// Agent output goes to .automaker directory
// Note: We use the original projectPath here (from config), not workDir
// because workDir might be a worktree path
const configProjectPath = this.config?.projectPath || workDir;
const featureDirForOutput = getFeatureDir(configProjectPath, featureId);
// Note: We use projectPath here, not workDir, because workDir might be a worktree path
const featureDirForOutput = getFeatureDir(projectPath, featureId);
const outputPath = path.join(featureDirForOutput, "agent-output.md");
// Incremental file writing state
@@ -1829,7 +1918,10 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
await fs.writeFile(outputPath, responseText);
} catch (error) {
// Log but don't crash - file write errors shouldn't stop execution
console.error(`[AutoMode] Failed to write agent output for ${featureId}:`, error);
console.error(
`[AutoMode] Failed to write agent output for ${featureId}:`,
error
);
}
};
@@ -1848,11 +1940,11 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
for (const block of msg.message.content) {
if (block.type === "text") {
// Add separator before new text if we already have content and it doesn't end with newlines
if (responseText.length > 0 && !responseText.endsWith('\n\n')) {
if (responseText.endsWith('\n')) {
responseText += '\n';
if (responseText.length > 0 && !responseText.endsWith("\n\n")) {
if (responseText.endsWith("\n")) {
responseText += "\n";
} else {
responseText += '\n\n';
responseText += "\n\n";
}
}
responseText += block.text || "";
@@ -2033,6 +2125,7 @@ After generating the revised spec, output:
throw new Error(msg.error || "Error during plan revision");
} else if (msg.type === "result" && msg.subtype === "success") {
revisionText += msg.result || "";
accumulateUsage(msg);
}
}
@@ -2170,6 +2263,7 @@ After generating the revised spec, output:
} else if (msg.type === "result" && msg.subtype === "success") {
taskOutput += msg.result || "";
responseText += msg.result || "";
accumulateUsage(msg);
}
}
@@ -2250,6 +2344,7 @@ Implement all the changes described in the plan above.`;
throw new Error(msg.error || "Unknown error during implementation");
} else if (msg.type === "result" && msg.subtype === "success") {
responseText += msg.result || "";
accumulateUsage(msg);
}
}
}
@@ -2275,12 +2370,16 @@ Implement all the changes described in the plan above.`;
});
// Also add to file output for persistence
if (responseText.length > 0 && !responseText.endsWith('\n')) {
responseText += '\n';
if (responseText.length > 0 && !responseText.endsWith("\n")) {
responseText += "\n";
}
responseText += `\n🔧 Tool: ${block.name}\n`;
if (block.input) {
responseText += `Input: ${JSON.stringify(block.input, null, 2)}\n`;
responseText += `Input: ${JSON.stringify(
block.input,
null,
2
)}\n`;
}
scheduleWrite();
}
@@ -2293,6 +2392,8 @@ Implement all the changes described in the plan above.`;
// The msg.result is just a summary which would lose all tool use details
// Just ensure final write happens
scheduleWrite();
// Capture token usage from the result message
accumulateUsage(msg);
}
}
@@ -2302,6 +2403,31 @@ Implement all the changes described in the plan above.`;
}
// Final write - ensure all accumulated content is saved
await writeToFile();
// Save token usage to the feature if any tokens were consumed
if (accumulatedUsage.totalTokens > 0) {
try {
// Load existing feature to check for previous token usage
const existingFeature = await this.featureLoader.get(projectPath, featureId);
if (existingFeature) {
// If feature already has token usage, add to it (for follow-ups)
const existingUsage = existingFeature.tokenUsage;
if (existingUsage) {
accumulatedUsage.inputTokens += existingUsage.inputTokens;
accumulatedUsage.outputTokens += existingUsage.outputTokens;
accumulatedUsage.cacheReadInputTokens += existingUsage.cacheReadInputTokens;
accumulatedUsage.cacheCreationInputTokens += existingUsage.cacheCreationInputTokens;
accumulatedUsage.totalTokens = accumulatedUsage.inputTokens + accumulatedUsage.outputTokens;
accumulatedUsage.costUSD += existingUsage.costUSD;
}
}
await this.featureLoader.update(projectPath, featureId, { tokenUsage: accumulatedUsage });
console.log(`[AutoMode] Token usage:`, accumulatedUsage);
console.log(`[AutoMode] Saved token usage for ${featureId}: ${accumulatedUsage.totalTokens} tokens, $${accumulatedUsage.costUSD.toFixed(4)}`);
} catch (error) {
console.error(`[AutoMode] Failed to save token usage for ${featureId}:`, error);
}
}
}
private async executeFeatureWithContext(
@@ -2420,7 +2546,28 @@ Begin implementing task ${task.id} now.`;
});
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
private sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, ms);
// If signal is provided and already aborted, reject immediately
if (signal?.aborted) {
clearTimeout(timeout);
reject(new Error("Aborted"));
return;
}
// Listen for abort signal
if (signal) {
signal.addEventListener(
"abort",
() => {
clearTimeout(timeout);
reject(new Error("Aborted"));
},
{ once: true }
);
}
});
}
}

View File

@@ -11,6 +11,7 @@ import {
getFeatureImagesDir,
ensureAutomakerDir,
} from "../lib/automaker-paths.js";
import type { TokenUsage } from "../providers/types.js";
export interface Feature {
id: string;
@@ -24,6 +25,8 @@ export interface Feature {
spec?: string;
model?: string;
imagePaths?: Array<string | { path: string; [key: string]: unknown }>;
// Branch info - worktree path is derived at runtime from branchName
branchName?: string; // Name of the feature branch (undefined = use current worktree)
skipTests?: boolean;
thinkingLevel?: string;
planningMode?: 'skip' | 'lite' | 'spec' | 'full';
@@ -41,6 +44,7 @@ export interface Feature {
error?: string;
summary?: string;
startedAt?: string;
tokenUsage?: TokenUsage;
[key: string]: unknown; // Keep catch-all for extensibility
}

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { createCreateHandler } from "@/routes/worktree/routes/create.js";
import { AUTOMAKER_INITIAL_COMMIT_MESSAGE } from "@/routes/worktree/common.js";
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs/promises";
import * as os from "os";
import * as path from "path";
const execAsync = promisify(exec);
describe("worktree create route - repositories without commits", () => {
let repoPath: string | null = null;
async function initRepoWithoutCommit() {
repoPath = await fs.mkdtemp(
path.join(os.tmpdir(), "automaker-no-commit-")
);
await execAsync("git init", { cwd: repoPath });
await execAsync('git config user.email "test@example.com"', {
cwd: repoPath,
});
await execAsync('git config user.name "Test User"', { cwd: repoPath });
// Intentionally skip creating an initial commit
}
afterEach(async () => {
if (!repoPath) {
return;
}
await fs.rm(repoPath, { recursive: true, force: true });
repoPath = null;
});
it("creates an initial commit before adding a worktree when HEAD is missing", async () => {
await initRepoWithoutCommit();
const handler = createCreateHandler();
const json = vi.fn();
const status = vi.fn().mockReturnThis();
const req = {
body: { projectPath: repoPath, branchName: "feature/no-head" },
} as any;
const res = {
json,
status,
} as any;
await handler(req, res);
expect(status).not.toHaveBeenCalled();
expect(json).toHaveBeenCalled();
const payload = json.mock.calls[0][0];
expect(payload.success).toBe(true);
const { stdout: commitCount } = await execAsync(
"git rev-list --count HEAD",
{ cwd: repoPath! }
);
expect(Number(commitCount.trim())).toBeGreaterThan(0);
const { stdout: latestMessage } = await execAsync(
"git log -1 --pretty=%B",
{ cwd: repoPath! }
);
expect(latestMessage.trim()).toBe(AUTOMAKER_INITIAL_COMMIT_MESSAGE);
});
});

View File

@@ -13,6 +13,10 @@ import {
} from "../helpers/git-test-repo.js";
import * as fs from "fs/promises";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
vi.mock("@/providers/provider-factory.js");
@@ -43,13 +47,24 @@ describe("auto-mode-service.ts (integration)", () => {
});
describe("worktree operations", () => {
it("should create git worktree for feature", async () => {
// Create a test feature
it("should use existing git worktree for feature", async () => {
const branchName = "feature/test-feature-1";
// Create a test feature with branchName set
await createTestFeature(testRepo.path, "test-feature-1", {
id: "test-feature-1",
category: "test",
description: "Test feature",
status: "pending",
branchName: branchName,
});
// Create worktree before executing (worktrees are now created when features are added/edited)
const worktreesDir = path.join(testRepo.path, ".worktrees");
const worktreePath = path.join(worktreesDir, "test-feature-1");
await fs.mkdir(worktreesDir, { recursive: true });
await execAsync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
cwd: testRepo.path,
});
// Mock provider to complete quickly
@@ -82,9 +97,20 @@ describe("auto-mode-service.ts (integration)", () => {
false // isAutoMode
);
// Verify branch was created
// Verify branch exists (was created when worktree was created)
const branches = await listBranches(testRepo.path);
expect(branches).toContain("feature/test-feature-1");
expect(branches).toContain(branchName);
// Verify worktree exists and is being used
// The service should have found and used the worktree (check via logs)
// We can verify the worktree exists by checking git worktree list
const worktrees = await listWorktrees(testRepo.path);
expect(worktrees.length).toBeGreaterThan(0);
// Verify that at least one worktree path contains our feature ID
const worktreePathsMatch = worktrees.some(wt =>
wt.includes("test-feature-1") || wt.includes(".worktrees")
);
expect(worktreePathsMatch).toBe(true);
// Note: Worktrees are not automatically cleaned up by the service
// This is expected behavior - manual cleanup is required

View File

@@ -1,57 +1,189 @@
import { describe, it, expect } from "vitest";
import {
APP_SPEC_XML_FORMAT,
specToXml,
getStructuredSpecPromptInstruction,
getAppSpecFormatInstruction,
APP_SPEC_XML_FORMAT,
type SpecOutput,
} from "@/lib/app-spec-format.js";
describe("app-spec-format.ts", () => {
describe("APP_SPEC_XML_FORMAT", () => {
it("should export a non-empty string constant", () => {
expect(typeof APP_SPEC_XML_FORMAT).toBe("string");
expect(APP_SPEC_XML_FORMAT.length).toBeGreaterThan(0);
describe("specToXml", () => {
it("should convert minimal spec to XML", () => {
const spec: SpecOutput = {
project_name: "Test Project",
overview: "A test project",
technology_stack: ["TypeScript", "Node.js"],
core_capabilities: ["Testing", "Development"],
implemented_features: [
{ name: "Feature 1", description: "First feature" },
],
};
const xml = specToXml(spec);
expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(xml).toContain("<project_specification>");
expect(xml).toContain("</project_specification>");
expect(xml).toContain("<project_name>Test Project</project_name>");
expect(xml).toContain("<technology>TypeScript</technology>");
expect(xml).toContain("<capability>Testing</capability>");
});
it("should contain XML format documentation", () => {
expect(APP_SPEC_XML_FORMAT).toContain("<project_specification>");
expect(APP_SPEC_XML_FORMAT).toContain("</project_specification>");
expect(APP_SPEC_XML_FORMAT).toContain("<project_name>");
expect(APP_SPEC_XML_FORMAT).toContain("<overview>");
expect(APP_SPEC_XML_FORMAT).toContain("<technology_stack>");
expect(APP_SPEC_XML_FORMAT).toContain("<core_capabilities>");
it("should escape XML special characters", () => {
const spec: SpecOutput = {
project_name: "Test & Project",
overview: "Description with <tags>",
technology_stack: ["TypeScript"],
core_capabilities: ["Cap"],
implemented_features: [],
};
const xml = specToXml(spec);
expect(xml).toContain("Test &amp; Project");
expect(xml).toContain("&lt;tags&gt;");
});
it("should contain XML escaping instructions", () => {
expect(APP_SPEC_XML_FORMAT).toContain("&lt;");
expect(APP_SPEC_XML_FORMAT).toContain("&gt;");
expect(APP_SPEC_XML_FORMAT).toContain("&amp;");
it("should include file_locations when provided", () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [
{
name: "Feature",
description: "Desc",
file_locations: ["src/index.ts"],
},
],
};
const xml = specToXml(spec);
expect(xml).toContain("<file_locations>");
expect(xml).toContain("<location>src/index.ts</location>");
});
it("should not include file_locations when empty", () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [
{ name: "Feature", description: "Desc", file_locations: [] },
],
};
const xml = specToXml(spec);
expect(xml).not.toContain("<file_locations>");
});
it("should include additional_requirements when provided", () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [],
additional_requirements: ["Node.js 18+"],
};
const xml = specToXml(spec);
expect(xml).toContain("<additional_requirements>");
expect(xml).toContain("<requirement>Node.js 18+</requirement>");
});
it("should include development_guidelines when provided", () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [],
development_guidelines: ["Use ESLint"],
};
const xml = specToXml(spec);
expect(xml).toContain("<development_guidelines>");
expect(xml).toContain("<guideline>Use ESLint</guideline>");
});
it("should include implementation_roadmap when provided", () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [],
implementation_roadmap: [
{ phase: "Phase 1", status: "completed", description: "Setup" },
],
};
const xml = specToXml(spec);
expect(xml).toContain("<implementation_roadmap>");
expect(xml).toContain("<status>completed</status>");
});
it("should not include optional sections when empty", () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [],
additional_requirements: [],
development_guidelines: [],
implementation_roadmap: [],
};
const xml = specToXml(spec);
expect(xml).not.toContain("<additional_requirements>");
expect(xml).not.toContain("<development_guidelines>");
expect(xml).not.toContain("<implementation_roadmap>");
});
});
describe("getStructuredSpecPromptInstruction", () => {
it("should return non-empty prompt instruction", () => {
const instruction = getStructuredSpecPromptInstruction();
expect(instruction).toBeTruthy();
expect(instruction.length).toBeGreaterThan(100);
});
it("should mention required fields", () => {
const instruction = getStructuredSpecPromptInstruction();
expect(instruction).toContain("project_name");
expect(instruction).toContain("overview");
expect(instruction).toContain("technology_stack");
});
});
describe("getAppSpecFormatInstruction", () => {
it("should return a string containing the XML format", () => {
it("should return non-empty format instruction", () => {
const instruction = getAppSpecFormatInstruction();
expect(typeof instruction).toBe("string");
expect(instruction).toContain(APP_SPEC_XML_FORMAT);
expect(instruction).toBeTruthy();
expect(instruction.length).toBeGreaterThan(100);
});
it("should contain critical formatting requirements", () => {
it("should include critical formatting requirements", () => {
const instruction = getAppSpecFormatInstruction();
expect(instruction).toContain("CRITICAL FORMATTING REQUIREMENTS");
expect(instruction).toContain("<project_specification>");
expect(instruction).toContain("</project_specification>");
});
});
it("should contain verification instructions", () => {
const instruction = getAppSpecFormatInstruction();
expect(instruction).toContain("VERIFICATION");
expect(instruction).toContain("exactly one root XML element");
});
it("should instruct not to use markdown", () => {
const instruction = getAppSpecFormatInstruction();
expect(instruction).toContain("Do NOT use markdown");
expect(instruction).toContain("no # headers");
expect(instruction).toContain("no **bold**");
describe("APP_SPEC_XML_FORMAT", () => {
it("should contain valid XML template structure", () => {
expect(APP_SPEC_XML_FORMAT).toContain("<project_specification>");
expect(APP_SPEC_XML_FORMAT).toContain("</project_specification>");
});
});
});

View File

@@ -16,16 +16,19 @@ import {
} from "@/lib/automaker-paths.js";
describe("automaker-paths.ts", () => {
const projectPath = "/test/project";
const projectPath = path.join("/test", "project");
describe("getAutomakerDir", () => {
it("should return path to .automaker directory", () => {
expect(getAutomakerDir(projectPath)).toBe("/test/project/.automaker");
expect(getAutomakerDir(projectPath)).toBe(
path.join(projectPath, ".automaker")
);
});
it("should handle paths with trailing slashes", () => {
expect(getAutomakerDir("/test/project/")).toBe(
path.join("/test/project/", ".automaker")
const pathWithSlash = path.join("/test", "project") + path.sep;
expect(getAutomakerDir(pathWithSlash)).toBe(
path.join(pathWithSlash, ".automaker")
);
});
});
@@ -33,7 +36,7 @@ describe("automaker-paths.ts", () => {
describe("getFeaturesDir", () => {
it("should return path to features directory", () => {
expect(getFeaturesDir(projectPath)).toBe(
"/test/project/.automaker/features"
path.join(projectPath, ".automaker", "features")
);
});
});
@@ -41,13 +44,13 @@ describe("automaker-paths.ts", () => {
describe("getFeatureDir", () => {
it("should return path to specific feature directory", () => {
expect(getFeatureDir(projectPath, "feature-123")).toBe(
"/test/project/.automaker/features/feature-123"
path.join(projectPath, ".automaker", "features", "feature-123")
);
});
it("should handle feature IDs with special characters", () => {
expect(getFeatureDir(projectPath, "my-feature_v2")).toBe(
"/test/project/.automaker/features/my-feature_v2"
path.join(projectPath, ".automaker", "features", "my-feature_v2")
);
});
});
@@ -55,27 +58,31 @@ describe("automaker-paths.ts", () => {
describe("getFeatureImagesDir", () => {
it("should return path to feature images directory", () => {
expect(getFeatureImagesDir(projectPath, "feature-123")).toBe(
"/test/project/.automaker/features/feature-123/images"
path.join(projectPath, ".automaker", "features", "feature-123", "images")
);
});
});
describe("getBoardDir", () => {
it("should return path to board directory", () => {
expect(getBoardDir(projectPath)).toBe("/test/project/.automaker/board");
expect(getBoardDir(projectPath)).toBe(
path.join(projectPath, ".automaker", "board")
);
});
});
describe("getImagesDir", () => {
it("should return path to images directory", () => {
expect(getImagesDir(projectPath)).toBe("/test/project/.automaker/images");
expect(getImagesDir(projectPath)).toBe(
path.join(projectPath, ".automaker", "images")
);
});
});
describe("getWorktreesDir", () => {
it("should return path to worktrees directory", () => {
expect(getWorktreesDir(projectPath)).toBe(
"/test/project/.automaker/worktrees"
path.join(projectPath, ".automaker", "worktrees")
);
});
});
@@ -83,7 +90,7 @@ describe("automaker-paths.ts", () => {
describe("getAppSpecPath", () => {
it("should return path to app_spec.txt file", () => {
expect(getAppSpecPath(projectPath)).toBe(
"/test/project/.automaker/app_spec.txt"
path.join(projectPath, ".automaker", "app_spec.txt")
);
});
});
@@ -91,7 +98,7 @@ describe("automaker-paths.ts", () => {
describe("getBranchTrackingPath", () => {
it("should return path to active-branches.json file", () => {
expect(getBranchTrackingPath(projectPath)).toBe(
"/test/project/.automaker/active-branches.json"
path.join(projectPath, ".automaker", "active-branches.json")
);
});
});

View File

@@ -40,9 +40,9 @@ describe("sdk-options.ts", () => {
describe("MAX_TURNS", () => {
it("should export turn presets", async () => {
const { MAX_TURNS } = await import("@/lib/sdk-options.js");
expect(MAX_TURNS.quick).toBe(5);
expect(MAX_TURNS.standard).toBe(20);
expect(MAX_TURNS.extended).toBe(50);
expect(MAX_TURNS.quick).toBe(50);
expect(MAX_TURNS.standard).toBe(100);
expect(MAX_TURNS.extended).toBe(250);
expect(MAX_TURNS.maximum).toBe(1000);
});
});
@@ -88,7 +88,7 @@ describe("sdk-options.ts", () => {
expect(options.cwd).toBe("/test/path");
expect(options.maxTurns).toBe(MAX_TURNS.maximum);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.specGeneration]);
expect(options.permissionMode).toBe("acceptEdits");
expect(options.permissionMode).toBe("default");
});
it("should include system prompt when provided", async () => {
@@ -141,7 +141,7 @@ describe("sdk-options.ts", () => {
const options = createSuggestionsOptions({ cwd: "/test/path" });
expect(options.cwd).toBe("/test/path");
expect(options.maxTurns).toBe(MAX_TURNS.quick);
expect(options.maxTurns).toBe(MAX_TURNS.extended);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.readOnly]);
});
});

View File

@@ -324,9 +324,9 @@ describe("auto-mode-service.ts - Planning Mode", () => {
describe("status management", () => {
it("should report correct status", () => {
const status = service.getStatus();
expect(status.autoLoopRunning).toBe(false);
expect(status.runningFeatures).toEqual([]);
expect(status.isRunning).toBe(false);
expect(status.runningCount).toBe(0);
});
});
});

View File

@@ -13,12 +13,9 @@
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# Vite
/dist/
/dist-electron/
# misc
.DS_Store
@@ -33,12 +30,8 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Playwright
/test-results/
@@ -47,5 +40,8 @@ next-env.d.ts
/playwright/.cache/
# Electron
/dist/
/release/
/server-bundle/
# TanStack Router generated
src/routeTree.gen.ts

36
apps/ui/eslint.config.mjs Normal file
View File

@@ -0,0 +1,36 @@
import { defineConfig, globalIgnores } from "eslint/config";
import js from "@eslint/js";
import ts from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
const eslintConfig = defineConfig([
js.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
plugins: {
"@typescript-eslint": ts,
},
rules: {
...ts.configs.recommended.rules,
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
},
},
globalIgnores([
"dist/**",
"dist-electron/**",
"node_modules/**",
"server-bundle/**",
"release/**",
"src/routeTree.gen.ts",
]),
]);
export default eslintConfig;

32
apps/ui/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en" suppressHydrationWarning>
<head>
<meta charset="UTF-8" />
<title>Automaker - Autonomous AI Development Studio</title>
<meta name="description" content="Build software autonomously with AI agents" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script>
// Prevent theme flash - apply stored theme before React hydrates
(function() {
try {
const stored = localStorage.getItem('automaker-storage');
if (stored) {
const data = JSON.parse(stored);
const theme = data.state?.theme;
if (theme && theme !== 'system' && theme !== 'light') {
// Apply the actual theme class (dark, retro, dracula, nord, etc.)
document.documentElement.classList.add(theme);
} else if (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
}
} catch (e) {}
})();
</script>
</head>
<body class="antialiased">
<div id="app"></div>
<script type="module" src="/src/renderer.tsx"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{
"name": "@automaker/app",
"name": "@automaker/ui",
"version": "0.1.0",
"description": "An autonomous AI development studio that helps you build software faster using AI-powered agents",
"homepage": "https://github.com/AutoMaker-Org/automaker",
@@ -13,25 +13,29 @@
},
"private": true,
"license": "Unlicense",
"main": "electron/main.js",
"main": "dist-electron/main.js",
"scripts": {
"dev": "next dev -p 3007",
"dev:web": "next dev -p 3007",
"dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"",
"dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"",
"build": "next build",
"build:electron": "node scripts/prepare-server.js && next build && electron-builder",
"build:electron:win": "node scripts/prepare-server.js && next build && electron-builder --win",
"build:electron:mac": "node scripts/prepare-server.js && next build && electron-builder --mac",
"build:electron:linux": "node scripts/prepare-server.js && next build && electron-builder --linux",
"dev": "vite",
"dev:web": "cross-env VITE_SKIP_ELECTRON=true vite",
"dev:electron": "vite",
"dev:electron:debug": "cross-env OPEN_DEVTOOLS=true vite",
"build": "vite build",
"build:electron": "node scripts/prepare-server.mjs && vite build && electron-builder",
"build:electron:dir": "node scripts/prepare-server.mjs && vite build && electron-builder --dir",
"build:electron:win": "node scripts/prepare-server.mjs && vite build && electron-builder --win",
"build:electron:win:dir": "node scripts/prepare-server.mjs && vite build && electron-builder --win --dir",
"build:electron:mac": "node scripts/prepare-server.mjs && vite build && electron-builder --mac",
"build:electron:mac:dir": "node scripts/prepare-server.mjs && vite build && electron-builder --mac --dir",
"build:electron:linux": "node scripts/prepare-server.mjs && vite build && electron-builder --linux",
"build:electron:linux:dir": "node scripts/prepare-server.mjs && vite build && electron-builder --linux --dir",
"postinstall": "electron-builder install-app-deps",
"start": "next start",
"preview": "vite preview",
"lint": "eslint",
"pretest": "node scripts/setup-e2e-fixtures.js",
"pretest": "node scripts/setup-e2e-fixtures.mjs",
"test": "playwright test",
"test:headed": "playwright test --headed",
"dev:electron:wsl": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron . --no-sandbox --disable-gpu\"",
"dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\""
"dev:electron:wsl": "cross-env vite",
"dev:electron:wsl:gpu": "cross-env MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA vite"
},
"dependencies": {
"@codemirror/lang-xml": "^6.1.0",
@@ -45,12 +49,15 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-router": "^1.141.6",
"@uiw/react-codemirror": "^4.25.4",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-webgl": "^0.18.0",
@@ -60,10 +67,9 @@
"cmdk": "^1.1.1",
"dotenv": "^17.2.3",
"geist": "^1.5.1",
"lucide-react": "^0.556.0",
"next": "^16.0.10",
"react": "19.2.0",
"react-dom": "19.2.0",
"lucide-react": "^0.562.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"sonner": "^2.0.7",
@@ -83,32 +89,39 @@
},
"devDependencies": {
"@electron/rebuild": "^4.0.2",
"@eslint/js": "^9.0.0",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.2.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-plugin": "^1.141.7",
"@types/node": "^22",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"@vitejs/plugin-react": "^5.1.2",
"cross-env": "^10.1.0",
"electron": "39.2.7",
"electron-builder": "^26.0.12",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"tailwindcss": "^4",
"eslint": "^9.39.2",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "5.9.3",
"wait-on": "^9.0.3"
"vite": "^7.3.0",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6"
},
"build": {
"appId": "com.automaker.app",
"productName": "Automaker",
"artifactName": "${productName}-${version}-${arch}.${ext}",
"afterPack": "./scripts/rebuild-server-natives.js",
"npmRebuild": false,
"afterPack": "./scripts/rebuild-server-natives.cjs",
"directories": {
"output": "dist"
"output": "release"
},
"files": [
"electron/**/*",
"out/**/*",
"dist/**/*",
"dist-electron/**/*",
"public/**/*",
"!node_modules/**/*"
],

View File

@@ -3,14 +3,15 @@ import { defineConfig, devices } from "@playwright/test";
const port = process.env.TEST_PORT || 3007;
const serverPort = process.env.TEST_SERVER_PORT || 3008;
const reuseServer = process.env.TEST_REUSE_SERVER === "true";
const mockAgent = process.env.CI === "true" || process.env.AUTOMAKER_MOCK_AGENT === "true";
const mockAgent =
process.env.CI === "true" || process.env.AUTOMAKER_MOCK_AGENT === "true";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
workers: undefined,
reporter: "html",
timeout: 30000,
use: {
@@ -43,15 +44,17 @@ export default defineConfig({
ALLOWED_PROJECT_DIRS: "/Users,/home,/tmp,/var/folders",
},
},
// Frontend Next.js server
// Frontend Vite dev server
{
command: `npx next dev -p ${port}`,
command: `npm run dev`,
url: `http://localhost:${port}`,
reuseExistingServer: true,
timeout: 120000,
env: {
...process.env,
NEXT_PUBLIC_SKIP_SETUP: "true",
VITE_SKIP_SETUP: "true",
// Skip electron plugin in CI - no display available for Electron
VITE_SKIP_ELECTRON: process.env.CI === "true" ? "true" : undefined,
},
},
],

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -12,7 +12,7 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Resolve workspace root (apps/app/scripts -> workspace root)
// Resolve workspace root (apps/ui/scripts -> workspace root)
const WORKSPACE_ROOT = path.resolve(__dirname, "../../..");
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");

7
apps/ui/src/App.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./utils/router";
import "./styles/global.css";
export default function App() {
return <RouterProvider router={router} />;
}

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useRef, useCallback, useEffect } from "react";
import { ImageIcon, Upload, Loader2, Trash2 } from "lucide-react";
@@ -72,7 +71,7 @@ export function BoardBackgroundModal({
useEffect(() => {
if (currentProject && backgroundSettings.imagePath) {
const serverUrl =
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
import.meta.env.VITE_SERVER_URL || "http://localhost:3008";
// Add cache-busting query parameter to force browser to reload image
const cacheBuster = imageVersion
? `&v=${imageVersion}`

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import {
@@ -129,7 +128,7 @@ export function FileBrowserDialog({
try {
// Get server URL from environment or default
const serverUrl =
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
import.meta.env.VITE_SERVER_URL || "http://localhost:3008";
const response = await fetch(`${serverUrl}/api/fs/browse`, {
method: "POST",

View File

@@ -1,9 +1,7 @@
"use client";
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useNavigate, useLocation } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { useAppStore, formatShortcut, type ThemeMode } from "@/store/app-store";
import { CoursePromoBadge } from "@/components/ui/course-promo-badge";
import {
FolderOpen,
Plus,
@@ -82,10 +80,8 @@ import { themeOptions } from "@/config/theme-options";
import type { SpecRegenerationEvent } from "@/types/electron";
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
import { NewProjectModal } from "@/components/new-project-modal";
import {
ProjectSetupDialog,
type FeatureCount,
} from "@/components/layout/project-setup-dialog";
import { CreateSpecDialog } from "@/components/views/spec-view/dialogs";
import type { FeatureCount } from "@/components/views/spec-view/types";
import {
DndContext,
DragEndEvent,
@@ -223,16 +219,17 @@ const BugReportButton = ({
};
export function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
const {
projects,
trashedProjects,
currentProject,
currentView,
sidebarOpen,
projectHistory,
upsertAndSetCurrentProject,
setCurrentProject,
setCurrentView,
toggleSidebar,
restoreTrashedProject,
deleteTrashedProject,
@@ -251,14 +248,13 @@ export function Sidebar() {
} = useAppStore();
// Environment variable flags for hiding sidebar items
// Note: Next.js requires static access to process.env variables (no dynamic keys)
const hideTerminal = process.env.NEXT_PUBLIC_HIDE_TERMINAL === "true";
const hideWiki = process.env.NEXT_PUBLIC_HIDE_WIKI === "true";
const hideTerminal = import.meta.env.VITE_HIDE_TERMINAL === "true";
const hideWiki = import.meta.env.VITE_HIDE_WIKI === "true";
const hideRunningAgents =
process.env.NEXT_PUBLIC_HIDE_RUNNING_AGENTS === "true";
const hideContext = process.env.NEXT_PUBLIC_HIDE_CONTEXT === "true";
const hideSpecEditor = process.env.NEXT_PUBLIC_HIDE_SPEC_EDITOR === "true";
const hideAiProfiles = process.env.NEXT_PUBLIC_HIDE_AI_PROFILES === "true";
import.meta.env.VITE_HIDE_RUNNING_AGENTS === "true";
const hideContext = import.meta.env.VITE_HIDE_CONTEXT === "true";
const hideSpecEditor = import.meta.env.VITE_HIDE_SPEC_EDITOR === "true";
const hideAiProfiles = import.meta.env.VITE_HIDE_AI_PROFILES === "true";
// Get customizable keyboard shortcuts
const shortcuts = useKeyboardShortcutsConfig();
@@ -291,6 +287,7 @@ export function Sidebar() {
const [setupProjectPath, setSetupProjectPath] = useState("");
const [projectOverview, setProjectOverview] = useState("");
const [generateFeatures, setGenerateFeatures] = useState(true);
const [analyzeProject, setAnalyzeProject] = useState(true);
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
@@ -429,7 +426,6 @@ export function Sidebar() {
unsubscribe();
};
}, [
setCurrentView,
creatingSpecProjectPath,
setupProjectPath,
setSpecCreatingForProject,
@@ -498,7 +494,7 @@ export function Sidebar() {
setupProjectPath,
projectOverview.trim(),
generateFeatures,
undefined, // analyzeProject - use default
analyzeProject,
generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features
);
@@ -527,6 +523,7 @@ export function Sidebar() {
setupProjectPath,
projectOverview,
generateFeatures,
analyzeProject,
featureCount,
setSpecCreatingForProject,
]);
@@ -1177,7 +1174,7 @@ export function Sidebar() {
if (item.shortcut) {
shortcutsList.push({
key: item.shortcut,
action: () => setCurrentView(item.id as any),
action: () => navigate({ to: `/${item.id}` as const }),
description: `Navigate to ${item.label}`,
});
}
@@ -1187,7 +1184,7 @@ export function Sidebar() {
// Add settings shortcut
shortcutsList.push({
key: shortcuts.settings,
action: () => setCurrentView("settings"),
action: () => navigate({ to: "/settings" }),
description: "Navigate to Settings",
});
}
@@ -1196,7 +1193,7 @@ export function Sidebar() {
}, [
shortcuts,
currentProject,
setCurrentView,
navigate,
toggleSidebar,
projects.length,
handleOpenFolder,
@@ -1210,15 +1207,15 @@ export function Sidebar() {
useKeyboardShortcuts(navigationShortcuts);
const isActiveRoute = (id: string) => {
return currentView === id;
// Map view IDs to route paths
const routePath = id === "welcome" ? "/" : `/${id}`;
return location.pathname === routePath;
};
return (
<aside
className={cn(
"flex-shrink-0 flex flex-col z-30 relative",
// Clean theme sidebar-glass class
"sidebar-glass",
// Glass morphism background with gradient
"bg-gradient-to-b from-sidebar/95 via-sidebar/85 to-sidebar/90 backdrop-blur-2xl",
// Premium border with subtle glow
@@ -1291,7 +1288,7 @@ export function Sidebar() {
"flex items-center gap-3 titlebar-no-drag cursor-pointer group",
!sidebarOpen && "flex-col gap-1"
)}
onClick={() => setCurrentView("welcome")}
onClick={() => navigate({ to: "/" })}
data-testid="logo-button"
>
{!sidebarOpen ? (
@@ -1849,15 +1846,13 @@ export function Sidebar() {
return (
<button
key={item.id}
onClick={() => setCurrentView(item.id as any)}
onClick={() => navigate({ to: `/${item.id}` as const })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
isActive
? [
// Active: Premium gradient with glow
// Clean theme nav-active class
"nav-active",
"bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10",
"text-foreground font-medium",
"border border-brand-500/30",
@@ -1876,9 +1871,6 @@ export function Sidebar() {
title={!sidebarOpen ? item.label : undefined}
data-testid={`nav-${item.id}`}
>
{isActive && (
<div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)}
<Icon
className={cn(
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
@@ -1898,8 +1890,6 @@ export function Sidebar() {
{item.shortcut && sidebarOpen && (
<span
className={cn(
// Clean theme shortcut-badge class
"shortcut-badge",
"hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md transition-all duration-200",
isActive
? "bg-brand-500/20 text-brand-400"
@@ -1925,7 +1915,7 @@ export function Sidebar() {
>
{item.label}
{item.shortcut && (
<span className="shortcut-badge ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
{formatShortcut(item.shortcut, true)}
</span>
)}
@@ -1951,13 +1941,11 @@ export function Sidebar() {
"bg-gradient-to-t from-background/10 via-sidebar/50 to-transparent"
)}
>
{/* Course Promo Badge */}
<CoursePromoBadge sidebarOpen={sidebarOpen} />
{/* Wiki Link */}
{!hideWiki && (
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("wiki")}
onClick={() => navigate({ to: "/wiki" })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
@@ -1980,9 +1968,6 @@ export function Sidebar() {
title={!sidebarOpen ? "Wiki" : undefined}
data-testid="wiki-link"
>
{isActiveRoute("wiki") && (
<div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)}
<BookOpen
className={cn(
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
@@ -2020,7 +2005,7 @@ export function Sidebar() {
{!hideRunningAgents && (
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("running-agents")}
onClick={() => navigate({ to: "/running-agents" })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
@@ -2043,9 +2028,6 @@ export function Sidebar() {
title={!sidebarOpen ? "Running Agents" : undefined}
data-testid="running-agents-link"
>
{isActiveRoute("running-agents") && (
<div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)}
<div className="relative">
<Activity
className={cn(
@@ -2059,8 +2041,6 @@ export function Sidebar() {
{!sidebarOpen && runningAgentsCount > 0 && (
<span
className={cn(
// Clean theme running-agents-badge class
"running-agents-badge",
"absolute -top-1.5 -right-1.5 flex items-center justify-center",
"min-w-4 h-4 px-1 text-[9px] font-bold rounded-full",
"bg-brand-500 text-white shadow-sm",
@@ -2084,8 +2064,6 @@ export function Sidebar() {
{sidebarOpen && runningAgentsCount > 0 && (
<span
className={cn(
// Clean theme running-agents-badge class
"running-agents-badge",
"hidden lg:flex items-center justify-center",
"min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full",
"bg-brand-500 text-white shadow-sm",
@@ -2122,7 +2100,7 @@ export function Sidebar() {
{/* Settings Link */}
<div className="p-2">
<button
onClick={() => setCurrentView("settings")}
onClick={() => navigate({ to: "/settings" })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
@@ -2145,9 +2123,6 @@ export function Sidebar() {
title={!sidebarOpen ? "Settings" : undefined}
data-testid="settings-button"
>
{isActiveRoute("settings") && (
<div className="absolute inset-y-0 left-0 w-1 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full shadow-sm shadow-brand-500/50"></div>
)}
<Settings
className={cn(
"w-[18px] h-[18px] shrink-0 transition-all duration-200",
@@ -2286,18 +2261,23 @@ export function Sidebar() {
</Dialog>
{/* New Project Setup Dialog */}
<ProjectSetupDialog
<CreateSpecDialog
open={showSetupDialog}
onOpenChange={setShowSetupDialog}
projectOverview={projectOverview}
onProjectOverviewChange={setProjectOverview}
generateFeatures={generateFeatures}
onGenerateFeaturesChange={setGenerateFeatures}
analyzeProject={analyzeProject}
onAnalyzeProjectChange={setAnalyzeProject}
featureCount={featureCount}
onFeatureCountChange={setFeatureCount}
onCreateSpec={handleCreateInitialSpec}
onSkip={handleSkipSetup}
isCreatingSpec={isCreatingSpec}
showSkipButton={true}
title="Set Up Your Project"
description="We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification."
/>
{/* New Project Onboarding Dialog */}

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import {

View File

@@ -1,12 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
@@ -116,8 +110,10 @@ export function SessionManager({
new Set()
);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] =
useState<SessionListItem | null>(null);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
useState(false);
// Check running state for all sessions
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
@@ -234,11 +230,7 @@ export function SessionManager({
const api = getElectronAPI();
if (!editingName.trim() || !api?.sessions) return;
const result = await api.sessions.update(
sessionId,
editingName,
undefined
);
const result = await api.sessions.update(sessionId, editingName, undefined);
if (result.success) {
setEditingSessionId(null);

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { ChevronDown } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { Check, ChevronsUpDown, LucideIcon } from "lucide-react";
@@ -35,6 +34,7 @@ interface AutocompleteProps {
emptyMessage?: string;
className?: string;
disabled?: boolean;
error?: boolean;
icon?: LucideIcon;
allowCreate?: boolean;
createLabel?: (value: string) => string;
@@ -58,6 +58,7 @@ export function Autocomplete({
emptyMessage = "No results found.",
className,
disabled = false,
error = false,
icon: Icon,
allowCreate = false,
createLabel = (v) => `Create "${v}"`,
@@ -130,6 +131,7 @@ export function Autocomplete({
className={cn(
"w-full justify-between",
Icon && "font-mono text-sm",
error && "border-destructive focus-visible:ring-destructive",
className
)}
data-testid={testId}

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { GitBranch } from "lucide-react";
@@ -8,9 +7,11 @@ interface BranchAutocompleteProps {
value: string;
onChange: (value: string) => void;
branches: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
placeholder?: string;
className?: string;
disabled?: boolean;
error?: boolean;
"data-testid"?: string;
}
@@ -18,20 +19,32 @@ export function BranchAutocomplete({
value,
onChange,
branches,
branchCardCounts,
placeholder = "Select a branch...",
className,
disabled = false,
error = false,
"data-testid": testId,
}: BranchAutocompleteProps) {
// Always include "main" at the top of suggestions
const branchOptions: AutocompleteOption[] = React.useMemo(() => {
const branchSet = new Set(["main", ...branches]);
return Array.from(branchSet).map((branch) => ({
value: branch,
label: branch,
badge: branch === "main" ? "default" : undefined,
}));
}, [branches]);
return Array.from(branchSet).map((branch) => {
const cardCount = branchCardCounts?.[branch];
// Show card count if available, otherwise show "default" for main branch only
const badge = branchCardCounts !== undefined
? String(cardCount ?? 0)
: branch === "main"
? "default"
: undefined;
return {
value: branch,
label: branch,
badge,
};
});
}, [branches, branchCardCounts]);
return (
<Autocomplete
@@ -43,6 +56,7 @@ export function BranchAutocomplete({
emptyMessage="No branches found."
className={className}
disabled={disabled}
error={error}
icon={GitBranch}
allowCreate
createLabel={(v) => `Create "${v}"`}

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { Autocomplete } from "@/components/ui/autocomplete";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import { Clock } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";
@@ -85,7 +84,7 @@ export function DescriptionImageDropZone({
// Construct server URL for loading saved images
const getImageServerUrl = useCallback((imagePath: string): string => {
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
const serverUrl = import.meta.env.VITE_SERVER_URL || "http://localhost:3008";
const projectPath = currentProject?.path || "";
return `${serverUrl}/api/fs/image?path=${encodeURIComponent(imagePath)}&projectPath=${encodeURIComponent(projectPath)}`;
}, [currentProject?.path]);

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
@@ -87,16 +86,18 @@ function DialogOverlay({
);
}
function DialogContent({
className,
children,
showCloseButton = true,
compact = false,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
export type DialogContentProps = Omit<
React.ComponentProps<typeof DialogPrimitive.Content>,
"ref"
> & {
showCloseButton?: boolean;
compact?: boolean;
}) {
};
const DialogContent = React.forwardRef<
HTMLDivElement,
DialogContentProps
>(({ className, children, showCloseButton = true, compact = false, ...props }, ref) => {
// Check if className contains a custom max-width
const hasCustomMaxWidth =
typeof className === "string" && className.includes("max-w-");
@@ -105,6 +106,7 @@ function DialogContent({
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogContentPrimitive
ref={ref}
data-slot="dialog-content"
className={cn(
"fixed top-[50%] left-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
@@ -148,7 +150,9 @@ function DialogContent({
</DialogContentPrimitive>
</DialogPortal>
);
}
});
DialogContent.displayName = "DialogContent";
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"

View File

@@ -1,4 +1,3 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect, useMemo, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { useEffect, useCallback, useRef } from "react";

Some files were not shown because too many files have changed in this diff Show More