Compare commits

..

384 Commits

Author SHA1 Message Date
SuperComboGamer
d4907a610e IDK 2025-12-18 19:06:14 -05:00
Web Dev Cody
c7312af3c8 Merge pull request #160 from AutoMaker-Org/implement-planning/speckits-rebase
Implement planning/speckits rebase
2025-12-17 22:43:14 -05:00
SuperComboGamer
f3dbc996d4 FINAL 2025-12-17 22:34:19 -05:00
SuperComboGamer
760f254f78 fix comment gemini 2025-12-17 22:20:16 -05:00
SuperComboGamer
91bff6c572 fix tests 2025-12-17 22:04:39 -05:00
SuperComboGamer
ecf34b178e Merge pull request #136 from AutoMaker-Org/implement-planning/speckits
feat: integrate planning mode functionality across components
2025-12-17 21:52:21 -05:00
SuperComboGamer
3cd6e8c13b build fix 2025-12-17 21:50:48 -05:00
SuperComboGamer
ca8341bf39 Merge remote-tracking branch 'origin/main' into implement-planning/speckits 2025-12-17 21:40:42 -05:00
SuperComboGamer
160bd8bfc7 FINSIHED 2025-12-17 21:29:36 -05:00
SuperComboGamer
0d1138dfcf feat: add task progress tracking to auto mode
- Introduced TaskProgressPanel to display task execution status in the AgentOutputModal.
- Enhanced useAutoMode hook to emit events for task start, completion, and phase completion.
- Updated AutoModeEvent type to include new task-related events.
- Implemented task parsing from generated specifications to track progress accurately.
- Improved auto mode service to handle task progress updates and emit relevant events.
2025-12-17 20:11:30 -05:00
SuperComboGamer
b112747073 feat: implement plan approval functionality in board view
- Introduced PlanApprovalDialog for reviewing and approving feature plans.
- Added state management for pending plan approvals and loading states.
- Enhanced BoardView to handle plan approval actions, including approve and reject functionalities.
- Updated KanbanCard and KanbanBoard components to include buttons for viewing and approving plans.
- Integrated plan approval logic into the auto mode service, allowing for user feedback and plan edits.
- Updated app state to manage default plan approval settings and integrate with existing feature workflows.
2025-12-17 19:39:09 -05:00
Web Dev Cody
cffdec91f1 Merge pull request #140 from AutoMaker-Org/feature/feature-1765862386892-fyafrxpcq
redesign file browser a bit
2025-12-17 12:52:19 -05:00
Cody Seibert
d9c87f8116 fix: skip flaky file browser test in CI
- Marked the test for opening a project via file browser as skipped in CI due to its unreliability in headless environments.
- This change aims to maintain the stability of the test suite while addressing the underlying issue in future updates.
2025-12-17 12:43:16 -05:00
Web Dev Cody
acfb8d2255 Merge pull request #139 from AutoMaker-Org/themes-and-notif
added 3 new themes and modified ding notif to be less harsh on the ears
2025-12-17 12:24:28 -05:00
Web Dev Cody
fe6106e807 Merge pull request #137 from AutoMaker-Org/fix/agent-output
refactor: remove direct file saving from AgentOutputModal and impleme…
2025-12-17 12:24:14 -05:00
trueheads
2af43d7c2d fixing import of Themes to satisfy pr build 2025-12-17 09:53:57 -06:00
Cody Seibert
a7a6ff2e6c redesign file browser a bit 2025-12-17 10:52:39 -05:00
trueheads
8f598d7ce3 gemini fixes and unbreaking scroll logic 2025-12-17 09:49:12 -06:00
trueheads
7f8092264a added 3 new themes and modified ding notif to be less harsh on the ears 2025-12-17 09:37:03 -06:00
Kacper
e0471fef09 feat: enhance summary extraction to support <summary> tags
- Updated the extractSummary function to capture content between <summary> and </summary> tags for improved log parsing.
- Retained fallback logic to extract summaries from traditional ## Summary sections, ensuring backward compatibility.
2025-12-17 16:36:56 +01:00
Kacper
043edde63b refactor: implement gemini suggestions 2025-12-17 16:19:31 +01:00
Web Dev Cody
4b9a211c49 Merge pull request #138 from AutoMaker-Org/refactor-worktree
refactor worktree into smaller components
2025-12-17 09:36:31 -05:00
Kacper
b59bbd93ba feat: add TodoWrite support in log viewer for enhanced task management
- Introduced a new TodoListRenderer component to display parsed todo items with status indicators and colors.
- Implemented a parseTodoContent function to extract todo items from TodoWrite JSON content.
- Enhanced LogEntryItem to conditionally render todo items when a TodoWrite entry is detected, improving log entry clarity and usability.
- Updated UI to visually differentiate between todo item statuses, enhancing user experience in task tracking.
2025-12-17 14:54:32 +01:00
Kacper
2a782392bc feat: enhance log parsing to support <summary> tags for structured output
- Introduced support for <summary> tags in log entries, allowing for better organization and parsing of summary content.
- Updated the detectEntryType function to recognize <summary> tags as a preferred format for summaries.
- Implemented summary accumulation logic to handle content between <summary> and </summary> tags.
- Modified the prompt in auto-mode service to instruct users to wrap their summaries in <summary> tags for consistency in log output.
2025-12-17 14:33:13 +01:00
Kacper
fe56ba133e feat: enhance log viewer with tool category support and filtering
- Added tool category icons and colors for log entries based on their metadata, improving visual differentiation.
- Implemented search functionality and filters for log entry types and tool categories, allowing users to customize their view.
- Enhanced log entry parsing to include tool-specific summaries and file paths, providing more context in the logs.
- Introduced a clear filters button to reset search and category filters, improving user experience.
- Updated the log viewer UI to accommodate new features, including a sticky header for better accessibility.
2025-12-17 14:30:24 +01:00
Cody Seibert
40a3046a3b refactor worktree into smaller components 2025-12-17 08:20:00 -05:00
Web Dev Cody
1aa8b5b56b Merge pull request #135 from AutoMaker-Org/feature-dependency-improvements
Feature Dependency Rework & Options Setting
2025-12-17 07:53:09 -05:00
Kacper
266e0c54b9 refactor: remove direct file saving from AgentOutputModal and implement debounced file writing in auto-mode service
- Removed the saveOutput function from AgentOutputModal to streamline state management, ensuring local state updates without direct file writes.
- Introduced a debounced file writing mechanism in the auto-mode service to handle incremental updates to agent output, improving performance and reliability.
- Enhanced error handling during file writes to prevent execution interruptions and ensure all content is saved correctly.
2025-12-17 13:22:20 +01:00
Cody Seibert
31550ab4e7 Merge branch 'main' into feature-dependency-improvements 2025-12-17 00:23:59 -05:00
Web Dev Cody
cce8d1569a Merge pull request #125 from AutoMaker-Org/feature/worktrees
Feature - Worktrees
2025-12-16 23:40:45 -05:00
Cody Seibert
ad051eb8f0 fix: skip failing feature lifecycle test in CI
- Skipped a specific feature lifecycle test that fails in GitHub Actions to prevent CI disruptions.
- This change ensures that the test suite continues to run smoothly while addressing the underlying issue in a future update.
2025-12-16 23:25:40 -05:00
SuperComboGamer
01098545cf feat: integrate planning mode functionality across components
- Added a new PlanningMode feature to manage default planning strategies for features.
- Updated the FeatureDefaultsSection to include a dropdown for selecting the default planning mode.
- Enhanced AddFeatureDialog and EditFeatureDialog to support planning mode selection and state management.
- Introduced PlanningModeSelector component for better user interaction with planning modes.
- Updated app state management to include default planning mode and related specifications.
- Refactored various UI components to ensure compatibility with new planning mode features.
2025-12-16 23:13:06 -05:00
trueheads
d58bd782ef adjustments per gemini suggestions 2025-12-16 22:09:24 -06:00
Cody Seibert
c11cb6a6cd fix: skip failing feature lifecycle test and improve code formatting
- Skipped a feature lifecycle test that fails in GitHub Actions to prevent CI issues.
- Improved code formatting for better readability, including consistent line breaks and indentation in test cases.
- Ensured that all feature-related locators and assertions are clearly structured for maintainability.
2025-12-16 22:48:57 -05:00
trueheads
64549f824c H M L 2025-12-16 21:42:39 -06:00
Cody Seibert
83fab5321e feat: add file renaming functionality in ContextView
- Implemented a rename dialog for files, allowing users to rename selected context files.
- Added state management for the rename dialog and file name input.
- Enhanced file handling to check for existing names and update file paths accordingly.
- Updated UI to include a pencil icon for triggering the rename action on files.
- Improved user experience by ensuring the renamed file is selected after the operation.
2025-12-16 22:36:22 -05:00
trueheads
bb47f22d6c build error fixes, and test expansion 2025-12-16 21:30:53 -06:00
Cody Seibert
4996a63bcc feat: improve Playwright configuration and enhance error handling in CreatePRDialog
- Updated Playwright configuration to always reuse existing servers, improving test efficiency.
- Enhanced CreatePRDialog to handle null browser URLs gracefully, ensuring better user experience during PR creation failures.
- Added new unit tests for app specification format and automaker paths, improving test coverage and reliability.
- Introduced tests for file system utilities and logger functionality, ensuring robust error handling and logging behavior.
- Implemented comprehensive tests for SDK options and dev server service, enhancing overall test stability and maintainability.
2025-12-16 22:04:47 -05:00
trueheads
f302234b0e Feature Dependency Rework & Options Setting 2025-12-16 21:02:42 -06:00
Cody Seibert
58d6ae02a5 feat: enhance worktree management and UI integration
- Refactored BoardView and WorktreeSelector components for improved readability and maintainability, including consistent formatting and structure.
- Updated feature handling to ensure correct worktree assignment and reset logic when worktrees are deleted, enhancing user experience.
- Enhanced KanbanCard to display priority badges with improved styling and layout.
- Removed deprecated revert feature logic from the server and client, streamlining the codebase.
- Introduced new tests for feature lifecycle and worktree integration, ensuring robust functionality and error handling.
2025-12-16 21:49:33 -05:00
Cody Seibert
f9ec7222f2 feat: update resumeFeature API to support optional useWorktrees parameter
- Modified the resumeFeature method across multiple files to accept an optional useWorktrees parameter, defaulting to false for improved control over worktree usage.
- Updated related hooks and service methods to ensure consistent handling of the new parameter.
- Enhanced server route logic to reflect the change, ensuring worktrees are only utilized when explicitly enabled.
2025-12-16 19:02:30 -05:00
Cody Seibert
360b7ebe08 fix: enhance test stability and error handling for worktree operations
- Updated feature lifecycle tests to ensure the correct modal close button is selected, improving test reliability.
- Refactored worktree integration tests for better readability and maintainability by formatting function calls and assertions.
- Introduced error handling improvements in the server routes to suppress unnecessary ENOENT logs for optional files, reducing noise in test outputs.
- Enhanced logging for worktree errors to conditionally suppress expected errors in test environments, improving clarity in error reporting.
2025-12-16 18:44:52 -05:00
Cody Seibert
ebc99d06eb Merge branch 'feature/worktrees' of github.com:AutoMaker-Org/automaker into feature/worktrees 2025-12-16 17:43:24 -05:00
Cody Seibert
f2600821d6 feat: implement autocomplete component and enhance server mock functionality
- Introduced a new Autocomplete component for improved user experience in selecting options across various UI components.
- Refactored BranchAutocomplete and CategoryAutocomplete to utilize the new Autocomplete component, streamlining code and enhancing maintainability.
- Updated Playwright configuration to support mock agent functionality during CI/CD, allowing for simulated API interactions without real calls.
- Added comprehensive end-to-end tests for feature lifecycle, ensuring robust validation of the complete feature management process.
- Enhanced auto-mode service to support mock responses, improving testing efficiency and reliability.
2025-12-16 17:43:23 -05:00
Kacper
6e08126875 feat: enhance CreatePRDialog and server-side PR creation logic
- Updated CreatePRDialog to reset form fields selectively when opened, preserving API response states until the dialog closes.
- Improved user feedback by adjusting toast notifications for branch push success and PR creation failures.
- Enhanced cross-platform compatibility in the server-side PR creation logic by refining path resolution and remote URL parsing.
- Implemented fallback mechanisms for retrieving repository URLs, ensuring robustness across different environments.
2025-12-16 23:28:53 +01:00
Cody Seibert
176eeca096 feat: enhance worktree functionality and UI integration
- Updated KanbanCard to conditionally display status badges based on feature attributes, improving visual feedback.
- Enhanced WorktreeSelector to conditionally render based on the worktree feature toggle, ensuring a cleaner UI when worktrees are disabled.
- Modified AddFeatureDialog and EditFeatureDialog to include branch selection only when worktrees are enabled, streamlining the feature creation process.
- Refactored useBoardActions and useBoardDragDrop hooks to create worktrees only when the feature is enabled, optimizing performance.
- Introduced comprehensive integration tests for worktree operations, ensuring robust functionality and error handling across various scenarios.
2025-12-16 17:16:34 -05:00
Cody Seibert
f6a9ae6335 Merge branch 'feature/worktrees' of github.com:AutoMaker-Org/automaker into feature/worktrees 2025-12-16 16:36:49 -05:00
Cody Seibert
30db67f89c feat: enhance worktree management and feature filtering
- Added logic to show all local branches as suggestions in the branch autocomplete, allowing users to type new branch names.
- Implemented current worktree information retrieval for filtering features based on the selected worktree's branch.
- Updated feature handling to filter backlog features by the currently selected worktree branch, ensuring only relevant features are displayed.
- Enhanced the WorktreeSelector component to utilize branch names for determining the appropriate worktree for features.
- Introduced integration tests for worktree creation, deletion, and feature management to ensure robust functionality.
2025-12-16 16:36:47 -05:00
Kacper
da90bafde8 feat: enhance path resolution for cross-platform compatibility in worktree handling
- Updated the worktree creation and retrieval logic to resolve paths to absolute for improved cross-platform compatibility.
- Ensured that provided worktree paths are validated and resolved correctly, preventing issues on different operating systems.
- Refactored existing functions to consistently return absolute paths, enhancing reliability across Windows, macOS, and Linux environments.
2025-12-16 22:16:21 +01:00
Cody Seibert
04d263b1ed feat: enhance worktree creation logic with existing worktree checks
- Added functionality to check for existing worktrees for a branch before creating a new one in the create worktree endpoint.
- Introduced a helper function to find existing worktrees by parsing the output of `git worktree list`.
- Updated the auto mode service to utilize the new worktree checking logic, improving efficiency and user experience.
- Removed redundant checks for existing worktrees to streamline the creation process.
2025-12-16 16:01:23 -05:00
Web Dev Cody
e8e79d8446 Merge pull request #91 from AutoMaker-Org/new-fixes-terminal
feat: enhance terminal functionality with debouncing and resize valid…
2025-12-16 15:57:06 -05:00
Cody Seibert
8c24381759 feat: add GitHub setup step and enhance setup flow
- Introduced a new GitHubSetupStep component for GitHub CLI configuration during the setup process.
- Updated SetupView to include the GitHub step in the setup flow, allowing users to skip or proceed based on their GitHub CLI status.
- Enhanced state management to track GitHub CLI installation and authentication status.
- Added logging for transitions between setup steps to improve user feedback.
- Updated related files to ensure cross-platform path normalization and compatibility.
2025-12-16 13:56:53 -05:00
Cody Seibert
8482cdab87 refactor: improve KanbanCard styling and enhance path normalization in worktree routes
- Updated the button variant in KanbanCard for better visual consistency.
- Adjusted CSS classes for improved styling of shortcut keys.
- Introduced a normalizePath function to ensure consistent path formatting across platforms.
- Updated worktree routes to utilize normalizePath for path handling, enhancing cross-platform compatibility.
2025-12-16 13:09:20 -05:00
Cody Seibert
064a395c4c fixing some bugs 2025-12-16 12:54:53 -05:00
Cody Seibert
d103d0aa45 default editor fixes, fix bug with worktree panel not showing 2025-12-16 12:35:36 -05:00
Cody Seibert
9509c8ea00 refactor: optimize worktree retrieval in BoardView component
- Introduced a stable empty array to prevent infinite loops in the selector.
- Updated worktree retrieval logic to use memoization for improved performance and clarity.
- Adjusted the handling of worktrees by project to ensure proper state management.
2025-12-16 12:18:18 -05:00
Cody Seibert
26b73fdaa9 Merge branch 'main' into feature/worktrees 2025-12-16 12:14:05 -05:00
Cody Seibert
a3c9c9cee5 Implement branch selection and worktree management features
- Added a new BranchAutocomplete component for selecting branches in feature dialogs.
- Enhanced BoardView to fetch and display branch suggestions.
- Updated CreateWorktreeDialog and EditFeatureDialog to include branch selection.
- Modified worktree management to ensure proper handling of branch-specific worktrees.
- Refactored related components and hooks to support the new branch management functionality.
- Removed unused revert and merge handlers from Kanban components for cleaner code.
2025-12-16 12:12:10 -05:00
Web Dev Cody
0d088962a0 Merge pull request #129 from leonvanzyl/main
fix: add Windows support for Claude CLI detection
2025-12-16 11:13:58 -05:00
trueheads
2ce4e02ada fix: implemented gemini appdata suggestion 2025-12-16 10:06:28 -06:00
Web Dev Cody
fad3ed1aae Merge pull request #128 from AutoMaker-Org/feat/improve-ui-add-feature-dialog
feat: enhance CategoryAutocomplete and AddFeatureDialog components
2025-12-16 10:16:50 -05:00
Leon van Zyl
81444d5603 fix: add Windows support for Claude CLI detection
Previously, the Claude CLI detection failed on Windows due to:

1. Shell command incompatibility
   - Used 'which claude || where claude 2>/dev/null' which fails on Windows
   - 'which' doesn't exist on Windows
   - '2>/dev/null' is Unix syntax (Windows uses '2>nul')
   - Now uses platform-specific commands: 'where' on Windows, 'which' on Unix

2. Missing Windows fallback paths
   - Only checked Unix paths like ~/.local/bin/claude
   - Added Windows-specific paths:
     * %USERPROFILE%\.local\bin\claude.exe
     * %APPDATA%\npm\claude.cmd
     * %USERPROFILE%\.npm-global\bin\claude.cmd

3. Credentials file detection
   - Only checked for 'credentials.json'
   - Claude CLI on Windows uses '.credentials.json' (hidden file)
   - Now checks both '.credentials.json' and 'credentials.json'

Additional improvements:
- Handle 'where' command returning multiple paths (takes first match)
- Maintains full backward compatibility with Linux and macOS
2025-12-16 17:16:09 +02:00
Kacper
e8b65dbd0b chore: remove .automaker file
- Deleted the .automaker causing .automaker folder to be removed
2025-12-16 16:09:51 +01:00
Kacper
f86bb3eab8 feat: enhance CategoryAutocomplete and AddFeatureDialog components
- Added responsive width handling to the CategoryAutocomplete component, ensuring the popover adjusts based on the trigger button's width.
- Updated the AddFeatureDialog button width from 180px to 200px for improved layout consistency.
2025-12-16 14:49:23 +01:00
Cody Seibert
54a102f029 moving pull push button 2025-12-16 02:41:00 -05:00
Web Dev Cody
2ee4ec65b4 Merge pull request #124 from AutoMaker-Org/feat/feature-priority
Added UI features back for priority, added/fixed category generation.…
2025-12-16 02:40:36 -05:00
Cody Seibert
166679cd36 adding a worktree switch feature 2025-12-16 02:39:11 -05:00
Cody Seibert
b95c54a539 remove duplicate commands in dropdowns 2025-12-16 02:27:19 -05:00
trueheads
e71be53459 fix: add missing imports and state for dependency tree dialog 2025-12-16 01:27:12 -06:00
Cody Seibert
8c5759d74e fixes 2025-12-16 02:24:49 -05:00
Cody Seibert
bd1c4e0690 prevent keyboard shortcuts when typing branch name 2025-12-16 02:20:10 -05:00
Cody Seibert
9a428eefe0 adding branch switcher support 2025-12-16 02:14:42 -05:00
Ben
8774d28bc4 Merge branch 'main' into feat/feature-priority 2025-12-16 01:13:27 -06:00
trueheads
9eb9c070cd resolving gemini errors and removing dupe code 2025-12-16 01:09:22 -06:00
Web Dev Cody
7110a690e1 Merge pull request #123 from AutoMaker-Org/enhance-feature-with-ai
feat: add AI enhancement feature to settings and board views
2025-12-16 02:01:30 -05:00
SuperComboGamer
1194e7d51e test: add unit tests for enhancement prompts functionality
- Introduced comprehensive unit tests for the enhancement prompts module, covering system prompt constants, example constants, and various utility functions.
- Validated the behavior of `getEnhancementPrompt`, `getSystemPrompt`, `getExamples`, `buildUserPrompt`, `isValidEnhancementMode`, and `getAvailableEnhancementModes`.
- Ensured that all enhancement modes are correctly handled and that prompts are built as expected.

This addition enhances code reliability by ensuring that the enhancement prompts logic is thoroughly tested.
2025-12-16 01:52:57 -05:00
SuperComboGamer
1641f9da5e refactor: streamline AI enhancement logic in feature dialogs
- Simplified the handling of enhanced text in AddFeatureDialog and EditFeatureDialog by storing the enhanced text in a variable before updating the state.
- Updated the dropdown menu and button components to ensure consistent styling and behavior across both dialogs.
- Enhanced user experience by ensuring the cursor style indicates interactivity in the dropdown menus.

This refactor improves code readability and maintains a consistent UI experience.
2025-12-16 01:48:28 -05:00
trueheads
ff4887773e Added UI features back for priority, added/fixed category generation. Added dependency trees for stories, see PR for rest 2025-12-16 00:42:55 -06:00
Web Dev Cody
15a580ece9 Merge pull request #122 from AutoMaker-Org/feature/delete-all-archived-sessions
feature/delete-all-archived-sessions
2025-12-16 01:30:05 -05:00
Web Dev Cody
b37d258698 Merge pull request #121 from AutoMaker-Org/ux/logs-button
ux/logs-button
2025-12-16 01:29:53 -05:00
Web Dev Cody
e0e7bb9190 Merge pull request #120 from AutoMaker-Org/fix-diffs
feat: enhance git diff functionality for untracked files
2025-12-16 01:29:33 -05:00
SuperComboGamer
7131c70186 feat: add AI enhancement feature to settings and board views
- Introduced AIEnhancementSection to settings view for selecting enhancement models.
- Implemented enhancement functionality in AddFeatureDialog and EditFeatureDialog, allowing users to enhance feature descriptions with AI.
- Added dropdown menu for selecting enhancement modes (improve, technical, simplify, acceptance).
- Integrated new API endpoints for enhancing text using Claude AI.
- Updated navigation to include AI enhancement section in settings.

This enhances user experience by providing AI-powered text enhancement capabilities directly within the application.
2025-12-16 01:28:35 -05:00
Cody Seibert
be98a59023 ttt 2025-12-16 01:20:41 -05:00
Cody Seibert
7e517101a0 fix 2025-12-16 01:18:34 -05:00
Cody Seibert
92f60cceb5 fix 2025-12-16 01:17:02 -05:00
Cody Seibert
b1dcb8a9d7 fix color of the logs button to be themed 2025-12-16 01:03:07 -05:00
SuperComboGamer
ec6ec7d569 feat: integrate git repository diff handling into common route
- Added functions to check if a path is a git repository and to parse git status output into a structured format.
- Refactored diff handling in both git and worktree routes to utilize the new common functions, improving code reuse and maintainability.
- Enhanced error logging for better debugging during git operations.

This update streamlines the process of retrieving diffs for both git and non-git directories, ensuring a consistent approach across the application.
2025-12-16 00:50:58 -05:00
SuperComboGamer
31bb069e75 feat: enhance git diff functionality for untracked files
- Implemented synthetic diff generation for untracked files in both git and non-git directories.
- Added fallback UI in the GitDiffPanel for files without diff content, ensuring better user experience.
- Improved error handling and logging for git operations, enhancing reliability in file diff retrieval.

This update allows users to see diffs for new files that are not yet tracked by git, improving the overall functionality of the diff panel.
2025-12-16 00:42:27 -05:00
Web Dev Cody
363be54303 Merge pull request #119 from AutoMaker-Org/chore/cleanup-gitignore
chore: cleanup gitignore and remove stale tracked files
2025-12-16 00:35:03 -05:00
Web Dev Cody
ca0f6661d3 Merge pull request #118 from AutoMaker-Org/refactor/settings-view-separate-panels
refactor: convert settings page to separate view panels
2025-12-16 00:29:04 -05:00
SuperComboGamer
cd803cd9bc chore: cleanup gitignore and remove stale tracked files
- Remove user-specific files from tracking:
  - .claude/settings.local.json (contains machine-specific paths)
  - backup.json (application state data)
  - logs/server.log (runtime log)
  - test-results/.last-run.json (playwright state)
  - apps/.DS_Store (macOS metadata)
  - apps/app/playwright.config.ts.bak (backup file)

- Add comprehensive gitignore patterns:
  - OS files: .DS_Store, Thumbs.db, Desktop.ini
  - IDE/Editor configs: .vscode/, .idea/, sublime
  - Backup/temp files: *.bak, *.swp, *.tmp, *~
  - Local settings: *.local.json
  - Test artifacts: test-results/, coverage/, .nyc_output/
  - Environment files: .env, .env.local (keeps .example)
  - Build outputs: .turbo/, build/, out/

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 00:20:37 -05:00
Cody Seibert
cbdc88c5d0 docs: add comprehensive Git workflow guide for branching, committing, and creating pull requests
- Introduced a new document detailing the standard workflow for Git operations including branch creation, staging, committing, pushing, and PR creation.
- Included best practices, troubleshooting tips, and quick reference commands to enhance user understanding and efficiency in using Git.
- Emphasized the importance of clear commit messages and branch naming conventions.
2025-12-15 23:16:04 -05:00
Cody Seibert
44b548c5c8 fix: address PR review comments
- Remove redundant case 'api-keys' from switch (handled by default)
- Improve type safety by using SettingsViewId in NavigationItem interface
- Simplify onCheckedChange callback in AudioSection
- Import NAV_ITEMS from config instead of duplicating locally
- Update SettingsNavigation props to use SettingsViewId type

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 23:11:12 -05:00
Cody Seibert
cc2ac3542d refactor: convert settings page to separate view panels
- Replace scroll-based navigation with view switching
- Add useSettingsView hook for managing active panel state
- Extract Audio section into its own component
- Remove scroll-mt-6 classes and IDs from section components
- Update navigation config to reflect current sections
- Create barrel export for settings-view hooks

This improves performance by only rendering the active section
instead of all sections in a single scrollable container.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 22:17:32 -05:00
Web Dev Cody
25044d40b9 Merge pull request #116 from AutoMaker-Org/feat/enchance-pasting-images
feat: add image paste functionality to DescriptionImageDropZone compo…
2025-12-15 21:52:48 -05:00
Web Dev Cody
676169b189 Merge pull request #114 from AutoMaker-Org/refactor/board-view-folder-structure
Refactor/board view folder structure
2025-12-15 21:51:49 -05:00
Kacper
c8c05efb8d fix: remove onClick handler causing wierd issue on windows that try to open microsoft store 2025-12-16 03:18:49 +01:00
Kacper
23cef5fd82 feat: enhance image handling in chat and drop zone components
- Updated ImageAttachment interface to make 'id' and 'size' optional for better compatibility with server messages.
- Improved image display in AgentView for user messages, including a count of attached images and a clickable preview.
- Refined ImageDropZone to conditionally render file size and ensure proper handling of image removal actions.
2025-12-16 03:11:01 +01:00
Web Dev Cody
c0b0b30541 Merge pull request #115 from AutoMaker-Org/api-key-redesign
chore: add lockfile linting check and convert SSH URLs to HTTPS
2025-12-15 20:54:07 -05:00
Kacper
7eeba5f17c feat: add image paste functionality to DescriptionImageDropZone component
- Implemented handlePaste function to process images from clipboard across all OS.
- Updated the component to handle pasted images and prevent default paste behavior.
- Enhanced user instructions to include pasting images in the UI.

Added a utility function to simulate pasting images in tests, ensuring cross-platform compatibility.
2025-12-16 02:49:26 +01:00
Cody Seibert
fcb2457e17 chore: update node-gyp dependency resolution from SSH to HTTPS for improved accessibilityapi-key-redesign 2025-12-15 20:24:11 -05:00
Cody Seibert
02e378905e chore: add lockfile linting check and convert SSH URLs to HTTPS
- Add npm script to check for SSH URLs in package-lock.json
- Convert electron/node-gyp dependency from SSH to HTTPS URL
- Add workflow step to lint lockfile in CI environment

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-15 20:21:09 -05:00
Web Dev Cody
87c0ab6daa Merge pull request #108 from AutoMaker-Org/api-key-redesign
redesign our approach for api keys to not use claude setup-token
2025-12-15 20:13:54 -05:00
Kacper
60f9da9208 fix: resolve CI OOM by fixing bloated package-lock.json
The package-lock.json was incorrectly regenerated with 1170 entries
instead of 452 (2.5x bloat) when cross-spawn was added to root.
This caused npm install to run out of memory on GitHub Actions.

- Remove unnecessary cross-spawn from root package.json
- Restore package-lock.json to proper workspace structure
- Remove NODE_OPTIONS workaround from workflow files

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 02:05:35 +01:00
SuperComboGamer
ff318d6ef5 fix: increase Node memory to 6GB and add --prefer-offline for npm install
- Increase NODE_OPTIONS from 4GB to 6GB to prevent OOM
- Add --prefer-offline to reduce network calls and speed up install

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 19:58:12 -05:00
Web Dev Cody
93d9e08de1 Merge pull request #112 from AutoMaker-Org/fix/setting-view-item-order
fix: reorder audio item in settings view navigation
2025-12-15 19:52:28 -05:00
SuperComboGamer
7edca6b823 try 2025-12-15 19:49:50 -05:00
Kacper
73eebe7c9e fix: reorder audio item in settings view navigation 2025-12-16 01:21:08 +01:00
Cody Seibert
049f9a9e37 chore: add Git configuration for HTTPS in workflow files to support CI environment 2025-12-15 19:19:23 -05:00
Kacper
658cbb8bd6 refactor(board-view): reorganize into modular folder structure
- Extract board-view into organized subfolders following new pattern:
  - components/: kanban-card, kanban-column
  - dialogs/: all dialog and modal components (8 files)
  - hooks/: all board-specific hooks (10 files)
  - shared/: reusable components between dialogs (model-selector, etc.)
- Rename all files to kebab-case convention
- Add barrel exports (index.ts) for clean imports
- Add docs/folder-pattern.md documenting the folder structure
- Reduce board-view.tsx from ~3600 lines to ~490 lines

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 01:10:28 +01:00
Cody Seibert
19f1c32805 chore: update Node.js version in workflow files from 20 to 22 2025-12-15 19:08:00 -05:00
Cody Seibert
ece8ff8cbc Merge branch 'main' into api-key-redesign 2025-12-15 19:00:14 -05:00
Cody Seibert
a3a648aef1 feat: add Accordion component with customizable behavior and animations, update Checkbox and Slider components for improved functionality, and enhance package dependencies 2025-12-15 18:57:32 -05:00
Web Dev Cody
3bc2b74d30 Merge pull request #105 from AutoMaker-Org/fix/bug-button-position
Fix/bug button position
2025-12-15 17:57:08 -05:00
Kacper
9d17cd7d9c refactor(board-view): extract BoardSearchBar component 2025-12-15 23:38:05 +01:00
Kacper
091c6b2737 refactor(board-view): extract BoardHeader component 2025-12-15 23:36:49 +01:00
Kacper
2880314931 refactor(board-view): extract EditFeatureDialog component 2025-12-15 23:35:59 +01:00
Kacper
0102719067 refactor(board-view): extract AddFeatureDialog component 2025-12-15 23:34:38 +01:00
trueheads
123b471b68 How many Devs does it take to center a navbar icon? 3, as it turns out. 2025-12-15 15:13:43 -06:00
Cody Seibert
b66d228460 feat: enhance CLI and API key verification buttons to hide when already verified 2025-12-15 15:12:49 -05:00
Kacper
770d67d8c4 feat: refactor bug report button into a reusable component for improved sidebar functionality 2025-12-15 20:49:22 +01:00
Cody Seibert
d42857ec26 refactor: remove CLAUDE_CODE_OAUTH_TOKEN references and update authentication to use ANTHROPIC_API_KEY exclusively 2025-12-15 14:33:58 -05:00
Cody Seibert
54b977ee1b redesign our approach for api keys to not use claude setup-token 2025-12-15 14:24:18 -05:00
Kacper
e8999ba908 chore: update README to include a detailed Table of Contents and Community & Support section 2025-12-15 20:14:44 +01:00
Kacper
96c4383b29 feat: Whe sidebar is closed the bug button is overlapping the l... 2025-12-15 20:07:27 +01:00
Web Dev Cody
93d1d2c41a Merge pull request #104 from AutoMaker-Org/chore/update-readme
chore: update clone url from ssh to https
2025-12-15 13:52:58 -05:00
Shirone
b075af5bc9 chore: update clone url from ssh to https 2025-12-15 19:51:03 +01:00
Web Dev Cody
07ca7fccb8 Merge pull request #102 from AutoMaker-Org/feat/disable-worktree-in-ui
feat: In our Feature Defaults section in setting view we have a...
2025-12-15 12:47:31 -05:00
Web Dev Cody
797643ffdc Merge pull request #101 from AutoMaker-Org/readme-update
updating readme to reflect logo and featureset
2025-12-15 12:47:11 -05:00
trueheads
7d4052be95 adjustments 2025-12-15 11:24:01 -06:00
Shirone
1036719f2a Update apps/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-15 18:19:46 +01:00
Kacper
1ab520eda3 feat: In our Feature Defaults section in setting view we have a...
Implemented by Automaker auto-mode
2025-12-15 18:17:19 +01:00
trueheads
658f7d816e updating readme to reflect logo and featureset 2025-12-15 11:01:36 -06:00
Web Dev Cody
835ab516a6 Merge pull request #99 from AutoMaker-Org/feat/ai-profiles-view-enhancement
Enhanced AI profiles view with better UX and comprehensive test coverage.
2025-12-15 10:06:16 -05:00
Web Dev Cody
00e098e57d Merge pull request #97 from AutoMaker-Org/ui-tweaks
feat: implement completed features management in BoardView and Kanban…
2025-12-15 10:06:04 -05:00
Cody Seibert
f04cac8e2f style: refine sidebar and dropdown menu components for improved UI
- Simplified the sidebar button's class structure by removing unnecessary overflow styling.
- Enhanced the visual representation of the trashed projects count with updated styling for better visibility.
- Wrapped the dropdown menu's subcontent in a portal for improved rendering and performance.
2025-12-15 09:59:20 -05:00
Kacper
c1b9f1cb28 refactor: address code review suggestions
- Simplify countCustomProfiles by reusing getCustomProfiles helper
- Fix misleading test name and assertion for thinking level controls
2025-12-15 15:11:15 +01:00
Kacper
7d8670ff1f feat: add comprehensive tests for AI profiles view
- Introduced a new test suite for the AI profiles view, covering profile creation, editing, deletion, and reordering functionalities.
- Implemented tests for form validation, including checks for empty and whitespace-only profile names.
- Enhanced utility functions for profile interactions, including profile card retrieval and dialog management.
- Improved error handling in toast notifications for better test reliability.
- Updated test utilities to support the new profiles view structure.
2025-12-15 14:59:27 +01:00
Kacper
9f9bcaff65 feat: enhanced ai profiles view
- Refactored profiles view into modular components for better maintainability
- Fixed input/textarea borders showing consistently when not focused (border-input -> border-border)
- Added animated hover effects on profile cards (border color and icon animations)
- Removed redundant Create Profile button, made empty state interactive
- Added confirmation dialog for profile deletion to prevent accidental removal
- Improved dialog scrolling behavior with max-height constraints
- Added ARIA labels to profile card buttons for better accessibility
- Created reusable DeleteConfirmDialog component
2025-12-15 13:02:56 +01:00
Cody Seibert
25b1789b0a fix: update ProjectSetupDialog to correctly handle open state and improve BoardView layout
- Added missing onOpenChange call in ProjectSetupDialog to ensure proper state management.
- Reformatted the COLUMNS array in BoardView for improved readability and consistency.
- Adjusted DragOverlay component's formatting for better code clarity.
2025-12-15 01:13:37 -05:00
Cody Seibert
2c8add3b54 Merge branch 'main' into ui-tweaks 2025-12-15 01:13:30 -05:00
Cody Seibert
f25d62fe25 feat: implement project setup dialog and refactor sidebar integration
- Added a new ProjectSetupDialog component to facilitate project specification generation, enhancing user experience by guiding users through project setup.
- Refactored the Sidebar component to integrate the new ProjectSetupDialog, replacing the previous inline dialog implementation for improved code organization and maintainability.
- Updated the sidebar to handle project overview and feature generation options, streamlining the project setup process.
- Removed the old dialog implementation from the Sidebar, reducing code duplication and improving clarity.
2025-12-15 01:07:47 -05:00
Web Dev Cody
7a9f55e1bd Merge pull request #96 from AutoMaker-Org/polish-UI
style: enhance UI components with improved styling and layout
2025-12-15 00:55:09 -05:00
trueheads
fbdf1689b3 added logo.png to satisfy e2e github error 2025-12-14 22:44:48 -06:00
trueheads
29ba2c5936 adjusted the application icon and added support for mac/linux/win 2025-12-14 22:32:17 -06:00
trueheads
493050fba1 Logo SVG now matches color theme that user has selected 2025-12-14 22:01:55 -06:00
trueheads
455f6fa95b Fixed Logo with new SVG and alignment. Also fixed Agent Runner 'agent sessions' styling to remove rounded border 2025-12-14 21:42:47 -06:00
Web Dev Cody
e14957900e Merge pull request #98 from AutoMaker-Org/feat/new-e2e-test
Refactor: Restructure test utilities and add context view E2E tests
2025-12-14 22:13:49 -05:00
Kacper
c0e0f8d214 refactor: enhance spec editor tests by utilizing utility functions
- Replaced direct element selectors with utility functions for improved readability and maintainability in spec editor tests.
- Streamlined waiting mechanisms by using the new waitForElement function, enhancing test reliability.
- Updated test cases to ensure consistent handling of element visibility and initialization, resulting in more efficient and clearer tests.
2025-12-15 02:52:02 +01:00
Kacper
caae869501 refactor: improve context view tests by utilizing utility functions
- Replaced direct element locators with utility functions for better readability and maintainability in context view tests.
- Removed unnecessary wait statements and replaced them with appropriate utility functions to enhance test reliability.
- Streamlined the verification process for file visibility and content loading, ensuring tests are more efficient and easier to understand.
2025-12-15 02:49:08 +01:00
Kacper
b998d253bb chore: update .gitignore and remove server log file
- Updated .gitignore to exclude the new 'logs' directory.
- Removed the 'server.log' file from the logs directory to clean up unnecessary log data.
2025-12-15 02:40:47 +01:00
Kacper
0b1123e3ce refactor: restructure test utilities and enhance context view tests
- Refactored test utilities by consolidating and organizing helper functions into dedicated modules for better maintainability and clarity.
- Introduced new utility functions for interactions, waiting, and element retrieval, improving the readability of test cases.
- Updated context view tests to utilize the new utility functions, enhancing test reliability and reducing code duplication.
- Removed deprecated utility functions and ensured all tests are aligned with the new structure.
2025-12-15 02:40:09 +01:00
Kacper
a412f5d0fb feat: add context view tests and enhance context drop zone
- Introduced a new test suite for the Context View, covering file management, editing, and edge cases.
- Added a data-testid attribute to the context drop zone for improved testability.
- Implemented various tests for creating, editing, deleting, and uploading context files, ensuring robust functionality and user experience.
2025-12-15 02:39:56 +01:00
Cody Seibert
919e08689a refactor: streamline feature implementation handling in BoardView and KanbanCard
- Introduced a helper function, handleStartImplementation, to manage concurrency checks and feature status updates when moving features from backlog to in_progress.
- Simplified the onImplement callback in KanbanCard to utilize the new helper function, enhancing code readability and maintainability.
- Removed redundant concurrency checks from multiple locations, centralizing the logic for better consistency and reducing code duplication.
2025-12-14 20:21:42 -05:00
Cody Seibert
72e803b56d feat: implement completed features management in BoardView and KanbanCard
- Added functionality to complete and unarchive features, allowing users to manage feature statuses effectively.
- Introduced a modal to display completed features, enhancing user experience by providing a dedicated view for archived items.
- Updated KanbanCard to include buttons for completing features and managing their states, improving interactivity and workflow.
- Modified the Feature interface to include a new "completed" status, ensuring comprehensive state management across the application.
2025-12-14 20:06:52 -05:00
SuperComboGamer
e378704c63 style: enhance UI components with improved styling and layout
- Updated global CSS to include new status colors for better visual feedback.
- Refined button, badge, card, and input components with enhanced styles and transitions for a more polished user experience.
- Adjusted sidebar and dialog components for improved aesthetics and usability.
- Implemented gradient backgrounds and shadow effects across various sections to elevate the overall design.
- Enhanced keyboard shortcuts and settings views with consistent styling and layout adjustments for better accessibility.
2025-12-14 19:21:20 -05:00
Web Dev Cody
f6c50ce336 Merge pull request #95 from AutoMaker-Org/refactor-api-approach
refactoring the api endpoints to be separate files to reduce context …
2025-12-14 18:29:49 -05:00
Cody Seibert
063224966c refactor: update unit tests for setRunningState to use new state management
- Replaced direct access to state variables with calls to the new getSpecRegenerationStatus function in unit tests for setRunningState.
- This change improves encapsulation and ensures that tests reflect the updated state management logic.
2025-12-14 18:24:29 -05:00
Cody Seibert
5d40e694a5 docs: update Agentic Jumpstart course link in README
- Modified the link to the Agentic Jumpstart course to include a UTM parameter for better tracking of referral sources.
- This change enhances the documentation by providing a more effective way to analyze course engagement.
2025-12-14 18:20:00 -05:00
Cody Seibert
4405a97d9b fix: enhance error logging for JSON parsing in suggestions generation
- Added error logging for failed JSON parsing in the suggestions generation route to improve debugging capabilities.
- This change ensures that any parsing errors are captured and logged, aiding in the identification of issues with AI response handling.
2025-12-14 18:18:21 -05:00
Cody Seibert
6733de9e0d refactor: encapsulate state management for spec and suggestions generation
- Made the generation status variables private and introduced getter functions for both spec and suggestions generation states.
- Updated relevant route handlers to utilize the new getter functions, improving encapsulation and reducing direct access to shared state.
- Enhanced code maintainability by centralizing state management logic.
2025-12-14 18:18:11 -05:00
Cody Seibert
01bae7d43e refactor: centralize error handling utilities across route modules
- Introduced a new common utility module for error handling, providing consistent methods for retrieving error messages and logging errors.
- Updated individual route modules to utilize the shared error handling functions, reducing code duplication and improving maintainability.
- Ensured all routes now log errors in a standardized format, enhancing debugging and monitoring capabilities.
2025-12-14 17:59:16 -05:00
Cody Seibert
6b30271441 refactoring the api endpoints to be separate files to reduce context usage 2025-12-14 17:53:21 -05:00
Web Dev Cody
cdc8334d82 Merge pull request #94 from AutoMaker-Org/app_spec_fixes
working on improving the app spec page
2025-12-14 17:49:55 -05:00
Web Dev Cody
4a3a98b562 Merge pull request #90 from AutoMaker-Org/fix-agent-runner
feat: implement SDK session ID handling for conversation continuity
2025-12-14 17:49:40 -05:00
Cody Seibert
c280225a4e refactor: reorganize spec regeneration routes and add unit tests
- Removed the old spec regeneration routes and replaced them with a new structure under the app-spec directory for better modularity.
- Introduced unit tests for common functionalities in app-spec, covering state management and error handling.
- Added documentation on route organization patterns to improve maintainability and clarity for future development.
2025-12-14 17:45:11 -05:00
Cody Seibert
b3ea506a73 working on improving the app spec page 2025-12-14 17:38:12 -05:00
Web Dev Cody
590437c78b Merge pull request #93 from AutoMaker-Org/random-fixes
Random fixes
2025-12-14 14:50:31 -05:00
SuperComboGamer
a5c61b0546 feat: improve terminal creation and resizing logic
- Added a debouncing mechanism for terminal creation to prevent rapid requests.
- Enhanced terminal resizing with rate limiting and suppression of output during resize to avoid duplicates.
- Updated scrollback handling to clear pending output when establishing new WebSocket connections.
- Improved stability of terminal fitting logic by ensuring dimensions are stable before fitting.
2025-12-14 14:40:34 -05:00
GTheMachine
790a1b8e20 Merge pull request #92 from AutoMaker-Org/copilot/fix-build-tests-issue
Fix test expectation for fs.readFile call count in agent-service.test.ts
2025-12-14 14:36:00 -05:00
Cody Seibert
fa47264c76 chore: update package-lock.json with new dependencies for CodeMirror and related libraries
- Added new dependencies for CodeMirror, including lang-xml, theme-one-dark, and various utilities to enhance the XML editing experience.
- Updated existing dependencies to their latest versions for improved functionality and security.
- Included additional modules for better code handling and syntax highlighting.
2025-12-14 14:19:12 -05:00
Cody Seibert
a4075fb637 Merge branch 'main' into random-fixes 2025-12-14 14:19:06 -05:00
Cody Seibert
20a7c8b5a8 feat: implement E2E testing workflow and enhance XML syntax editor
- Added a new GitHub Actions workflow for end-to-end (E2E) testing, including setup for Node.js, Playwright, and server initialization.
- Introduced a setup script for E2E test fixtures to create necessary directories and files.
- Integrated CodeMirror for XML syntax editing in the XmlSyntaxEditor component, improving code highlighting and editing experience.
- Updated package dependencies in package.json and package-lock.json to include new libraries for XML handling and theming.
- Refactored various components for improved readability and consistency, including the sidebar and file browser dialog.
- Added tests for spec editor persistence to ensure data integrity across sessions.
2025-12-14 14:12:38 -05:00
copilot-swe-agent[bot]
202494156b Fix test expectation for fs.readFile call count in agent-service.test.ts
The test "should reuse existing session if already started" expected fs.readFile to be called 1 time, but startConversation calls it 2 times on first call (loadSession + loadMetadata). The second call correctly reuses the in-memory session.

Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com>
2025-12-14 18:57:54 +00:00
copilot-swe-agent[bot]
7558fed4e4 Initial plan 2025-12-14 18:51:25 +00:00
SuperComboGamer
480589510e feat: enhance terminal functionality with debouncing and resize validation
- Implemented debouncing for terminal tab creation to prevent rapid requests.
- Improved terminal resizing logic with validation for minimum dimensions and deduplication of resize messages.
- Updated terminal panel to handle focus and cleanup more efficiently, preventing memory leaks.
- Enhanced initial connection handling to ensure scrollback data is sent before subscribing to terminal data.
2025-12-14 13:48:26 -05:00
SuperComboGamer
999ed5b51b fix storage for long term 2025-12-14 13:24:11 -05:00
Web Dev Cody
589155fa1c Merge pull request #89 from AutoMaker-Org/logging
chore: update dependencies and improve project structure
2025-12-14 12:42:58 -05:00
Cody Seibert
ae13551033 fixing the input box issue 2025-12-14 12:41:19 -05:00
Cody Seibert
038caeb2a0 test: update conversation history test to include sdkSessionId handling
- Renamed test case to clarify that it handles conversation history with sdkSessionId using the resume option.
- Updated assertions to verify that the sdk.query method is called with the correct options when a session ID is provided.
2025-12-14 11:10:57 -05:00
Cody Seibert
7b34c9a108 test: update security tests to allow all paths with permissions disabled
- Modified test cases in security.test.ts to reflect that all paths are allowed when permissions are disabled.
- Updated descriptions of test cases to clarify the new behavior regarding path validation and error handling.
2025-12-14 11:04:28 -05:00
SuperComboGamer
5a1fe23ddb feat: implement SDK session ID handling for conversation continuity
- Added support for resuming conversations using the Claude SDK session ID.
- Updated the ClaudeProvider to conditionally resume sessions based on the presence of a session ID and conversation history.
- Enhanced the AgentService to capture and store the SDK session ID from incoming messages, ensuring continuity in conversations.
2025-12-14 11:02:42 -05:00
Cody Seibert
9bb843f82f chore: update dependencies and improve project structure
- Added `morgan` for enhanced request logging in the server.
- Updated `package-lock.json` to include new dependencies and their types.
- Refactored the `NewProjectModal` component for improved readability and structure.
- Enhanced the `FileBrowserDialog` to support initial path selection and improved error handling.
- Updated various components to ensure consistent formatting and better user experience.
- Introduced XML format specification for app specifications to maintain consistency across the application.
2025-12-14 10:59:52 -05:00
Cody Seibert
ebc4f1422a Merge branch 'main' of github.com:webdevcody/automaker 2025-12-14 01:01:01 -05:00
Cody Seibert
96bfa8f131 Add logo_larger.png and update sidebar component for improved branding display
- Introduced a new logo_larger.png file to the public assets.
- Updated the Sidebar component to enhance the branding display based on sidebar state, ensuring a consistent user experience.
2025-12-14 01:01:00 -05:00
Web Dev Cody
406ba14af5 Merge pull request #68 from AutoMaker-Org/fixing-main
Fixing main
2025-12-14 00:57:58 -05:00
Web Dev Cody
13841b1af6 Merge pull request #67 from AutoMaker-Org/move-marketing
Add .DS_Store files and update public assets in marketing app
2025-12-14 00:53:29 -05:00
Cody Seibert
7b3be213e4 refactor: improve auto mode service stop logic and event emission
- Updated the stopAutoLoop method to emit the "auto_mode_stopped" event immediately when the loop is explicitly stopped, enhancing event handling.
- Improved code readability by restructuring feature retrieval calls in integration tests for better clarity.
2025-12-14 00:51:35 -05:00
Cody Seibert
b52b9ba236 feat: enhance project initialization and improve logging in auto mode service
- Added a default categories.json file to the project initialization structure.
- Improved code formatting and readability in the auto-mode-service.ts file by restructuring console log statements and method calls.
- Updated feature status checks to include "backlog" in addition to "pending" and "ready".
2025-12-14 00:43:52 -05:00
Cody Seibert
58f466b443 feat: update terminal shortcut and improve code formatting
- Added a hasInstallScript property to package-lock.json.
- Refactored the app-store.ts file for improved readability by formatting function parameters and object properties.
- Updated the default terminal shortcut from "Cmd+`" to "T" and implemented migration logic for state persistence.
- Incremented version number in the terminal state management to reflect breaking changes.
2025-12-14 00:20:11 -05:00
Cody Seibert
13e3f05a7a refactor: enhance init.sh and server startup error handling
- Refactored init.sh to introduce a reusable function for killing processes on specified ports, improving code clarity and maintainability.
- Added a cleanup function to ensure proper resource management on exit.
- Updated server startup logic in index.ts to handle port conflicts gracefully, providing clear error messages and suggestions for resolution.
- Improved logging for server status and health checks during initialization.
2025-12-13 22:06:53 -05:00
Cody Seibert
7f5cdc0345 chore: update package.json and refactor terminal WebSocket connection handling
- Added a postinstall script in package.json to set permissions for spawn-helper on macOS.
- Refactored the terminal WebSocket connection handling in index.ts for improved readability and consistency.
- Enhanced error logging and connection management in the terminal service.
- Cleaned up formatting and indentation across multiple files for better code clarity.
2025-12-13 22:02:30 -05:00
Cody Seibert
c21a298e07 refactor: improve ClaudeProvider query execution and message handling
- Enhanced the executeQuery method to better handle conversation history and user messages, ensuring compliance with SDK requirements.
- Introduced a default tools array for allowedTools, simplifying the options setup.
- Updated the getAvailableModels method to use type assertions for model tiers and ensured proper return type with TypeScript's satisfies operator.
- Added error handling during query execution to log and propagate errors effectively.
2025-12-13 21:53:55 -05:00
Cody Seibert
3d940e21d5 Merge branch 'main' into move-marketing 2025-12-13 20:29:11 -05:00
Shirone
6446dd5d3a Merge pull request #60 from AutoMaker-Org/feat/add-unit-testing
feat: add unit testing to app/server and remove codex support
2025-12-14 02:27:37 +01:00
Kacper
38cff827b3 Merge branch 'main' into feat/add-unit-testing
Resolved conflicts:
- apps/app/package.json: Combined build:electron scripts from main with postinstall script from feature branch
- package-lock.json: Accepted main version and regenerated with npm install

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 02:22:49 +01:00
Cody Seibert
cc4310b368 Refactor marketing configuration and sidebar display
- Removed the IS_MARKETING flag from app-config.ts to simplify configuration.
- Updated the Sidebar component to always display the "AutoMaker" branding, removing conditional rendering based on the marketing flag.
- Cleaned up package.json by removing the dev:marketing script and ensuring consistency in test commands.
- Cleaned up package-lock.json by removing references to the marketing app and its dependencies.
2025-12-13 20:20:03 -05:00
Cody Seibert
d248e74492 Add .DS_Store files and update public assets in marketing app
- Added .DS_Store files to the root and apps directories.
- Removed outdated icon files: icon_gold.png and icon.png.
- Added new logo_big.png file.
- Deleted logo_larger.png.
- Updated logo.png with new content.
- Removed Dockerfile, package.json, and public HTML files from the marketing app, streamlining the project structure.
2025-12-13 20:18:26 -05:00
Shirone
a3d74fbe6e Merge pull request #65 from AutoMaker-Org/fix-electron-build
feat: enhance Electron build process and server preparation
2025-12-14 02:16:00 +01:00
Web Dev Cody
54dad0135f Merge pull request #66 from AutoMaker-Org/license-tweak
Update LICENSE file to clarify Core Contributor status management and…
2025-12-13 19:30:12 -05:00
Cody Seibert
d9440e86a2 Update LICENSE file to clarify amendment process and section numbering
- Renamed section 8 to "LICENSE AMENDMENTS" and added provisions for unanimous agreement among Core Contributors for any amendments to the License Agreement.
- Renumbered subsequent sections for improved clarity and organization.
2025-12-13 19:26:49 -05:00
Cody Seibert
1f716142af Refine Core Contributor status revocation criteria in LICENSE file
- Clarified the conditions under which Core Contributor status may be revoked, ensuring that the definition of "contributed" is clearly stated in relation to communication and code contributions.
2025-12-13 19:25:11 -05:00
Cody Seibert
f2eb6c3745 Update LICENSE file to amend Core Contributor status management
- Added a provision to update the list of Core Contributors to reflect any changes in status, ensuring clarity in the management of contributor roles.
2025-12-13 19:24:10 -05:00
Cody Seibert
a08641a59b Update LICENSE file to clarify Core Contributor status management and commercial licensing terms
- Added provisions for the revocation and reinstatement of Core Contributor status, requiring unanimous votes for both actions.
- Introduced a new section outlining the process for discussing and issuing commercial licenses among Core Contributors.
- Renumbered sections for clarity and consistency throughout the document.
2025-12-13 19:17:59 -05:00
Kacper
8dc3bdde67 feat: enhance Electron app packaging and server preparation
- Added afterPack script in package.json to rebuild native modules for the server bundle.
- Improved icon handling in main.js to support cross-platform formats and verify icon existence.
- Updated startStaticServer function to return a promise for better error handling.
- Introduced a new script, rebuild-server-natives.js, to rebuild native modules based on the target architecture.
- Enhanced prepare-server.js to include native module rebuilding step for improved terminal functionality.
2025-12-14 01:08:35 +01:00
Kacper
223fff9ef9 feat: update application icon in package.json
- Changed the application icon from "public/logo_larger.png" to "public/icon.ico" for improved branding.
- Added new icon file "icon.ico" to the public directory.
2025-12-14 00:27:24 +01:00
Kacper
986f6c034f feat: improve project path handling in InterviewView component
- Updated the project path construction to use platform-specific path separators, enhancing compatibility across different operating systems.
- Implemented a check for the Electron API to determine the appropriate path separator based on the user's platform.
2025-12-14 00:20:58 +01:00
Kacper
10e647570b feat: improve FileBrowserDialog layout and styling
- Enhanced the layout of the FileBrowserDialog component by adding overflow handling and padding to improve visual consistency.
- Updated the DialogHeader and DialogFooter with additional styling for better separation and usability.
2025-12-14 00:15:04 +01:00
Kacper
6ac888c5ce feat: implement auto-collapse functionality for sidebar on small screens
- Added useEffect hook to automatically collapse the sidebar when the screen width is below 1024px.
- Included event listener for media query changes to handle sidebar state dynamically.
2025-12-14 00:03:48 +01:00
Kacper
1bda0259db feat: enhance workspace management and path handling
- Added functionality to set a default workspace directory in Electron, creating it if it doesn't exist.
- Improved project path construction in the New Project Modal to use platform-specific path separators.
- Enhanced error handling in the Templates route for parent directory access, including logging for better debugging.
2025-12-13 23:52:14 +01:00
Kacper
bea115d1e4 feat: update Electron configuration and static server implementation
- Upgraded Electron version to 39.2.7 and TypeScript to 5.9.3 in package-lock.json.
- Modified next.config.ts to set output to "export" for static site generation.
- Changed package.json to include the output directory for deployment.
- Enhanced main.js to implement a static file server for production builds, serving files from the "out" directory.
- Adjusted the loading mechanism to use the static server in production and the Next.js dev server in development.
2025-12-13 23:12:10 +01:00
SuperComboGamer
bc46a18372 feat: enhance Electron build process and server preparation
- Added new build scripts for Electron targeting Windows, macOS, and Linux.
- Updated the main build script to include server preparation steps.
- Introduced a new script to prepare the server for bundling with Electron, including cleaning previous builds and installing production dependencies.
- Modified the Electron main process to verify server file existence and improved error handling.
- Updated .gitignore to exclude the new server-bundle directory.
2025-12-13 16:40:09 -05:00
Web Dev Cody
e2c238f4f8 Merge pull request #64 from AutoMaker-Org/new-license
New license
2025-12-13 16:01:04 -05:00
Cody Seibert
574680fc11 fix: update server startup command to use node from PATH and improve error handling for tsx resolution 2025-12-13 15:55:21 -05:00
Kacper
673dcd1113 chore: add installation of Linux native bindings in CI workflows to address npm optional dependencies issue 2025-12-13 21:49:16 +01:00
Kacper
41ae35bcdb chore: restructure package-lock.json by moving dependencies under apps/app and removing unused entries 2025-12-13 21:42:02 +01:00
Cody Seibert
ae02b30aba fix: clarify internal use and governing law in licensing documentation
- Updated the LICENSE file to specify that modifications for internal use are allowed within the organization, whether commercial or non-profit.
- Revised the governing law clause to explicitly state that the agreement is governed by the laws of the State of Tennessee, USA.
- Adjusted the README to reflect these clarifications regarding internal use and modification rights.
2025-12-13 15:34:46 -05:00
Kacper
a2ad1d9420 chore: update .npmrc to comment out platform-specific bindings for faster installs 2025-12-13 21:33:17 +01:00
Kacper
1f4e801c58 chore: update electron version to 39.2.7 and add postinstall script in package.json; add unit tests for terminal service 2025-12-13 21:28:22 +01:00
Cody Seibert
ed65b096e3 feat: update licensing terms and disclaimer in documentation
- Revised the LICENSE file to establish a comprehensive Automaker License Agreement, replacing the previous dual licensing structure.
- Enhanced the README to reflect the new licensing terms, clarifying allowed uses and restrictions, including the "No Monetization of the Tool" rule.
- Updated the DISCLAIMER.md to include a more detailed warning about the risks associated with using AI-generated code and the limitations of liability.
- Removed the obsolete LICENSE-AGPL-3.0 and LICENSE-COMMERCIAL files to streamline licensing documentation.
2025-12-13 15:25:24 -05:00
Kacper
ff06821fcd chore: update .npmrc to include platform-specific bindings and add new applications to package-lock.json 2025-12-13 21:14:52 +01:00
Kacper
25edfecbd4 fix: correct error message 2025-12-13 20:52:49 +01:00
Kacper
aa83583ee9 refactor: remove ultrathink toast notifications and clean up component structure in BoardView and WelcomeStep 2025-12-13 20:44:34 +01:00
Kacper
7fe3dff655 feat: remoe codex references after merging of main branch 2025-12-13 20:38:05 +01:00
Kacper
7c6d9d3723 chore: update Next.js version to 16.0.10 in package.json and package-lock.json 2025-12-13 20:27:07 +01:00
Kacper
a1ff498585 chore: clean up package-lock.json by removing resolved and integrity fields for several dependencies 2025-12-13 20:22:58 +01:00
Kacper
37f45ee89b feat: remove codex support 2025-12-13 20:17:24 +01:00
Shirone
83fbf55781 Merge branch 'main' into feat/add-unit-testing 2025-12-13 19:53:00 +01:00
Shirone
1db24ab887 Merge pull request #63 from AutoMaker-Org/wiki
feat: add Wiki view and sidebar link
2025-12-13 19:45:49 +01:00
SuperComboGamer
e27e0b2343 feat: add Wiki view and sidebar link
- Introduced a new Wiki view component to the application.
- Updated the sidebar to include a button for navigating to the Wiki view.
- Modified the app store to support the new "wiki" view mode.
2025-12-13 13:23:13 -05:00
Ben
54311a887c Merge pull request #59 from AutoMaker-Org/feat/integrated-terminal
feat: add integrated terminal with tab system and theme support
2025-12-13 11:40:22 -06:00
trueheads
89216c01e5 fixes for windows, but maybe breaking linux 2025-12-13 11:34:59 -06:00
Cody Seibert
9e9cffde6b feat: implement dual licensing structure and update license documentation
- Introduced a dual licensing model for the project, allowing users to choose between the GNU Affero General Public License v3.0 (AGPL-3.0) and a Commercial License.
- Added detailed licensing information in the LICENSE and README files, clarifying the terms and conditions for both licenses.
- Created a new LICENSE-AGPL-3.0 file to include the full text of the AGPL-3.0 license.
- Established a LICENSE-COMMERCIAL file outlining the terms for commercial use and the process for obtaining a Business License.
- Updated the README to reflect the new licensing structure and emphasize the need for a Business License for commercial use.
2025-12-13 10:17:26 -05:00
Kacper
c24cd9721c fix: correct model check for Codex API key validation
- Updated the model check logic to only consider "gpt-" prefixed models, removing the previous check for unsupported models.
- Adjusted error message for authentication failures to provide clearer guidance on resolving API key issues.
2025-12-13 13:46:11 +01:00
Shirone
8c33b1c751 Merge branch 'main' into feat/add-unit-testing 2025-12-13 13:44:46 +01:00
Kacper
4d0d15d1d5 ci: add GitHub Actions workflow for test suite
- Added test.yml workflow to run on PRs and pushes to main/master
- Runs server tests with coverage on every PR
- Commented out Codecov integration (can be enabled when token is configured)
- Added test:server:coverage script to root package.json
- Adjusted coverage thresholds to match current coverage levels:
  - lines: 70% (current: 72.73%)
  - statements: 70% (current: 72.65%)
  - branches: 64% (current: 64.66%)
  - functions: 80% (current: 80.87%)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 13:43:15 +01:00
Kacper
4ba82e131a ci: add GitHub Actions workflow for test suite
- Added test.yml workflow to run on PRs and pushes to main/master
- Runs server tests with coverage on every PR
- Uploads coverage reports to Codecov
- Added test:server:coverage script to root package.json
- Coverage thresholds enforced: 80% lines/functions, 75% branches

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 13:35:19 +01:00
Kacper
23ff99d2e2 feat: add comprehensive integration tests for auto-mode-service
- Created git-test-repo helper for managing test git repositories
- Added 13 integration tests covering:
  - Worktree operations (create, error handling, non-worktree mode)
  - Feature execution (status updates, model selection, duplicate prevention)
  - Auto loop (start/stop, pending features, max concurrency, events)
  - Error handling (provider errors, continue after failures)
- Integration tests use real git operations with temporary repos
- All 416 tests passing with 72.65% overall coverage
- Service coverage improved: agent-service 58%, auto-mode-service 44%, feature-loader 66%

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 13:34:27 +01:00
Kacper
0473b35db3 refactor: restrict model checks to gpt-* for OpenAI/Codex models
- Updated model resolution logic to only check for gpt-* models, removing references to unsupported o1/o3 models in both model-resolver and provider-factory files.
- Enhanced comments for clarity regarding model support in Codex CLI.
2025-12-13 13:12:04 +01:00
Cody Seibert
f71533ab17 feat: improve URL accessibility checks and download handling
- Enhanced the URL accessibility check function to handle multiple redirect types and provide detailed feedback on accessibility status, including content type validation.
- Updated the download function to follow redirects correctly and ensure proper error handling, improving the reliability of downloading source archives from GitHub.
- Adjusted the main function to utilize the final URLs after redirects for downloading, ensuring accurate resource retrieval.
2025-12-13 11:53:26 +01:00
Cody Seibert
8709b5d34b feat: implement URL accessibility check with exponential backoff
- Added a new function to check the accessibility of URLs with retries and exponential backoff, improving the reliability of downloading source archives from GitHub.
- Updated the main function to wait for the source archives to be accessible before proceeding with the download, enhancing error handling and user feedback.
2025-12-13 11:53:26 +01:00
Cody Seibert
88a059ca52 chore: specify shell for version extraction in release workflow
- Updated the release workflow to explicitly set the shell to bash for the version extraction steps, ensuring consistent execution across environments.
2025-12-13 11:53:26 +01:00
Cody Seibert
f3ffb22487 testing releases 2025-12-13 11:53:26 +01:00
Cody Seibert
b915a43eb0 fix: update release URL in marketing pages
- Changed the default release URL from 'https://releases.automaker.dev/releases.json' to 'https://releases.automaker.app/releases.json' in both index.html and releases.html files to ensure correct resource loading.
2025-12-13 11:53:26 +01:00
Cody Seibert
8b2b7662ee feat: enhance board background settings and introduce animated borders
- Added default background settings to streamline background management across components.
- Implemented animated border styles for in-progress cards to improve visual feedback.
- Refactored BoardBackgroundModal and BoardView components to utilize the new default settings, ensuring consistent background behavior.
- Updated KanbanCard to support animated borders, enhancing the user experience during task progress.
- Improved Sidebar component by optimizing the fetching of running agents count with a more efficient use of hooks.
2025-12-13 11:53:26 +01:00
Cody Seibert
e6d3e8e5a5 chore: clean up .gitignore by removing redundant node_modules entry
- Removed duplicate entry for node_modules from the .gitignore file to streamline ignored files and improve clarity.
2025-12-13 11:52:47 +01:00
Cody Seibert
26e01c930f feat: add project management actions to WelcomeView
- Introduced `addProject` and `setCurrentProject` actions to the WelcomeView component for enhanced project management capabilities.
- Updated the component's state management to support these new actions, improving user experience in project handling.
2025-12-13 11:52:47 +01:00
Cody Seibert
8621a3095d feat: enhance background image handling with cache-busting
- Added a cache-busting query parameter to the background image URL to ensure the browser reloads the image when updated.
- Updated the AppState to include an optional imageVersion property for managing image updates.
- Modified the BoardBackgroundModal and BoardView components to utilize the new imageVersion for dynamic image loading.
2025-12-13 11:52:47 +01:00
Cody Seibert
6e7352e67e feat: implement upsert project functionality in sidebar and welcome view
- Refactored project handling in Sidebar and WelcomeView components to use a new `upsertAndSetCurrentProject` action for creating or updating projects.
- Enhanced theme preservation logic during project creation and updates by integrating theme management directly into the store action.
- Cleaned up redundant code related to project existence checks and state updates, improving maintainability and readability.
2025-12-13 11:52:47 +01:00
Cody Seibert
7e3f77cb38 feat: add video demo section to marketing page
- Introduced a new video demo section to showcase features with an embedded video player.
- Styled the video container for responsive design and improved aesthetics.
- Added media queries for better display on smaller screens.
2025-12-13 11:52:47 +01:00
Cody Seibert
75b73c55e0 feat: introduce marketing mode and update sidebar display
- Added a new configuration flag `IS_MARKETING` to toggle marketing mode.
- Updated the sidebar component to conditionally display the marketing URL when in marketing mode.
- Refactored event type naming for consistency in the sidebar logic.
- Cleaned up formatting in the HttpApiClient for improved readability.
2025-12-13 11:52:47 +01:00
Cody Seibert
ebd928e3b6 feat: add red theme and board background modal
- Introduced a new red theme with custom color variables for a bold aesthetic.
- Updated the theme management to include the new red theme option.
- Added a BoardBackgroundModal component for managing board background settings, including image uploads and opacity controls.
- Enhanced KanbanCard and KanbanColumn components to support new background settings such as opacity and border visibility.
- Updated API client to handle saving and deleting board backgrounds.
- Refactored theme application logic to accommodate the new preview theme functionality.
2025-12-13 11:52:47 +01:00
Cody Seibert
80cbabeeb0 various fixes 2025-12-13 11:48:53 +01:00
Cody Seibert
05910905ee adding new project from template 2025-12-13 11:48:53 +01:00
SuperComboGamer
66fe3392ad commit 2025-12-13 02:04:14 -05:00
SuperComboGamer
3a553c892d docs: fix terminal documentation
- Terminal is NOT password protected by default
- Add TERMINAL_PASSWORD to .env to enable protection
- Add TERMINAL_ENABLED=false to disable terminal completely

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 02:04:05 -05:00
SuperComboGamer
ca506a208e docs: add terminal documentation
Explains terminal features including:
- Password protection and how to disable it
- Keyboard shortcuts (Alt+D, Alt+S, Alt+W)
- Theming, font size, scrollback
- Architecture overview
- Troubleshooting tips

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 02:00:52 -05:00
SuperComboGamer
cbca6fa6e4 fix: change split-down shortcut to Alt+S to avoid system conflict
- Change split-down from Alt+Shift+D to Alt+S (Alt+Shift is Windows
  keyboard layout switch shortcut)
- Use event.code for keyboard-layout-independent key detection
- Add red theme to dark theme scrollbar selectors
- Add red-themed scrollbar styling with dark red colors
- Tone down white/bright colors in red terminal theme

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:54:04 -05:00
SuperComboGamer
951010b64d fix: add missing red terminal theme and fix split panel type
- Add red terminal theme with dark red-accented color scheme
- Add size property to split type in TerminalPanelContent to support
  nested splits with size tracking

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:47:49 -05:00
SuperComboGamer
08221c6660 fix: move terminal creation debounce to view level
The per-panel debounce didn't work because each new terminal has
its own fresh ref. Move debounce to createTerminal function with:
- 500ms cooldown between creations
- isCreating flag to prevent concurrent requests

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:44:24 -05:00
SuperComboGamer
ffd8752cde feat: add debounce to terminal shortcuts and show in keyboard layout
- Add 300ms cooldown to prevent rapid terminal creation when holding keys
- Merge DEFAULT_KEYBOARD_SHORTCUTS with user shortcuts so terminal
  shortcuts (Alt+D, Alt+Shift+D, Alt+W) show in keyboard layout
- Fix keyboard map to handle undefined shortcuts from old persisted state

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:42:40 -05:00
SuperComboGamer
deae01712a fix: intercept terminal shortcuts at xterm level
When the terminal is focused, xterm captures keyboard events before
they reach the window. Use attachCustomKeyEventHandler to intercept
Alt+D, Alt+Shift+D, and Alt+W directly at the xterm level.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:39:05 -05:00
SuperComboGamer
14d1562903 fix: handle undefined shortcuts in parseShortcut and formatShortcut
Add guards to handle undefined/null shortcuts for users with
old persisted state missing the new terminal shortcuts.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:35:18 -05:00
SuperComboGamer
8c100230ab fix: add safety checks for undefined shortcuts in keyboard map
Handle cases where users have old persisted state that doesn't
include the new terminal shortcuts.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:34:27 -05:00
SuperComboGamer
8eb374d77c fix: use Alt-based shortcuts to avoid browser conflicts
- Split right: Alt+D
- Split down: Alt+Shift+D
- Close terminal: Alt+W

Alt modifier avoids conflicts with both terminal signals and browser shortcuts.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:32:36 -05:00
SuperComboGamer
998ad354d2 fix: change terminal shortcuts to avoid conflicts with shell signals
- Split right: Cmd+Shift+D / Ctrl+Shift+D (was Cmd+D which conflicts with EOF)
- Split down: Cmd+Shift+E / Ctrl+Shift+E
- Close: Cmd+Shift+W / Ctrl+Shift+W (was Cmd+W which conflicts with delete word)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:31:02 -05:00
SuperComboGamer
a2bd1b593b fix: handle undefined shortcuts for users with persisted state
Users with existing persisted state won't have the new terminal
shortcuts, so guard against undefined values.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:29:39 -05:00
SuperComboGamer
2ebb650609 feat: add terminal keyboard shortcuts with cross-platform support
- Add splitTerminalRight, splitTerminalDown, closeTerminal to KeyboardShortcuts
- Wire up shortcuts in terminal view (Cmd+D, Cmd+Shift+D, Cmd+W on Mac)
- Auto-detect platform and use Ctrl instead of Cmd on Linux/Windows

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:28:58 -05:00
SuperComboGamer
11ddcfaf90 fix: throttle terminal output to prevent system lockup under heavy load
- Batch terminal output at ~60fps max to prevent overwhelming WebSocket
- Reduce scrollback buffer from 100KB to 50KB per terminal
- Clean up flush timeouts on session kill/cleanup
- Should fix lockups when running npm run dev with high output

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:21:52 -05:00
SuperComboGamer
be4a0b292c fix: split terminal inside current panel instead of at root
When clicking split on a terminal, the new terminal is now added
as a sibling of that specific terminal rather than at the root
of the layout tree.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:07:33 -05:00
SuperComboGamer
18494547bc fix: address code review feedback
- Display actual shell name instead of hardcoded "bash"
- Fix type assertion by making findFirstTerminal accept null

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:05:12 -05:00
Cody Seibert
26074f9390 feat: improve URL accessibility checks and download handling
- Enhanced the URL accessibility check function to handle multiple redirect types and provide detailed feedback on accessibility status, including content type validation.
- Updated the download function to follow redirects correctly and ensure proper error handling, improving the reliability of downloading source archives from GitHub.
- Adjusted the main function to utilize the final URLs after redirects for downloading, ensuring accurate resource retrieval.
2025-12-13 01:03:26 -05:00
SuperComboGamer
272905b884 fix: add terminal keyboard shortcut to KeyboardShortcuts interface
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:02:38 -05:00
Cody Seibert
0ad2de90ee feat: implement URL accessibility check with exponential backoff
- Added a new function to check the accessibility of URLs with retries and exponential backoff, improving the reliability of downloading source archives from GitHub.
- Updated the main function to wait for the source archives to be accessible before proceeding with the download, enhancing error handling and user feedback.
2025-12-13 01:01:35 -05:00
SuperComboGamer
21cbdba530 fix: add missing Terminal icon import in sidebar
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:00:01 -05:00
SuperComboGamer
04ccd6f81c feat: add integrated terminal with tab system and theme support
- Add terminal view with draggable split panels and multi-tab support
- Implement terminal WebSocket server with password protection
- Add per-terminal font size that persists when moving between tabs
- Support all 12 app themes with matching terminal colors
- Add keyboard shortcut (Ctrl+`) to toggle terminal view
- Include scrollback buffer for session history on reconnect

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 00:57:28 -05:00
Cody Seibert
af04e69dc7 chore: specify shell for version extraction in release workflow
- Updated the release workflow to explicitly set the shell to bash for the version extraction steps, ensuring consistent execution across environments.
2025-12-13 00:51:23 -05:00
Cody Seibert
935316cb51 testing releases 2025-12-13 00:46:24 -05:00
Web Dev Cody
e608f46a49 Merge pull request #52 from AutoMaker-Org/new-project-from-template
feat: Add new project from template feature and UI enhancements
2025-12-13 00:32:56 -05:00
Cody Seibert
8de4056417 fix: update release URL in marketing pages
- Changed the default release URL from 'https://releases.automaker.dev/releases.json' to 'https://releases.automaker.app/releases.json' in both index.html and releases.html files to ensure correct resource loading.
2025-12-13 00:29:03 -05:00
Cody Seibert
9196a1afb4 feat: enhance board background settings and introduce animated borders
- Added default background settings to streamline background management across components.
- Implemented animated border styles for in-progress cards to improve visual feedback.
- Refactored BoardBackgroundModal and BoardView components to utilize the new default settings, ensuring consistent background behavior.
- Updated KanbanCard to support animated borders, enhancing the user experience during task progress.
- Improved Sidebar component by optimizing the fetching of running agents count with a more efficient use of hooks.
2025-12-13 00:25:16 -05:00
Cody Seibert
eaef95c4a3 chore: clean up .gitignore by removing redundant node_modules entry
- Removed duplicate entry for node_modules from the .gitignore file to streamline ignored files and improve clarity.
2025-12-12 23:56:33 -05:00
Cody Seibert
3dd10aa8c7 feat: add project management actions to WelcomeView
- Introduced `addProject` and `setCurrentProject` actions to the WelcomeView component for enhanced project management capabilities.
- Updated the component's state management to support these new actions, improving user experience in project handling.
2025-12-12 23:45:36 -05:00
Cody Seibert
104f478f89 feat: enhance background image handling with cache-busting
- Added a cache-busting query parameter to the background image URL to ensure the browser reloads the image when updated.
- Updated the AppState to include an optional imageVersion property for managing image updates.
- Modified the BoardBackgroundModal and BoardView components to utilize the new imageVersion for dynamic image loading.
2025-12-12 23:09:51 -05:00
Cody Seibert
b32af0c86b feat: implement upsert project functionality in sidebar and welcome view
- Refactored project handling in Sidebar and WelcomeView components to use a new `upsertAndSetCurrentProject` action for creating or updating projects.
- Enhanced theme preservation logic during project creation and updates by integrating theme management directly into the store action.
- Cleaned up redundant code related to project existence checks and state updates, improving maintainability and readability.
2025-12-12 23:06:22 -05:00
Cody Seibert
c991d5f2f7 feat: add video demo section to marketing page
- Introduced a new video demo section to showcase features with an embedded video player.
- Styled the video container for responsive design and improved aesthetics.
- Added media queries for better display on smaller screens.
2025-12-12 22:51:39 -05:00
Cody Seibert
b3a4fd2be1 feat: introduce marketing mode and update sidebar display
- Added a new configuration flag `IS_MARKETING` to toggle marketing mode.
- Updated the sidebar component to conditionally display the marketing URL when in marketing mode.
- Refactored event type naming for consistency in the sidebar logic.
- Cleaned up formatting in the HttpApiClient for improved readability.
2025-12-12 22:42:43 -05:00
Shirone
25f5f7d6b2 Update apps/app/src/store/app-store.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-13 04:42:08 +01:00
Kacper
2f2eab6e02 refactor: update auto-mode-service to use dynamic model resolution
- Replaced hardcoded model string with dynamic resolution for the analysis model, allowing for future flexibility.
- Enhanced error handling to provide specific authentication failure messages based on the model type, improving user feedback.

This change streamlines the model selection process and improves error clarity for users.
2025-12-13 04:37:53 +01:00
Kacper
6726050969 Merge main into feat/codex-new-model - resolved conflict in auto-mode-service.ts 2025-12-13 04:35:32 +01:00
Kacper
d08eba2331 fix: resolve TypeScript compilation errors
Fixed 4 TypeScript errors:
- fs.ts: Removed duplicate 'os' import (lines 8 and 10)
- spec-regeneration.ts: Removed dead code checking for impossible error type (2 occurrences)

The error type checks were comparing msg.type to "error", but the SDK type union
does not include "error" as a valid message type. Errors are properly handled
in the catch blocks, so these checks were unreachable dead code.

All TypeScript compilation now passes cleanly.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 04:30:13 +01:00
Kacper
7cbdb3db73 refactor: eliminate code duplication with shared utilities
Created 5 new utility modules in apps/server/src/lib/ to eliminate ~320 lines of duplicated code:
- image-handler.ts: Centralized image processing (MIME types, base64, content blocks)
- prompt-builder.ts: Standardized prompt building with image attachments
- model-resolver.ts: Model alias resolution and provider routing
- conversation-utils.ts: Conversation history processing for providers
- error-handler.ts: Error classification and user-friendly messages

Updated services and providers to use shared utilities:
- agent-service.ts: -51 lines (removed duplicate image handling, model logic)
- auto-mode-service.ts: -75 lines (removed MODEL_MAP, duplicate utilities)
- claude-provider.ts: -10 lines (uses conversation-utils)
- codex-provider.ts: -5 lines (uses conversation-utils)

Added comprehensive documentation:
- docs/server/utilities.md: Complete reference for all 9 lib utilities
- docs/server/providers.md: Provider architecture guide with examples

Benefits:
- Single source of truth for critical business logic
- Improved maintainability and testability
- Consistent behavior across services and providers
- Better documentation for future development

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 04:26:58 +01:00
Cody Seibert
28328d7d1e feat: add red theme and board background modal
- Introduced a new red theme with custom color variables for a bold aesthetic.
- Updated the theme management to include the new red theme option.
- Added a BoardBackgroundModal component for managing board background settings, including image uploads and opacity controls.
- Enhanced KanbanCard and KanbanColumn components to support new background settings such as opacity and border visibility.
- Updated API client to handle saving and deleting board backgrounds.
- Refactored theme application logic to accommodate the new preview theme functionality.
2025-12-12 22:05:16 -05:00
Kacper
0519aba820 feat: add missing Codex models and restore subprocess logs
- Added gpt-5.1-codex-mini model (lightweight, faster)
- Added gpt-5.1 model (general-purpose)
- Restored subprocess spawn/exit logs for debugging
- Now all 5 Codex models are available:
  * GPT-5.2
  * GPT-5.1 Codex Max
  * GPT-5.1 Codex
  * GPT-5.1 Codex Mini
  * GPT-5.1

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 03:50:31 +01:00
Kacper
a65b16cbae feat: implement modular provider architecture with Codex CLI support
Implements a flexible provider pattern that supports both Claude Agent SDK
and OpenAI Codex CLI, enabling future expansion to other AI providers
(Cursor, OpenCode, etc.) with minimal changes.

## Architecture Changes

### New Provider System
- Created provider abstraction layer with BaseProvider interface
- Model-based routing: model prefix determines provider
  - `gpt-*`, `o*` → CodexProvider (subprocess CLI)
  - `claude-*`, `opus/sonnet/haiku` → ClaudeProvider (SDK)
- Providers implement common ExecuteOptions interface

### New Files Created
- `providers/types.ts` - Shared interfaces (ExecuteOptions, ProviderMessage, etc.)
- `providers/base-provider.ts` - Abstract base class
- `providers/claude-provider.ts` - Claude Agent SDK wrapper
- `providers/codex-provider.ts` - Codex CLI subprocess executor
- `providers/codex-cli-detector.ts` - Installation & auth detection
- `providers/codex-config-manager.ts` - TOML config management
- `providers/provider-factory.ts` - Model-based provider routing
- `lib/subprocess-manager.ts` - Reusable subprocess utilities

## Features Implemented

### Codex CLI Integration
- Spawns Codex CLI as subprocess with JSONL output
- Converts Codex events to Claude SDK-compatible format
- Supports both `codex login` and OPENAI_API_KEY auth methods
- Handles: reasoning, messages, commands, todos, file changes
- Extracts text from content blocks for non-vision CLI

### Conversation History
- Added conversationHistory support to ExecuteOptions
- ClaudeProvider: yields previous messages to SDK
- CodexProvider: prepends history as text context
- Follow-up prompts maintain full conversation context

### Image Upload Support
- Images embedded as base64 for vision models
- Image paths appended to prompt text for Read tool access
- Auto-mode: copies images to feature folder
- Follow-up: combines original + new images
- Updates feature.json with image metadata

### Session Model Persistence
- Added `model` field to Session and SessionMetadata
- Sessions remember model preference across interactions
- API endpoints accept model parameter
- Auto-mode respects feature's model setting

## Modified Files

### Services
- `agent-service.ts`:
  - Added conversation history building
  - Uses ProviderFactory instead of direct SDK calls
  - Appends image paths to prompts
  - Added model parameter and persistence

- `auto-mode-service.ts`:
  - Removed OpenAI model block restriction
  - Uses ProviderFactory for all models
  - Added image support in buildFeaturePrompt
  - Follow-up: loads context, copies images, updates feature.json
  - Returns to waiting_approval after follow-up

### Routes
- `agent.ts`: Added model parameter to /send endpoint
- `sessions.ts`: Added model field to create/update
- `models.ts`: Added Codex models (gpt-5.2, gpt-5.1-codex*)

### Configuration
- `.env.example`: Added OPENAI_API_KEY and CODEX_CLI_PATH
- `.gitignore`: Added provider-specific ignores

## Bug Fixes
- Fixed image path resolution (relative → absolute)
- Fixed Codex empty prompt when images attached
- Fixed follow-up status management (in_progress → waiting_approval)
- Fixed follow-up images not appearing in prompt text
- Removed OpenAI model restrictions in auto-mode

## Testing Notes
- Codex CLI authentication verified with both methods
- Image uploads work for both Claude (vision) and Codex (Read tool)
- Follow-up prompts maintain full context
- Conversation history persists across turns
- Model switching works per-session

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 03:45:41 +01:00
Cody Seibert
346c38d6da Merge branch 'main' into new-project-from-template 2025-12-12 20:51:23 -05:00
Cody Seibert
ca4809ca06 various fixes 2025-12-12 20:51:01 -05:00
Web Dev Cody
9afcc5fae6 Merge pull request #53 from AutoMaker-Org/cleanup/remove-dead-electron-code
chore: remove ~13,000 lines of dead Electron code
2025-12-12 19:50:20 -05:00
Cody Seibert
383dc66952 Merge branch 'cleanup/remove-dead-electron-code' of github.com:webdevcody/automaker into cleanup/remove-dead-electron-code 2025-12-12 19:42:05 -05:00
Cody Seibert
3c466f0150 chore: update .gitignore to include data directory
- Added 'data' directory to .gitignore to prevent tracking of generated files.
- Ensured that sensitive environment files remain untracked by keeping '.env' entry.
2025-12-12 19:42:03 -05:00
Cody Seibert
fe9b26c49e feat: add delete session functionality with confirmation dialog
- Introduced a new DeleteSessionDialog component for confirming session deletions.
- Integrated the delete session dialog into the SessionManager component, allowing users to delete sessions with a confirmation prompt.
- Updated the UI to handle session deletion more intuitively, enhancing user experience.
- Refactored existing delete confirmation logic to utilize the new DeleteConfirmDialog component for consistency across the application.
2025-12-12 19:41:52 -05:00
Kacper
55603cb5c7 feat: add GPT-5.2 model support and refresh profiles functionality
- Introduced the GPT-5.2 model with advanced coding capabilities across various components.
- Added a new button in ProfilesView to refresh default profiles, enhancing user experience.
- Updated CodexSetupStep to clarify authentication requirements and added commands for verifying login status.
- Enhanced utility functions to recognize the new GPT-5.2 model in the application.
2025-12-13 01:36:15 +01:00
trueheads
97d05148d4 adding shell script for ease of launching project, until cody gets off his butt and switches the npm run stuff ;) 2025-12-12 18:26:51 -06:00
SuperComboGamer
437063630c fix: add setWindowOpenHandler for external links
Restores the handler that opens target="_blank" links in the default
browser instead of trying to create a new Electron window.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 19:07:26 -05:00
SuperComboGamer
0dc2e72263 chore: remove ~13,000 lines of dead Electron code
All business logic has been migrated to the REST API server. This removes
obsolete Electron IPC handlers and services that are no longer used:

- Deleted electron/services/ directory (18 files)
- Deleted electron/agent-service.js
- Deleted electron/auto-mode-service.js
- Renamed main-simplified.js → main.js
- Renamed preload-simplified.js → preload.js
- Removed unused dependencies from package.json

The Electron layer now only handles native OS features (10 IPC handlers):
- File dialogs, shell operations, and app info

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 18:57:41 -05:00
Cody Seibert
5544031164 Merge branch 'main' into new-project-from-template 2025-12-12 18:31:09 -05:00
Kacper
9cf5fff0ad feat: add markdown preview functionality to ContextView component
- Implemented a toggle button to switch between edit and preview modes for markdown files.
- Added a new state to manage preview mode and a utility function to identify markdown file types.
- Enhanced the rendering logic to display markdown content in a preview card when in preview mode.
2025-12-13 00:30:04 +01:00
Web Dev Cody
77918018fa Merge pull request #42 from AutoMaker-Org/removing-electron-features-build-api
chore: update project management and API integration
2025-12-12 18:09:56 -05:00
Alec Koifman
1a4e9ccfcb fix themes 2025-12-12 17:52:06 -05:00
Alec Koifman
5873e888a9 Merge branch 'fs/ui' into removing-electron-features-build-api
Resolved conflict in http-api-client.ts by adopting the server-side
file browser dialog approach from fs/ui branch.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 17:38:05 -05:00
Alec Koifman
28bbc3e0e1 chore: update .gitignore and remove obsolete automaker files
- Added .automaker/ to .gitignore to prevent tracking of the entire directory.
- Deleted outdated files including app_spec.txt, categories.json, memory.md, clean-code.md, and gemini.md from the .automaker context.
- Enhanced the mcp-server-factory.js and spec-regeneration-service.js to enforce status management for new features, ensuring they default to "backlog" and clarifying status handling in comments.
- Introduced a new file browsing endpoint in fs.ts to improve directory navigation while maintaining security constraints.
2025-12-12 17:34:16 -05:00
Cody Seibert
be4aadb632 adding new project from template 2025-12-12 17:14:31 -05:00
trueheads
62082fbaf5 massive overhaul of auth system logic, and subtle fixes to UI 2025-12-12 15:59:21 -06:00
Cody Seibert
a4c5567768 feat: enhance CoursePromoBadge component with sidebar state management
- Updated CoursePromoBadge to accept a sidebarOpen prop, allowing it to render differently based on the sidebar's state.
- Implemented tooltip functionality for the collapsed state, improving user interaction and accessibility.
- Adjusted styles and structure to support both expanded and collapsed views of the badge.
2025-12-12 15:53:11 -05:00
Cody Seibert
6c7adb140d fix: correct build command for Electron in package.json
- Updated the build command for Electron to ensure it correctly references the workspace for the app.
- This change improves the build process and resolves potential issues with workspace management.
2025-12-12 15:25:06 -05:00
Cody Seibert
18182bbc94 feat: add errorType to AutoModeActivity interface for enhanced error handling
- Introduced errorType property to the AutoModeActivity interface to categorize errors as "authentication" or "execution".
- This addition improves the granularity of error reporting and handling within the application.
2025-12-12 15:20:35 -05:00
Kacper
0bb774375e feat: implement file browser context and dialog for directory selection
- Introduced a new FileBrowserProvider to manage file browsing state and functionality.
- Added FileBrowserDialog component for user interface to navigate and select directories.
- Updated Home component to utilize the file browser context and provide global access.
- Enhanced HttpApiClient to use the new file browser for directory and file selection.
- Implemented server-side route for browsing directories, including drive detection on Windows.
2025-12-12 19:20:32 +01:00
Kacper
4924cf1453 feat: implement web-based file picker for improved directory and file selection
- Replaced prompt-based directory input with a web-based directory picker in HttpApiClient.
- Added server endpoint for resolving directory paths based on directory name and file structure.
- Enhanced error handling and logging for directory and file selection processes.
- Updated file picker to validate file existence with the server for better user feedback.
2025-12-12 19:15:31 +01:00
Alec Koifman
c079b3ef88 fix packages 2025-12-12 11:39:00 -05:00
Alec Koifman
fade47afdc fix agent runner archive on web 2025-12-12 11:15:44 -05:00
Cody Seibert
813ede2dde chore: update .gitignore and add R2 upload script for artifact management
- Added .automaker/images/ to .gitignore to prevent tracking of generated images.
- Deleted obsolete agent-output.md and feature.json files related to removed "Agent Tools" feature.
- Introduced a new script for uploading build artifacts to R2 and updating releases.json.
- Enhanced GitHub Actions workflow to include artifact uploads for different platforms.
2025-12-12 10:39:12 -05:00
Cody Seibert
b950f13e11 feat: remove "Agent Tools" from side navigation and related components
- Deleted the "Agent Tools" navigation item from the sidebar.
- Updated keyboard shortcuts and app store types to reflect the removal.
- Cleaned up imports and references in relevant files.
- Retained the `agent-tools-view.tsx` component for potential future use.
2025-12-12 02:50:02 -05:00
Cody Seibert
ba24753630 feat: move "Report Bug / Feature Request" button to header for improved accessibility
- Relocated the button from the bottom sidebar to the header next to the AutoMaker logo.
- Updated the button to be a compact icon-only version with a tooltip on hover.
- Adjusted the header layout to accommodate the new button placement.
- Removed the old button from the sidebar to streamline the UI.
2025-12-12 02:43:26 -05:00
Cody Seibert
8e65f0b338 refactor: streamline Electron API integration and enhance UI components
- Removed unused Electron API methods and simplified the main process.
- Introduced a new workspace picker modal for improved project selection.
- Enhanced error handling for authentication issues across various components.
- Updated UI styles for dark mode support and added new CSS variables.
- Refactored session management to utilize a centralized API access method.
- Added server routes for workspace management, including directory listing and configuration checks.
2025-12-12 02:14:52 -05:00
SuperComboGamer
4b9bd2641f chore: update project management and API integration
- Added new scripts for server development and full application startup in package.json.
- Enhanced project management by checking for existing projects to avoid duplicates.
- Improved API integration with better error handling and connection checks in the Electron API.
- Updated UI components to reflect changes in project and session management.
- Refactored authentication status display to include more detailed information on methods used.
2025-12-12 00:23:43 -05:00
Web Dev Cody
9978de0377 Merge pull request #40 from AutoMaker-Org/docs/update-readme-with-run-instructions
docs: add comprehensive run instructions and improve README formatting
2025-12-11 23:28:25 -05:00
Cody Seibert
0c776b173a docs: add comprehensive run instructions and improve README formatting 2025-12-11 23:20:40 -05:00
Web Dev Cody
02a1af3314 Merge pull request #37 from AutoMaker-Org/refactor/monorepo-restructure
Refactor/monorepo restructure
2025-12-11 23:11:37 -05:00
Cody Seibert
2c4a977b4a chore: update package.json to enhance build configuration
- Added `artifactName` template for output files.
- Set `executableName` for the application to improve clarity in the build process.
2025-12-11 22:59:29 -05:00
Cody Seibert
28cc1400e9 refactor: simplify font imports in layout.tsx
- Removed unnecessary variable assignments for GeistSans and GeistMono.
- Updated imports to directly use the Geist font components.
2025-12-11 22:52:35 -05:00
Web Dev Cody
193627e166 Merge pull request #39 from AutoMaker-Org/copilot/fix-build-issues
Fix build issues by switching from Google Fonts to local fonts
2025-12-11 22:46:55 -05:00
Cody Seibert
72560cbd36 chore: update package-lock.json to remove unnecessary dependencies and add optional flags
- Removed `@tailwindcss/oxide-linux-x64-gnu` and `lightningcss-linux-x64-gnu` dependencies.
- Added `dev` and `optional` flags for Linux x64 binaries to enhance compatibility.
2025-12-11 22:44:11 -05:00
Cody Seibert
8b62bf9817 trying stuff 2025-12-11 22:43:57 -05:00
GTheMachine
95913714cc Update apps/app/src/app/layout.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 19:19:08 -08:00
copilot-swe-agent[bot]
9f0c648391 Fix build issues by switching from Google Fonts to local fonts
- Added `geist` npm package for local font files
- Updated layout.tsx to import fonts from the geist package
- Build now succeeds without network access to Google Fonts

Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com>
2025-12-12 03:06:02 +00:00
copilot-swe-agent[bot]
0b3ffb642a Initial plan 2025-12-12 02:58:08 +00:00
SuperComboGamer
7282113a0f fix: add missing native binaries for WSL development
The monorepo restructure caused npm to skip installing platform-specific
optional dependencies for tailwindcss and lightningcss. This adds explicit
dependencies for the Linux x64 binaries and removes the duplicate lockfile.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 21:47:45 -05:00
Cody Seibert
ada2703d3e feat: add debug option for Electron development
- Introduced a new command `dev:electron:debug` in both package.json files to launch Electron with DevTools open.
- Updated main.js to conditionally open DevTools based on the `OPEN_DEVTOOLS` environment variable.
- Refactored some IPC handler code for better readability and consistency.
2025-12-11 21:21:59 -05:00
Cody Seibert
2cc9e47747 chore: update GitHub workflows for consistency and clarity
- Standardized quotes in YAML files for better readability.
- Adjusted cache-dependency-path to point to the correct location.
- Removed unnecessary working-directory specifications to simplify the workflow.
2025-12-11 21:14:17 -05:00
Cody Seibert
e6857c6feb feat: disable automated testing by default and rename checkbox
- Change default skipTests value from false to true (tests disabled by default)
- Rename checkbox label from 'Skip automated testing' to 'Enable automated testing'
- Invert checkbox logic so checked means tests enabled
- Update help text to clarify the new behavior

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 21:13:45 -05:00
Cody Seibert
b5d31db57c chore: remove email-course-reminder.mjml 2025-12-11 20:43:27 -05:00
Cody Seibert
1bb20e5070 refactor: restructure project to monorepo with apps directory 2025-12-11 20:34:49 -05:00
Web Dev Cody
7cb5a6a4df Merge pull request #35 from AutoMaker-Org/refactor/remove-duplicated-kanban-display-view
refactor(settings): remove kanban display section from settings view
2025-12-11 20:08:28 -05:00
Web Dev Cody
e1e84226c9 Merge pull request #31 from AutoMaker-Org/feat/enchance-welcome-page-setup
feat: enchance welcome page setup
2025-12-11 20:06:29 -05:00
Web Dev Cody
7ff97a42cb Merge pull request #26 from AutoMaker-Org/bugfix/appspec-complete-overhaul
Complete overhaul for app spec system. Created logic to auto generate…
2025-12-11 17:28:16 -05:00
Kacper
502ac9e100 refactor(settings): remove kanban display section from settings view
Moved kanban display configuration from settings to board view to improve UX.
Removed KanbanDisplaySection component, related imports, and navigation items.
2025-12-11 23:21:38 +01:00
Shirone
e6e2fa70e2 Merge pull request #34 from AutoMaker-Org/style/enchance-sidebar-style
style: enhance sidebar layout for better responsiveness
2025-12-11 22:43:59 +01:00
Kacper
067f051ff7 style: enhance sidebar layout for better responsiveness
- Adjusted padding and flex properties in the sidebar component to improve layout when the sidebar is open or closed.
- Ensured consistent alignment and spacing for a more user-friendly interface.
2025-12-11 22:05:36 +01:00
Kacper
13de3131b7 fix: infinite loop when checking cli status 2025-12-11 21:20:34 +01:00
Kacper
c8a9ae64b6 fix: address Gemini code review suggestions
Fixed two critical code quality issues identified in code review:

1. Auth Method Selector - Tailwind CSS Dynamic Classes:
   - Replaced dynamic class name concatenation with static class mapping
   - Tailwind JIT compiler requires complete class names at build time
   - Added getBadgeClasses() helper function with predefined color mappings
   - Prevents broken styling from unparseable dynamic classes

2. Claude CLI Detector - Async Terminal Detection:
   - Moved execSync loop to async/await pattern to prevent UI blocking
   - Refactored Linux terminal detection to run before Promise constructor
   - Prevents main process freezing during terminal emulator detection
   - Improves responsiveness on Linux systems

Both fixes improve code reliability and user experience.
2025-12-11 21:07:15 +01:00
Shirone
7383babd68 chore: update app/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 20:59:39 +01:00
Kacper
9dc5d64a26 refactor: modularize setup view into reusable components and hooks
Refactored setup-view.tsx (1,740 → 148 lines, -91.5%) by extracting:
- 8 reusable UI components (status badges, terminal output, etc.)
- 4 custom hooks (CLI status, installation, OAuth, token save)
- 4 step components (welcome, complete, Claude setup, Codex setup)
- 1 dialog component (OAuth token modal)

All business logic separated from UI, zero code duplication, fully type-safe.
2025-12-11 20:41:44 +01:00
Kacper
b39a88ba15 feat: enhance Claude CLI detector for macOS support
- Updated ClaudeCliDetector to include support for macOS (darwin) in the preference for using pty, improving compatibility across platforms.
2025-12-11 19:16:22 +01:00
Kacper
0510ab31e3 feat: implement OAuth token setup for Claude CLI
- Added a new SetupTokenModal component for handling OAuth token authentication.
- Integrated in-app terminal support using node-pty for seamless user experience.
- Updated ClaudeCliDetector to extract tokens from command output and handle authentication flow.
- Enhanced README with Windows-specific notes and authentication instructions.
- Updated package.json and package-lock.json to include necessary dependencies for the new functionality.
2025-12-11 19:08:08 +01:00
GTheMachine
e6a6b4cd78 Merge pull request #27 from AutoMaker-Org/copilot/sub-pr-26
Fix TypeScript build errors in SpecRegenerationAPI interface
2025-12-11 12:19:34 -05:00
copilot-swe-agent[bot]
1e6412e90e Fix TypeScript build errors in SpecRegenerationAPI interface
Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com>
2025-12-11 17:16:30 +00:00
copilot-swe-agent[bot]
7419288189 Initial plan 2025-12-11 17:08:45 +00:00
trueheads
f757270198 including type error commit fixes 2025-12-11 10:20:58 -06:00
trueheads
ca57b9e3ca ok one final pr to remove gemini-found race condition 2025-12-11 10:15:39 -06:00
trueheads
6352a1df19 final code review 2025-12-11 09:59:48 -06:00
Ben
f460e689f1 Update app/electron/services/feature-loader.js
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-11 09:54:22 -06:00
trueheads
74efaadb59 further gemini reviews and fixes 2025-12-11 09:36:38 -06:00
trueheads
a602b1b519 adjustments based on gemini review 2025-12-11 09:14:41 -06:00
Ben
4b1f4576ae Update app/src/components/views/spec-view.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 08:51:06 -06:00
Ben
60065806f4 Update app/electron/services/spec-regeneration-service.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 08:50:30 -06:00
Ben
9381162a8e Update app/electron/services/mcp-server-factory.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 08:50:13 -06:00
trueheads
626219fa62 Scans the project once again before generating features that may already be implemented. Soft tested, worked the 1 time I tried, created half as many items for an existing project. 2025-12-11 04:01:29 -06:00
trueheads
8d6f825c5c Overhaul updates for appspec & feature generation. Added detail back to kanban feature creation 2025-12-11 03:40:16 -06:00
trueheads
c198c10244 Complete overhaul for app spec system. Created logic to auto generate kanban stories after the fact as well as added logging logic and visual aids to tell what stage of the process the app spec creation is in. May need refinement for state-based updates as the menu doesnt update as dynamicly as id like 2025-12-11 03:01:45 -06:00
Web Dev Cody
acae5526b7 Merge pull request #25 from AutoMaker-Org/fix-build
feat: add repository information to package.json
2025-12-11 00:12:59 -05:00
Cody Seibert
d6ce71702e feat: add repository information to package.json
- Included repository details in package.json to enhance project metadata and facilitate easier access to the source code.
2025-12-11 00:08:55 -05:00
Web Dev Cody
a2d2f52cf8 Merge pull request #24 from AutoMaker-Org/fix-build
feat: add PR build check workflow and enhance feature management
2025-12-10 23:47:44 -05:00
Cody Seibert
7c7a044417 feat: update application icons and add marketing assets
- Replaced the application icon in package.json with a larger version for better visibility.
- Added a new logo image file `logo_larger.png` to the public directory.
- Introduced a Dockerfile for the marketing section to serve static files using Nginx.
- Created a new `index.html` file for the marketing site, featuring a responsive layout and sections for features and technology stack.
2025-12-10 23:43:23 -05:00
Cody Seibert
58a59aa391 Merge branch 'main' into fix-build 2025-12-10 23:37:14 -05:00
Web Dev Cody
9c6ff4c2e3 Merge pull request #21 from AutoMaker-Org/fix/auth-status-display-and-console-cleanup
fix: improve auth status display and remove verbose console logging
2025-12-10 23:32:46 -05:00
Cody Seibert
f2a443afad feat: move "Report Bug / Feature Request" button to header
- Relocated the "Report Bug / Feature Request" button from the bottom of the sidebar to the header, next to the AutoMaker logo for improved accessibility.
- Updated the button to be a compact icon-only version with a tooltip on hover.
- Adjusted the header layout to accommodate the new button placement.
- Removed the old button from the sidebar to streamline the interface.
2025-12-10 23:28:29 -05:00
Cody Seibert
9dc3124738 feat: update package.json with project metadata
- Added project description, homepage, author details, and maintainer information to package.json for better project documentation and visibility.
2025-12-10 23:17:47 -05:00
SuperComboGamer
e553b39454 fix 2025-12-10 23:14:23 -05:00
SuperComboGamer
0fca89e0b5 Merge pull request #23 from AutoMaker-Org/wsl
wsl
2025-12-10 23:13:28 -05:00
Cody Seibert
67a448ce91 feat: add PR build check workflow and enhance feature management
- Introduced a new GitHub Actions workflow for PR build checks to ensure code quality and consistency.
- Updated `analysis-view.tsx`, `interview-view.tsx`, and `setup-view.tsx` to incorporate a new `Feature` type for better feature management.
- Refactored various components to improve code readability and maintainability.
- Adjusted type imports in `delete-project-dialog.tsx` and `settings-navigation.tsx` for consistency.
- Enhanced project initialization logic in `project-init.ts` to ensure proper type handling.
- Updated Electron API types in `electron.d.ts` for better clarity and functionality.
2025-12-10 23:10:04 -05:00
SuperComboGamer
081fc09e4f wsl 2025-12-10 23:09:15 -05:00
SuperComboGamer
f96fa6561e Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 20:08:13 -08:00
SuperComboGamer
c667c1c682 Update app/electron/services/claude-cli-detector.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 20:07:54 -08:00
SuperComboGamer
d474208e8b fix: improve auth status display and remove verbose console logging
- Fix authentication status display in settings showing "Method: Unknown"
  - Add support for CLAUDE_CODE_OAUTH_TOKEN environment variable
  - Update ClaudeAuthStatus type to include all auth methods
  - Fix method mapping in use-cli-status hook
  - Display correct auth method labels in UI

- Remove verbose console logging from:
  - claude-cli-detector.js
  - codex-cli-detector.js
  - agent-service.js
  - main.js (IPC, Security logs)

- Fix TypeScript errors:
  - Add proper type exports for AutoModeEvent
  - Fix Project import paths
  - Add null checks for api.features
  - Add openExternalLink to ElectronAPI type
  - Add type annotation for REQUIRED_STRUCTURE

- Update README with clearer getting started guide

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 22:28:27 -05:00
569 changed files with 97167 additions and 46314 deletions

View File

@@ -1,202 +0,0 @@
<project_specification>
<project_name>Automaker - Autonomous AI Development Studio</project_name>
<overview>
Automaker is a sophisticated desktop application that empowers developers to build software autonomously through AI-powered agents. Built with Electron and Next.js, it provides an intelligent GUI for project management, feature tracking via Kanban boards, and autonomous code generation. The application leverages multiple AI models (Claude, GPT) and supports complex workflows including git worktree isolation, testing automation, and multi-model agent execution. It acts as a complete development orchestrator, managing the entire lifecycle from specification to verified implementation.
</overview>
<technology_stack>
<frontend>
<framework>Next.js 16.0.7 (App Router)</framework>
<ui_library>shadcn/ui with Radix UI primitives</ui_library>
<styling>Tailwind CSS 4.0</styling>
<state_management>Zustand with persistence</state_management>
<drag_drop>@dnd-kit for Kanban board</drag_drop>
<icons>Lucide React</icons>
<query_client>TanStack Query for server state</query_client>
</frontend>
<desktop_shell>
<framework>Electron 39.2.6</framework>
<language>TypeScript 5.x</language>
<inter_process_communication>Electron IPC with security sandboxing</inter_process_communication>
<file_system>Node.js fs/promises with path validation</file_system>
</desktop_shell>
<ai_engine>
<primary_model>Claude 3.5 (Opus, Sonnet, Haiku) via Anthropic Claude Agent SDK</primary_model>
<secondary_model>GPT-5.1 Codex family via OpenAI CLI</secondary_model>
<orchestration>Custom Agent Service with streaming responses</orchestration>
<model_registry>Dynamic model provider system with CLI detection</model_registry>
</ai_engine>
<testing>
<framework>Playwright for E2E testing</framework>
<unit>Jest/Vitest compatible</unit>
<integration>Agent-driven test execution and verification</integration>
</testing>
<version_control>
<system>Git with worktree isolation support</system>
<branching>Feature branch management</branching>
<workflow>Automated commit and merge capabilities</workflow>
</version_control>
</technology_stack>
<core_capabilities>
<project_management>
- Open and manage multiple local projects
- Project-specific themes and configurations
- Session management with project context
- Recently used project cycling (Q/E shortcuts)
- Project search and type-ahead selection
- Trash and restore functionality for projects
</project_management>
<intelligent_analysis>
- Auto-generation and updating of app_spec.txt
- Feature extraction from existing codebases
- Technology stack detection and documentation
- Project structure analysis with file tree visualization - "Project Ingestion": Analyzes existing codebases to understand structure
- Auto-generation of `.automaker/app_spec.txt` based on codebase analysis
- Auto-generation of features in `.automaker/features/{id}/feature.json`:
- Scans code for implemented features
- Creates test cases for existing features
- Marks existing features as "passes": true automatically
</intelligent_analysis>
<kanban_workflow>
- Visual representation of features from `.automaker/features/` folder
- Drag-and-drop interface to reprioritize tasks
- direct editing of feature details (steps, description) from the card
- Visual Kanban board with drag-and-drop functionality
- Multiple status columns: Backlog, In Progress, Waiting Approval, Verified
- Feature cards with detailed information display (3 detail levels)
- Real-time status updates during agent execution
- Search and filtering capabilities
- Category management and autocomplete
- Image attachment support for feature descriptions
</kanban_workflow>
<autonomous_agent_engine>
- Multi-model agent system with profile-based execution
- Streaming agent output with real-time logs
- Git worktree isolation for safe feature development
- Automatic testing and verification workflows
- Context-aware prompt generation
- Agent memory and learning capabilities
- Concurrent feature processing with configurable limits
- Follow-up and resume capabilities
</autonomous_agent_engine>
<advanced_workflows>
- Git worktree management for isolated development
- Feature-specific branching and merging
- Automated commit generation with file tracking
- Test-driven development support
- Code review and approval workflows
- Revert and rollback capabilities
</advanced_workflows>
<user_interface>
- Dark/Light theme support with 12 custom themes
- Per-project theme configurations
- Comprehensive keyboard shortcut system
- Sidebar navigation with project switching
- Multi-view architecture (Board, Spec, Agent, Context, Settings)
- Setup wizard for first-time configuration
- CLI integration status monitoring
</user_interface>
<extensibility>
- AI Profile system for model/thinking level presets
- Keyboard shortcut customization
- Model provider plugin architecture
- Context file management for agent guidance
- Feature suggestion generation
- Spec regeneration workflows
</extensibility>
</core_capabilities>
<ui_layout>
<window_structure>
- Sidebar: Project List, Settings, Logs, Plugins
- Main Content:
- **Spec View**: Split editor for `.automaker/app_spec.txt`
- **Board View**: Kanban board for `.automaker/features/` folder
- **Code View**: Read-only Monaco editor to see what the agent is writing
- **Agent View**: Chat-like interface showing agent thought process and tool usage. Also used for the "New Project Interview".
</window_structure>
<theme>
- Dark/Light mode support (system sync)
- "Hacker" aesthetic option (terminal-like)
- Professional/Clean default
</theme>
</ui_layout>
<development_workflow>
<local_testing>
- "Browser Mode": Run the Next.js frontend in a standard browser with mocked Electron IPC for rapid UI iteration.
- "Electron Mode": Full desktop app testing.
- Hot Reloading for both Main and Renderer processes.
</local_testing>
</development_workflow>
<implemented_features>
- Complete Kanban board with drag-and-drop functionality
- Multi-model AI agent execution (Claude + GPT/Codex)
- Git worktree isolation for features
- Real-time agent output streaming and logging
- Project management with session persistence
- Theme system with 12 themes + per-project themes
- Comprehensive settings panel with all configurations
- Feature image attachment and context system
- Agent profiles with model/thinking level presets
- Keyboard shortcut system with customization
- CLI integration detection (Claude Code + Codex CLI)
- Auto mode for autonomous feature processing
- Feature suggestions generation
- Spec regeneration and project analysis
- Context file management
- Chat history and session management
- File diff viewing and git integration
- Search and filtering across all features
- Category management and autocomplete
- Test automation and verification workflows
</implemented_features>
<implementation_roadmap>
<phase_1_foundation>
- Enhanced error handling and recovery mechanisms
- Performance optimization for large projects
- Improved memory management for long-running sessions
- Advanced logging and debugging capabilities
</phase_1_foundation>
<phase_2_core_logic>
- Plugin system for custom model providers
- Advanced workflow customization engine
- Team collaboration features
- Cloud synchronization capabilities
- Advanced project templates and scaffolding
</phase_2_core_logic>
<phase_3_kanban_and_interaction>
- Build Kanban board with drag-and-drop
- Connect Kanban state to `.automaker/features/` filesystem
- Implement "Run Feature" capability
- Integrate standard prompts library
</phase_3_kanban_and_interaction>
<phase_3_polish>
- Enhanced accessibility features
- Advanced theme customization
- Performance monitoring and analytics
- Documentation generation automation
- Integration with external development tools
- Advanced security auditing and sandboxing
</phase_3_polish>
<phase_4_polish>
- Advanced terminal integration
- Settings & Extensibility
- UI refinement
</phase_4_polish>
</implementation_roadmap>
</project_specification>

View File

@@ -1,9 +0,0 @@
[
"Agent Runner",
"Core",
"Kanban",
"Other",
"Settings",
"Uncategorized",
"ka"
]

View File

@@ -1,70 +0,0 @@
You are a very strong reasoner and planner. Use these critical instructions to structure your plans, thoughts, and responses.
Before taking any action (either tool calls or responses to the user), you must proactively, methodically, and independently plan and reason about:
1. Logical dependencies and constraints:
Analyze the intended action against the following factors. Resolve conflicts in order of importance:
1.1) Policy-based rules, mandatory prerequisites, and constraints.
1.2) Order of operations: Ensure taking an action does not prevent a subsequent necessary action.
1.2.1) The user may request actions in a random order, but you may need to reorder operations to maximize successful completion of the task.
1.3) Other prerequisites (information and/or actions needed).
1.4) Explicit user constraints or preferences.
2. Risk assessment:
What are the consequences of taking the action? Will the new state cause any future issues?
2.1) For exploratory tasks (like searches), missing optional parameters is a LOW risk.
Prefer calling the tool with the available information over asking the user, unless your Rule 1 (Logical Dependencies) reasoning determines that optional information is required for a later step in your plan.
3. Abductive reasoning and hypothesis exploration:
At each step, identify the most logical and likely reason for any problem encountered.
3.1) Look beyond immediate or obvious causes. The most likely reason may not be the simplest and may require deeper inference.
3.2) Hypotheses may require additional research. Each hypothesis may take multiple steps to test.
3.3) Prioritize hypotheses based on likelihood, but do not discard less likely ones prematurely. A low-probability event may still be the root cause.
4. Outcome evaluation and adaptability:
Does the previous observation require any changes to your plan?
4.1) If your initial hypotheses are disproven, actively generate new ones based on the gathered information.
5. Information availability:
Incorporate all applicable and alternative sources of information, including:
5.1) Using available tools and their capabilities
5.2) All policies, rules, checklists, and constraints
5.3) Previous observations and conversation history
5.4) Information only available by asking the user
6. Precision and Grounding:
Ensure your reasoning is extremely precise and relevant to each exact ongoing situation.
6.1) Verify your claims by quoting the exact applicable information (including policies) when referring to them.
7. Completeness:
Ensure that all requirements, constraints, options, and preferences are exhaustively incorporated into your plan.
7.1) Resolve conflicts using the order of importance in #1.
7.2) Avoid premature conclusions: There may be multiple relevant options for a given situation.
7.2.1) To check for whether an option is relevant, reason about all information sources from #5.
7.2.2) You may need to consult the user to even know whether something is applicable. Do not assume it is not applicable without checking.
7.3) Review applicable sources of information from #5 to confirm which are relevant to the current state.
8. Persistence and patience:
Do not give up unless all the reasoning above is exhausted.
8.1) Don't be dissuaded by time taken or user frustration.
8.2) This persistence must be intelligent: On transient errors (e.g. please try again), you must retry unless an explicit retry limit (e.g., max x tries) has been reached. If such a limit is hit, you must stop. On other errors, you must change your strategy or arguments, not repeat the same failed call.
9. Inhibit your response:
Only take an action after all the above reasoning is completed. Once you've taken an action, you cannot take it back.

View File

@@ -1,172 +0,0 @@
# Agent Memory - Lessons Learned
This file documents issues encountered by previous agents and their solutions. Read this before starting work to avoid repeating mistakes.
## Testing Issues
### Issue: Mock project setup not navigating to board view
**Problem:** Setting `currentProject` in localStorage didn't automatically show the board view - app stayed on welcome view.
**Fix:** The `currentView` state is not persisted in localStorage. Instead of trying to set it, have tests click on the recent project from the welcome view to trigger `setCurrentProject()` which handles the view transition properly.
```typescript
// Don't do this:
await setupMockProject(page); // Sets localStorage
await page.goto("/");
await waitForElement(page, "board-view"); // ❌ Fails - still on welcome view
// Do this instead:
await setupMockProject(page);
await page.goto("/");
await waitForElement(page, "welcome-view");
const recentProject = page.locator(
'[data-testid="recent-project-test-project-1"]'
);
await recentProject.click(); // ✅ Triggers proper view transition
await waitForElement(page, "board-view");
```
### Issue: View output button test IDs are conditional
**Problem:** Tests failed looking for `view-output-inprogress-${featureId}` when the actual button had `view-output-${featureId}`.
**Fix:** The button test ID depends on whether the feature is actively running:
- `view-output-${featureId}` - shown when feature is in `runningAutoTasks` (actively running)
- `view-output-inprogress-${featureId}` - shown when status is "in_progress" but NOT actively running
After dragging a feature to in_progress, wait for the `auto_mode_feature_start` event to fire before looking for the button:
```typescript
// Wait for feature to start running
const viewOutputButton = page
.locator(
`[data-testid="view-output-${featureId}"], [data-testid="view-output-inprogress-${featureId}"]`
)
.first();
await expect(viewOutputButton).toBeVisible({ timeout: 8000 });
```
### Issue: Elements not appearing due to async event timing
**Problem:** Tests checked for UI elements before async events (like `auto_mode_feature_start`) had fired and updated the UI.
**Fix:** Add appropriate timeouts when waiting for elements that depend on async events. The mock auto mode takes ~2.4 seconds to complete, so allow sufficient time:
```typescript
// Mock auto mode timing: ~2.4s + 1.5s delay = ~4s total
await waitForAgentOutputModalHidden(page, { timeout: 10000 });
```
### Issue: Slider interaction testing
**Problem:** Clicking on slider track didn't reliably set specific values.
**Fix:** Use the slider's keyboard interaction or calculate the exact click position on the track. For max value, click on the rightmost edge of the track.
### Issue: Port binding blocked in sandbox mode
**Problem:** Playwright tests couldn't bind to port in sandbox mode.
**Fix:** Tests don't need sandbox disabled - the issue was TEST_REUSE_SERVER environment variable. Make sure to start the dev server separately or let Playwright's webServer config handle it.
## Code Architecture
### Issue: Understanding store state persistence
**Problem:** Not all store state is persisted to localStorage.
**Fix:** Check the `partialize` function in `app-store.ts` to see which state is persisted:
```typescript
partialize: (state) => ({
projects: state.projects,
currentProject: state.currentProject,
theme: state.theme,
sidebarOpen: state.sidebarOpen,
apiKeys: state.apiKeys,
chatSessions: state.chatSessions,
chatHistoryOpen: state.chatHistoryOpen,
maxConcurrency: state.maxConcurrency, // Added for concurrency feature
});
```
Note: `currentView` is NOT persisted - it's managed through actions.
### Issue: Auto mode task lifecycle
**Problem:** Confusion about when features are considered "running" vs "in_progress".
**Fix:** Understand the task lifecycle:
1. Feature dragged to "in_progress" column → status becomes "in_progress"
2. `auto_mode_feature_start` event fires → feature added to `runningAutoTasks`
3. Agent works on feature → periodic events sent
4. `auto_mode_feature_complete` event fires → feature removed from `runningAutoTasks`
5. If `passes: true` → status becomes "verified", if `passes: false` → stays "in_progress"
### Issue: waiting_approval features not draggable when skipTests=true
**Problem:** Features in `waiting_approval` status couldn't be dragged to `verified` column, even though the code appeared to handle it.
**Fix:** The order of condition checks in `handleDragEnd` matters. The `skipTests` check was catching `waiting_approval` features before the `waiting_approval` status check could handle them. Move the `waiting_approval` status check **before** the `skipTests` check in `board-view.tsx`:
```typescript
// Correct order in handleDragEnd:
if (draggedFeature.status === "backlog") {
// ...
} else if (draggedFeature.status === "waiting_approval") {
// Handle waiting_approval BEFORE skipTests check
// because waiting_approval features often have skipTests=true
} else if (draggedFeature.skipTests) {
// Handle other skipTests features
}
```
## Best Practices Discovered
### Testing utilities are critical
Create comprehensive testing utilities in `tests/utils.ts` to avoid repeating selector logic:
- `waitForElement` - waits for elements to appear
- `waitForElementHidden` - waits for elements to disappear
- `setupMockProject` - sets up mock localStorage state
- `navigateToBoard` - handles navigation from welcome to board view
### Always add data-testid attributes
When implementing features, immediately add `data-testid` attributes to key UI elements. This makes tests more reliable and easier to write.
### Test timeouts should be generous but not excessive
- Default timeout: 30s (set in playwright.config.ts)
- Element waits: 5-15s for critical elements
- Auto mode completion: 10s (accounts for ~4s mock duration)
- Don't increase timeouts past 10s for individual operations
### Mock auto mode timing
The mock auto mode in `electron.ts` has predictable timing:
- Total duration: ~2.4 seconds (300+500+300+300+500+500ms)
- Plus 1.5s delay before auto-closing modals
- Total: ~4 seconds from start to completion
### Issue: HotkeyButton conflicting with useKeyboardShortcuts
**Problem:** Adding `HotkeyButton` with a simple key (like "N") to buttons that already had keyboard shortcuts registered via `useKeyboardShortcuts` caused the hotkey to stop working. Both registered duplicate listeners, and the HotkeyButton's `stopPropagation()` call could interfere.
**Fix:** When a simple single-key hotkey is already handled by `useKeyboardShortcuts`, set `hotkeyActive={false}` on the `HotkeyButton` so it only displays the indicator badge without registering a duplicate listener:
```tsx
// In views that already use useKeyboardShortcuts for the "N" key:
<HotkeyButton
onClick={() => setShowAddDialog(true)}
hotkey={shortcuts.addFeature}
hotkeyActive={false} // <-- Important! Prevents duplicate listener
>
Add Feature
</HotkeyButton>
// HotkeyButton should only actively listen when it's the sole handler (e.g., Cmd+Enter in dialogs)
<HotkeyButton
onClick={handleSubmit}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={isDialogOpen} // Active when dialog is open
>
Submit
</HotkeyButton>
```

394
.github/scripts/upload-to-r2.js vendored Normal file
View File

@@ -0,0 +1,394 @@
const {
S3Client,
PutObjectCommand,
GetObjectCommand,
} = require("@aws-sdk/client-s3");
const fs = require("fs");
const path = require("path");
const https = require("https");
const { pipeline } = require("stream/promises");
const s3Client = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});
const BUCKET = process.env.R2_BUCKET_NAME;
const PUBLIC_URL = process.env.R2_PUBLIC_URL;
const VERSION = process.env.RELEASE_VERSION;
const RELEASE_TAG = process.env.RELEASE_TAG || `v${VERSION}`;
const GITHUB_REPO = process.env.GITHUB_REPOSITORY;
async function fetchExistingReleases() {
try {
const response = await s3Client.send(
new GetObjectCommand({
Bucket: BUCKET,
Key: "releases.json",
})
);
const body = await response.Body.transformToString();
return JSON.parse(body);
} catch (error) {
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
console.log("No existing releases.json found, creating new one");
return { latestVersion: null, releases: [] };
}
throw error;
}
}
async function uploadFile(localPath, r2Key, contentType) {
const fileBuffer = fs.readFileSync(localPath);
const stats = fs.statSync(localPath);
await s3Client.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: r2Key,
Body: fileBuffer,
ContentType: contentType,
})
);
console.log(`Uploaded: ${r2Key} (${stats.size} bytes)`);
return stats.size;
}
function findArtifacts(dir, pattern) {
if (!fs.existsSync(dir)) return [];
const files = fs.readdirSync(dir);
return files.filter((f) => pattern.test(f)).map((f) => path.join(dir, f));
}
async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const result = await new Promise((resolve, reject) => {
const request = https.get(url, { timeout: 10000 }, (response) => {
const statusCode = response.statusCode;
// Follow redirects
if (
statusCode === 302 ||
statusCode === 301 ||
statusCode === 307 ||
statusCode === 308
) {
const redirectUrl = response.headers.location;
response.destroy();
if (!redirectUrl) {
resolve({
accessible: false,
statusCode,
error: "Redirect without location header",
});
return;
}
// Follow the redirect URL
return https
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
const redirectStatus = redirectResponse.statusCode;
const contentType =
redirectResponse.headers["content-type"] || "";
// Check if it's actually a file (zip/tar.gz) and not HTML
const isFile =
contentType.includes("application/zip") ||
contentType.includes("application/gzip") ||
contentType.includes("application/x-gzip") ||
contentType.includes("application/x-tar") ||
redirectUrl.includes(".zip") ||
redirectUrl.includes(".tar.gz");
const isGood =
redirectStatus >= 200 && redirectStatus < 300 && isFile;
redirectResponse.destroy();
resolve({
accessible: isGood,
statusCode: redirectStatus,
finalUrl: redirectUrl,
contentType,
});
})
.on("error", (error) => {
resolve({
accessible: false,
statusCode,
error: error.message,
});
})
.on("timeout", function () {
this.destroy();
resolve({
accessible: false,
statusCode,
error: "Timeout following redirect",
});
});
}
// Check if status is good (200-299 range) and it's actually a file
const contentType = response.headers["content-type"] || "";
const isFile =
contentType.includes("application/zip") ||
contentType.includes("application/gzip") ||
contentType.includes("application/x-gzip") ||
contentType.includes("application/x-tar") ||
url.includes(".zip") ||
url.includes(".tar.gz");
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
response.destroy();
resolve({ accessible: isGood, statusCode, contentType });
});
request.on("error", (error) => {
resolve({
accessible: false,
statusCode: null,
error: error.message,
});
});
request.on("timeout", () => {
request.destroy();
resolve({
accessible: false,
statusCode: null,
error: "Request timeout",
});
});
});
if (result.accessible) {
if (attempt > 0) {
console.log(
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
);
} else {
console.log(
`✓ URL ${url} is accessible (status: ${result.statusCode})`
);
}
return result.finalUrl || url; // Return the final URL (after redirects) if available
} else {
const errorMsg = result.error ? ` - ${result.error}` : "";
const statusMsg = result.statusCode
? ` (status: ${result.statusCode})`
: "";
const contentTypeMsg = result.contentType
? ` [content-type: ${result.contentType}]`
: "";
console.log(
`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`
);
}
} catch (error) {
console.log(`✗ URL ${url} check failed: ${error.message}`);
}
if (attempt < maxRetries - 1) {
const delay = initialDelay * Math.pow(2, attempt);
console.log(
` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(`URL ${url} is not accessible after ${maxRetries} attempts`);
}
async function downloadFromGitHub(url, outputPath) {
return new Promise((resolve, reject) => {
const request = https.get(url, { timeout: 30000 }, (response) => {
const statusCode = response.statusCode;
// Follow redirects (all redirect types)
if (
statusCode === 301 ||
statusCode === 302 ||
statusCode === 307 ||
statusCode === 308
) {
const redirectUrl = response.headers.location;
response.destroy();
if (!redirectUrl) {
reject(new Error(`Redirect without location header for ${url}`));
return;
}
// Resolve relative redirects
const finalRedirectUrl = redirectUrl.startsWith("http")
? redirectUrl
: new URL(redirectUrl, url).href;
console.log(` Following redirect: ${finalRedirectUrl}`);
return downloadFromGitHub(finalRedirectUrl, outputPath)
.then(resolve)
.catch(reject);
}
if (statusCode !== 200) {
response.destroy();
reject(
new Error(
`Failed to download ${url}: ${statusCode} ${response.statusMessage}`
)
);
return;
}
const fileStream = fs.createWriteStream(outputPath);
response.pipe(fileStream);
fileStream.on("finish", () => {
fileStream.close();
resolve();
});
fileStream.on("error", (error) => {
response.destroy();
reject(error);
});
});
request.on("error", reject);
request.on("timeout", () => {
request.destroy();
reject(new Error(`Request timeout for ${url}`));
});
});
}
async function main() {
const artifactsDir = "artifacts";
const tempDir = path.join(artifactsDir, "temp");
// Create temp directory for downloaded GitHub archives
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Download source archives from GitHub
const githubZipUrl = `https://github.com/${GITHUB_REPO}/archive/refs/tags/${RELEASE_TAG}.zip`;
const githubTarGzUrl = `https://github.com/${GITHUB_REPO}/archive/refs/tags/${RELEASE_TAG}.tar.gz`;
const sourceZipPath = path.join(tempDir, `automaker-${VERSION}.zip`);
const sourceTarGzPath = path.join(tempDir, `automaker-${VERSION}.tar.gz`);
console.log(`Waiting for source archives to be available on GitHub...`);
console.log(` ZIP: ${githubZipUrl}`);
console.log(` TAR.GZ: ${githubTarGzUrl}`);
// Wait for archives to be accessible with exponential backoff
// This returns the final URL after following redirects
const finalZipUrl = await checkUrlAccessible(githubZipUrl);
const finalTarGzUrl = await checkUrlAccessible(githubTarGzUrl);
console.log(`Downloading source archives from GitHub...`);
await downloadFromGitHub(finalZipUrl, sourceZipPath);
await downloadFromGitHub(finalTarGzUrl, sourceTarGzPath);
console.log(`Downloaded source archives successfully`);
// Find all artifacts
const artifacts = {
windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/),
macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/),
macosArm: findArtifacts(
path.join(artifactsDir, "macos-builds"),
/-arm64\.dmg$/
),
linux: findArtifacts(
path.join(artifactsDir, "linux-builds"),
/\.AppImage$/
),
sourceZip: [sourceZipPath],
sourceTarGz: [sourceTarGzPath],
};
console.log("Found artifacts:");
for (const [platform, files] of Object.entries(artifacts)) {
console.log(
` ${platform}: ${
files.length > 0
? files.map((f) => path.basename(f)).join(", ")
: "none"
}`
);
}
// Upload each artifact to R2
const assets = {};
const contentTypes = {
windows: "application/x-msdownload",
macos: "application/x-apple-diskimage",
macosArm: "application/x-apple-diskimage",
linux: "application/x-executable",
sourceZip: "application/zip",
sourceTarGz: "application/gzip",
};
for (const [platform, files] of Object.entries(artifacts)) {
if (files.length === 0) {
console.warn(`Warning: No artifact found for ${platform}`);
continue;
}
// Use the first matching file for each platform
const localPath = files[0];
const filename = path.basename(localPath);
const r2Key = `releases/${VERSION}/${filename}`;
const size = await uploadFile(localPath, r2Key, contentTypes[platform]);
assets[platform] = {
url: `${PUBLIC_URL}/releases/${VERSION}/${filename}`,
filename,
size,
arch:
platform === "macosArm"
? "arm64"
: platform === "sourceZip" || platform === "sourceTarGz"
? "source"
: "x64",
};
}
// Fetch and update releases.json
const releasesData = await fetchExistingReleases();
const newRelease = {
version: VERSION,
date: new Date().toISOString(),
assets,
githubReleaseUrl: `https://github.com/${GITHUB_REPO}/releases/tag/${RELEASE_TAG}`,
};
// Remove existing entry for this version if re-running
releasesData.releases = releasesData.releases.filter(
(r) => r.version !== VERSION
);
// Prepend new release
releasesData.releases.unshift(newRelease);
releasesData.latestVersion = VERSION;
// Upload updated releases.json
await s3Client.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: "releases.json",
Body: JSON.stringify(releasesData, null, 2),
ContentType: "application/json",
CacheControl: "public, max-age=60",
})
);
console.log("Successfully updated releases.json");
console.log(`Latest version: ${VERSION}`);
console.log(`Total releases: ${releasesData.releases.length}`);
}
main().catch((err) => {
console.error("Failed to upload to R2:", err);
process.exit(1);
});

96
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: E2E Tests
on:
pull_request:
branches:
- "*"
push:
branches:
- main
- master
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
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)
# 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: Install Playwright browsers
run: npx playwright install --with-deps chromium
working-directory: apps/app
- name: Build server
run: npm run build --workspace=apps/server
- name: Start backend server
run: npm run start --workspace=apps/server &
env:
PORT: 3008
NODE_ENV: test
- name: Wait for backend server
run: |
echo "Waiting for backend server to be ready..."
for i in {1..30}; do
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
echo "Backend server is ready!"
exit 0
fi
echo "Waiting... ($i/30)"
sleep 1
done
echo "Backend server failed to start!"
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
env:
CI: true
NEXT_PUBLIC_SERVER_URL: http://localhost:3008
NEXT_PUBLIC_SKIP_SETUP: "true"
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: apps/app/playwright-report/
retention-days: 7
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: apps/app/test-results/
retention-days: 7

49
.github/workflows/pr-check.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: PR Build Check
on:
pull_request:
branches:
- "*"
push:
branches:
- main
- master
jobs:
build:
runs-on: ubuntu-latest
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: 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

View File

@@ -3,13 +3,13 @@ name: Build and Release Electron App
on:
push:
tags:
- 'v*.*.*' # Triggers on version tags like v1.0.0
workflow_dispatch: # Allows manual triggering
- "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)'
description: "Version to release (e.g., v1.0.0)"
required: true
default: 'v0.1.0'
default: "v0.1.0"
jobs:
build-and-release:
@@ -19,10 +19,13 @@ jobs:
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 }}
@@ -36,31 +39,58 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: app/package-lock.json
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
working-directory: ./app
run: npm ci
# 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'
working-directory: ./app
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --mac --x64 --arm64
- name: Build Electron App (Windows)
if: matrix.os == 'windows-latest'
working-directory: ./app
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --win --x64
- name: Build Electron App (Linux)
if: matrix.os == 'ubuntu-latest'
working-directory: ./app
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:electron -- --linux --x64
@@ -70,13 +100,81 @@ jobs:
with:
tag_name: ${{ github.event.inputs.version || github.ref_name }}
files: |
app/dist/*.exe
app/dist/*.dmg
app/dist/*.AppImage
app/dist/*.zip
app/dist/*.deb
app/dist/*.rpm
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

58
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Test Suite
on:
pull_request:
branches:
- "*"
push:
branches:
- main
- master
jobs:
test:
runs-on: ubuntu-latest
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)
# 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 server tests with coverage
run: npm run test:server:coverage
env:
NODE_ENV: test
# - name: Upload coverage reports
# uses: codecov/codecov-action@v4
# if: always()
# with:
# files: ./apps/server/coverage/coverage-final.json
# flags: server
# name: server-coverage
# env:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

80
.gitignore vendored
View File

@@ -1,2 +1,80 @@
#added by trueheads > will remove once supercombo adds multi-os support
#added by trueheads > will remove once supercombo adds multi-os support
launch.sh
# Dependencies
node_modules/
# Build outputs
dist/
build/
out/
.next/
.turbo/
# Automaker
.automaker/images/
.automaker/
/.automaker/*
/.automaker/
.worktrees/
/logs
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS-specific files
.DS_Store
.DS_Store?
._*
Thumbs.db
ehthumbs.db
Desktop.ini
# IDE/Editor configs
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
# Editor backup/temp files
*~
*.bak
*.backup
*.orig
*.swp
*.swo
*.tmp
*.temp
# Local settings (user-specific)
*.local.json
# Application state/backup
backup.json
# Test artifacts
test-results/
coverage/
.nyc_output/
*.lcov
playwright-report/
blob-report/
# Environment files (keep .example)
.env
.env.local
.env.*.local
!.env.example
!.env.local.example
# TypeScript
*.tsbuildinfo
# Misc
*.pem

16
.npmrc Normal file
View File

@@ -0,0 +1,16 @@
# Cross-platform compatibility for Tailwind CSS v4 and lightningcss
# These packages use platform-specific optional dependencies that npm
# automatically resolves based on your OS (macOS, Linux, Windows, WSL)
#
# IMPORTANT: When switching platforms or getting platform mismatch errors:
# 1. Delete node_modules: rm -rf node_modules apps/*/node_modules
# 2. Run: npm install
#
# In CI/CD: Use "npm install" instead of "npm ci" to allow npm to resolve
# the correct platform-specific binaries at install time.
# Include bindings for all platforms in package-lock.json to support CI/CD
# This ensures Linux, macOS, and Windows bindings are all present
# NOTE: Only enable when regenerating package-lock.json, then comment out to keep installs fast
# supportedArchitectures.os=linux,darwin,win32
# supportedArchitectures.cpu=x64,arm64

View File

@@ -19,9 +19,11 @@ While we have made efforts to review this codebase for security vulnerabilities
## Recommendations
### 1. Review the Code First
Before running Automaker, we strongly recommend reviewing the source code yourself to understand what operations it performs and ensure you are comfortable with its behavior.
### 2. Use Sandboxing (Highly Recommended)
**We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Instead, consider:
- **Docker**: Run Automaker in a Docker container to isolate it from your host system
@@ -29,20 +31,25 @@ Before running Automaker, we strongly recommend reviewing the source code yourse
- **Cloud Development Environment**: Use a cloud-based development environment that provides isolation
### 3. Limit Access
If you must run locally:
- Create a dedicated user account with limited permissions
- Only grant access to specific project directories
- Avoid running with administrator/root privileges
- Keep sensitive files and credentials outside of project directories
### 4. Monitor Activity
- Review the agent's actions in the output logs
- Pay attention to file modifications and command executions
- Stop the agent immediately if you notice unexpected behavior
## No Warranty
## No Warranty & Limitation of Liability
This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
THE SOFTWARE UTILIZES ARTIFICIAL INTELLIGENCE TO GENERATE CODE, EXECUTE COMMANDS, AND INTERACT WITH YOUR FILE SYSTEM. YOU ACKNOWLEDGE THAT AI SYSTEMS CAN BE UNPREDICTABLE, MAY GENERATE INCORRECT, INSECURE, OR DESTRUCTIVE CODE, AND MAY TAKE ACTIONS THAT COULD DAMAGE YOUR SYSTEM, FILES, OR HARDWARE.
This software is provided "as is", without warranty of any kind, express or implied. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, including but not limited to hardware damage, data loss, financial loss, or business interruption, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
## Acknowledgment

154
LICENSE
View File

@@ -1,21 +1,141 @@
MIT License
AUTOMAKER LICENSE AGREEMENT
Copyright (c) 2025 Cody Seibert
This License Agreement ("Agreement") is entered into between you ("Licensee") and the copyright holders of Automaker ("Licensor"). By using, copying, modifying, downloading, cloning, or distributing the Software (as defined below), you agree to be bound by the terms of this Agreement.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. DEFINITIONS
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"Software" means the Automaker software, including all source code, object code, documentation, and related materials.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Generated Files" means files created by the Software during normal operation to store internal state, configuration, or working data, including but not limited to app_spec.txt, feature.json, and similar files generated by the Software. Generated Files are not considered part of the Software for the purposes of this license and are not subject to the restrictions herein.
"Derivative Work" means any work that is based on, derived from, or incorporates the Software or any substantial portion of it, including but not limited to modifications, forks, adaptations, translations, or any altered version of the Software.
"Monetization" means any activity that generates revenue, income, or commercial benefit from the Software itself or any Derivative Work, including but not limited to:
- Reselling, redistributing, or sublicensing the Software, any Derivative Work, or any substantial portion thereof
- Including the Software, any Derivative Work, or substantial portions thereof in a product or service that you sell or distribute
- Offering the Software, any Derivative Work, or substantial portions thereof as a standalone product or service for sale
- Hosting the Software or any Derivative Work as a service (whether free or paid) for use by others, including cloud hosting, Software-as-a-Service (SaaS), or any other form of hosted access for third parties
- Extracting, reselling, redistributing, or sublicensing any prompts, context, or other instructional content bundled within the Software
- Creating, distributing, or selling modified versions, forks, or Derivative Works of the Software
Monetization does NOT include:
- Using the Software internally within your organization, regardless of whether your organization is for-profit
- Using the Software to build products or services that generate revenue, as long as you are not reselling or redistributing the Software itself
- Using the Software to provide services for which fees are charged, as long as the Software itself is not being resold or redistributed
- Hosting the Software anywhere for personal use by a single developer, as long as the Software is not made accessible to others
"Core Contributors" means the following individuals who are granted perpetual, royalty-free licenses:
- Cody Seibert (webdevcody)
- SuperComboGamer (SCG)
- Kacper Lachowicz (Shironex, Shirone)
- Ben Scott (trueheads)
2. GRANT OF LICENSE
Subject to the terms and conditions of this Agreement, Licensor hereby grants to Licensee a non-exclusive, non-transferable license to use, copy, modify, and distribute the Software, provided that:
a) Licensee may freely clone, install, and use the Software locally or within an organization for the purpose of building, developing, and maintaining other products, software, or services. There are no restrictions on the products you build _using_ the Software.
b) Licensee may run the Software on personal or organizational infrastructure for internal use.
c) Core Contributors are each individually granted a perpetual, worldwide, royalty-free, non-exclusive license to use, copy, modify, distribute, and sublicense the Software for any purpose, including Monetization, without payment of any fees or royalties. Each Core Contributor may exercise these rights independently and does not require permission, consent, or approval from any other Core Contributor to Monetize the Software in any way they see fit.
d) Commercial licenses for the Software may be discussed and issued to external parties or companies seeking to use the Software for financial gain or Monetization purposes. Core Contributors already have full rights under section 2(c) and do not require commercial licenses. Any commercial license issued to external parties shall require a unanimous vote by all Core Contributors and shall be granted in writing and signed by all Core Contributors.
e) The list of individuals defined as "Core Contributors" in Section 1 shall be amended to reflect any revocation or reinstatement of status made under this section.
3. RESTRICTIONS
Licensee may NOT:
- Engage in any Monetization of the Software or any Derivative Work without explicit written permission from all Core Contributors
- Resell, redistribute, or sublicense the Software, any Derivative Work, or any substantial portion thereof
- Create, distribute, or sell modified versions, forks, or Derivative Works of the Software for any commercial purpose
- Include the Software, any Derivative Work, or substantial portions thereof in a product or service that you sell or distribute
- Offer the Software, any Derivative Work, or substantial portions thereof as a standalone product or service for sale
- Extract, resell, redistribute, or sublicense any prompts, context, or other instructional content bundled within the Software
- Host the Software or any Derivative Work as a service (whether free or paid) for use by others (except Core Contributors)
- Remove or alter any copyright notices or license terms
- Use the Software in any manner that violates applicable laws or regulations
Licensee MAY:
- Use the Software internally within their organization (commercial or non-profit)
- Use the Software to build other commercial products (products that do NOT contain the Software or Derivative Works)
- Modify the Software for internal use within their organization (commercial or non-profit)
4. CORE CONTRIBUTOR STATUS MANAGEMENT
a) Core Contributor status may be revoked indefinitely by the remaining Core Contributors if:
- A Core Contributor cannot be reached for a period of one (1) month through reasonable means of communication (including but not limited to email, Discord, GitHub, or other project communication channels)
- AND the Core Contributor has not contributed to the project during that one-month period. For purposes of this section, "contributed" means at least one of the following activities:
- Discussing the Software through project communication channels
- Committing code changes to the project repository
- Submitting bug fixes or patches
- Participating in project-related discussions or decision-making
b) Revocation of Core Contributor status requires a unanimous vote by all other Core Contributors (excluding the Core Contributor whose status is being considered for revocation).
c) Upon revocation of Core Contributor status, the individual shall no longer be considered a Core Contributor and shall lose the rights granted under section 2(c) of this Agreement. However, any Contributions made prior to revocation shall remain subject to the terms of section 5 (CONTRIBUTIONS AND RIGHTS ASSIGNMENT).
d) A revoked Core Contributor may be reinstated to Core Contributor status with a unanimous vote by all current Core Contributors. Upon reinstatement, the individual shall regain all rights granted under section 2(c) of this Agreement.
5. CONTRIBUTIONS AND RIGHTS ASSIGNMENT
By submitting, pushing, or contributing any code, documentation, pull requests, issues, or other materials ("Contributions") to the Automaker project, you agree to the following terms without reservation:
a) **Full Ownership Transfer & Rights Grant:** You hereby assign to the Core Contributors all right, title, and interest in and to your Contributions, including all copyrights, patents, and other intellectual property rights. If such assignment is not effective under applicable law, you grant the Core Contributors an unrestricted, perpetual, worldwide, non-exclusive, royalty-free, fully paid-up, irrevocable, sublicensable, and transferable license to use, reproduce, modify, adapt, publish, translate, create derivative works from, distribute, perform, display, and otherwise exploit your Contributions in any manner they see fit, including for any commercial purpose or Monetization.
b) **No Take-Backs:** You understand and agree that this grant of rights is irrevocable ("no take-backs"). You cannot revoke, rescind, or terminate this grant of rights once your Contribution has been submitted.
c) **Waiver of Moral Rights:** You waive any "moral rights" or other rights with respect to attribution of authorship or integrity of materials regarding your Contributions that you may have under any applicable law.
d) **Right to Contribute:** You represent and warrant that you are the original author of the Contributions, or that you have sufficient rights to grant the rights conveyed by this section, and that your Contributions do not infringe upon the rights of any third party.
6. TERMINATION
This license will terminate automatically if Licensee breaches any term of this Agreement. Upon termination, Licensee must immediately cease all use of the Software and destroy all copies in their possession.
7. HIGH RISK DISCLAIMER AND LIMITATION OF LIABILITY
a) **AI RISKS:** THE SOFTWARE UTILIZES ARTIFICIAL INTELLIGENCE TO GENERATE CODE, EXECUTE COMMANDS, AND INTERACT WITH YOUR FILE SYSTEM. YOU ACKNOWLEDGE THAT AI SYSTEMS CAN BE UNPREDICTABLE, MAY GENERATE INCORRECT, INSECURE, OR DESTRUCTIVE CODE, AND MAY TAKE ACTIONS THAT COULD DAMAGE YOUR SYSTEM, FILES, OR HARDWARE.
b) **USE AT YOUR OWN RISK:** YOU AGREE THAT YOUR USE OF THE SOFTWARE IS SOLELY AT YOUR OWN RISK. THE CORE CONTRIBUTORS AND LICENSOR DO NOT GUARANTEE THAT THE SOFTWARE OR ANY CODE GENERATED BY IT WILL BE SAFE, BUG-FREE, OR FUNCTIONAL.
c) **NO WARRANTY:** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
d) **LIMITATION OF LIABILITY:** IN NO EVENT SHALL THE CORE CONTRIBUTORS, LICENSORS, OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE, INCLUDING BUT NOT LIMITED TO:
- DAMAGE TO HARDWARE OR COMPUTER SYSTEMS
- DATA LOSS OR CORRUPTION
- GENERATION OF BAD, VULNERABLE, OR MALICIOUS CODE
- FINANCIAL LOSSES
- BUSINESS INTERRUPTION
8. LICENSE AMENDMENTS
Any amendment, modification, or update to this License Agreement must be agreed upon unanimously by all Core Contributors. No changes to this Agreement shall be effective unless all Core Contributors have provided their written consent or approval through a unanimous vote.
9. CONTACT
For inquiries regarding this license or permissions for Monetization, please contact the Core Contributors through the official project channels:
- Agentic Jumpstart Discord: https://discord.gg/JUDWZDN3VT
- Website: https://automaker.app
- Email: automakerapp@gmail.com
Any permission for Monetization requires the unanimous written consent of all Core Contributors.
10. GOVERNING LAW
This Agreement shall be governed by and construed in accordance with the laws of the State of Tennessee, USA, without regard to conflict of law principles.
By using the Software, you acknowledge that you have read this Agreement, understand it, and agree to be bound by its terms and conditions.
---
Copyright (c) 2025 Automaker Core Contributors

223
README.md
View File

@@ -1,6 +1,71 @@
<p align="center">
<img src="apps/app/public/readme_logo.png" alt="Automaker Logo" height="80" />
</p>
> **[!TIP]**
>
> **Learn more about Agentic Coding!**
>
> Automaker itself was built by a group of engineers using AI and agentic coding techniques to build features faster than ever. By leveraging tools like Cursor IDE and Claude Code CLI, the team orchestrated AI agents to implement complex functionality in days instead of weeks.
>
> **Learn how:** Master these same techniques and workflows in the [Agentic Jumpstart course](https://agenticjumpstart.com/?utm=automaker).
# Automaker
Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion.
**Stop typing code. Start directing AI agents.**
<details open>
<summary><h2>Table of Contents</h2></summary>
- [What Makes Automaker Different?](#what-makes-automaker-different)
- [The Workflow](#the-workflow)
- [Powered by Claude Code](#powered-by-claude-code)
- [Why This Matters](#why-this-matters)
- [Security Disclaimer](#security-disclaimer)
- [Community & Support](#community--support)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [How to Run](#how-to-run)
- [Development Mode](#development-mode)
- [Electron Desktop App (Recommended)](#electron-desktop-app-recommended)
- [Web Browser Mode](#web-browser-mode)
- [Building for Production](#building-for-production)
- [Running Production Build](#running-production-build)
- [Testing](#testing)
- [Linting](#linting)
- [Authentication Options](#authentication-options)
- [Persistent Setup (Optional)](#persistent-setup-optional)
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Learn More](#learn-more)
- [License](#license)
</details>
Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them.
![Automaker UI](https://i.imgur.com/jdwKydM.png)
## What Makes Automaker Different?
Traditional development tools help you write code. Automaker helps you **orchestrate AI agents** to build entire features autonomously. Think of it as having a team of AI developers working for you—you define what needs to be built, and Automaker handles the implementation.
### The Workflow
1. **Add Features** - Describe features you want built (with text, images, or screenshots)
2. **Move to "In Progress"** - Automaker automatically assigns an AI agent to implement the feature
3. **Watch It Build** - See real-time progress as the agent writes code, runs tests, and makes changes
4. **Review & Verify** - Review the changes, run tests, and approve when ready
5. **Ship Faster** - Build entire applications in days, not weeks
### Powered by Claude Code
Automaker leverages the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commits—all while working in isolated git worktrees to keep your main branch safe.
### Why This Matters
The future of software development is **agentic coding**—where developers become architects directing AI agents rather than manual coders. Automaker puts this future in your hands today, letting you experience what it's like to build software 10x faster with AI agents handling the implementation while you focus on architecture and business logic.
---
@@ -18,56 +83,133 @@ Automaker is an autonomous AI development studio that helps you build software f
---
## Community & Support
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
- 🚀 Show off projects built with AI agents
- 🤝 Collaborate with other developers and contributors
👉 **Join the Discord:**
https://discord.gg/jjem7aEDKU
---
## Getting Started
**Step 1:** Clone this repository:
### Prerequisites
- Node.js 18+
- npm
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
### Quick Start
```bash
git clone git@github.com:AutoMaker-Org/automaker.git
# 1. Clone the repo
git clone https://github.com/AutoMaker-Org/automaker.git
cd automaker
```
**Step 2:** Install dependencies:
```bash
# 2. Install dependencies
npm install
# 3. Run Automaker (pick your mode)
npm run dev
# Then choose your run mode when prompted, or use specific commands below
```
**Step 3:** Run the Claude Code setup token command:
## How to Run
### Development Mode
Start Automaker in development mode:
```bash
claude setup-token
npm run dev
```
> **⚠️ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching.
This will prompt you to choose your run mode, or you can specify a mode directly:
**Step 4:** Export the Claude Code OAuth token in your shell:
```bash
export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"
```
**Step 5:** Start the development server:
#### Electron Desktop App (Recommended)
```bash
# Standard development mode
npm run dev:electron
# With DevTools open automatically
npm run dev:electron:debug
# For WSL (Windows Subsystem for Linux)
npm run dev:electron:wsl
# For WSL with GPU acceleration
npm run dev:electron:wsl:gpu
```
This will start both the Next.js development server and the Electron application.
**Step 6:** MOST IMPORANT: Run the Following after all is setup
#### Web Browser Mode
```bash
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
# Run in web browser (http://localhost:3007)
npm run dev:web
```
### Building for Production
```bash
# Build Next.js app
npm run build
# Build Electron app for distribution
npm run build:electron
```
### Running Production Build
```bash
# Start production Next.js server
npm run start
```
### Testing
```bash
# Run tests headless
npm run test
# Run tests with browser visible
npm run test:headed
```
### Linting
```bash
# Run ESLint
npm run lint
```
### Authentication Options
Automaker supports multiple authentication methods (in order of priority):
| Method | Environment Variable | Description |
| ---------------- | -------------------- | ------------------------------- |
| API Key (env) | `ANTHROPIC_API_KEY` | Anthropic API key |
| API Key (stored) | — | Anthropic API key stored in app |
### Persistent Setup (Optional)
Add to your `~/.bashrc` or `~/.zshrc`:
```bash
export ANTHROPIC_API_KEY="YOUR_API_KEY_HERE"
```
Then restart your terminal or run `source ~/.bashrc`.
## Features
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
@@ -105,4 +247,27 @@ To learn more about Next.js, take a look at the following resources:
## License
See [LICENSE](../LICENSE) for details.
This project is licensed under the **Automaker License Agreement**. See [LICENSE](LICENSE) for the full text.
**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:**
- By contributing to this repository, you grant the Core Contributors full, irrevocable rights to your code (copyright assignment).
**Core Contributors** (Cody Seibert (webdevcody), SuperComboGamer (SCG), Kacper Lachowicz (Shironex, Shirone), and Ben Scott (trueheads)) are granted perpetual, royalty-free licenses for any use, including monetization.

310
REFACTORING_CANDIDATES.md Normal file
View File

@@ -0,0 +1,310 @@
# 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,108 +0,0 @@
# Automaker
Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion.
---
> **[!CAUTION]**
>
> ## Security Disclaimer
>
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
>
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
>
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
>
> **[Read the full disclaimer](../DISCLAIMER.md)**
---
## Getting Started
**Step 1:** Clone this repository:
```bash
git clone git@github.com:AutoMaker-Org/automaker.git
cd automaker
```
**Step 2:** Install dependencies:
```bash
npm install
```
**Step 3:** Run the Claude Code setup token command:
```bash
claude setup-token
```
> **⚠️ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching.
**Step 4:** Export the Claude Code OAuth token in your shell:
```bash
export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"
```
**Step 5:** Start the development server:
```bash
npm run dev:electron
```
This will start both the Next.js development server and the Electron application.
**Step 6:** MOST IMPORANT: Run the Following after all is setup
```bash
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
echo "W"
```
## Features
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
- 🤖 **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress"
- 🧠 **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more
- 💭 **Extended Thinking** - Enable extended thinking modes for complex problem-solving
- 📡 **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented
- 🔍 **Project Analysis** - AI-powered project structure analysis to understand your codebase
- 📁 **Context Management** - Add context files to help AI agents understand your project better
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on your project
- 🖼️ **Image Support** - Attach images and screenshots to feature descriptions
-**Concurrent Processing** - Configure concurrency to process multiple features simultaneously
- 🧪 **Test Integration** - Automatic test running and verification for implemented features
- 🔀 **Git Integration** - View git diffs and track changes made by AI agents
- 👤 **AI Profiles** - Create and manage different AI agent profiles for various tasks
- 💬 **Chat History** - Keep track of conversations and interactions with AI agents
- ⌨️ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts
- 🎨 **Dark/Light Theme** - Beautiful UI with theme support
- 🖥️ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux
## Tech Stack
- [Next.js](https://nextjs.org) - React framework
- [Electron](https://www.electronjs.org/) - Desktop application framework
- [Tailwind CSS](https://tailwindcss.com/) - Styling
- [Zustand](https://zustand-demo.pmnd.rs/) - State management
- [dnd-kit](https://dndkit.com/) - Drag and drop functionality
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## License
See [LICENSE](../LICENSE) for details.

View File

@@ -1,684 +0,0 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const path = require("path");
const fs = require("fs/promises");
/**
* Agent Service - Runs Claude agents in the Electron main process
* This service survives Next.js restarts and maintains conversation state
*/
class AgentService {
constructor() {
this.sessions = new Map(); // sessionId -> { messages, isRunning, abortController }
this.stateDir = null; // Will be set when app is ready
}
/**
* Initialize the service with app data directory
*/
async initialize(appDataPath) {
this.stateDir = path.join(appDataPath, "agent-sessions");
this.metadataFile = path.join(appDataPath, "sessions-metadata.json");
await fs.mkdir(this.stateDir, { recursive: true });
console.log("[AgentService] Initialized with state dir:", this.stateDir);
}
/**
* Start or resume a conversation
*/
async startConversation({ sessionId, workingDirectory }) {
console.log("[AgentService] Starting conversation:", sessionId);
// Initialize session if it doesn't exist
if (!this.sessions.has(sessionId)) {
const messages = await this.loadSession(sessionId);
this.sessions.set(sessionId, {
messages,
isRunning: false,
abortController: null,
workingDirectory: workingDirectory || process.cwd(),
});
}
const session = this.sessions.get(sessionId);
return {
success: true,
messages: session.messages,
sessionId,
};
}
/**
* Send a message to the agent and stream responses
*/
async sendMessage({
sessionId,
message,
workingDirectory,
imagePaths,
sendToRenderer,
}) {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
if (session.isRunning) {
throw new Error("Agent is already processing a message");
}
// Read images from temp files and convert to base64 for storage
const images = [];
if (imagePaths && imagePaths.length > 0) {
const fs = require("fs/promises");
const path = require("path");
for (const imagePath of imagePaths) {
try {
const imageBuffer = await fs.readFile(imagePath);
const base64Data = imageBuffer.toString("base64");
// Determine media type from file extension
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || "image/png";
images.push({
data: base64Data,
mimeType: mediaType,
filename: path.basename(imagePath),
});
console.log(
`[AgentService] Loaded image from ${imagePath} for storage`
);
} catch (error) {
console.error(
`[AgentService] Failed to load image from ${imagePath}:`,
error
);
}
}
}
// Add user message to conversation with base64 images
const userMessage = {
id: this.generateId(),
role: "user",
content: message,
images: images.length > 0 ? images : undefined,
timestamp: new Date().toISOString(),
};
session.messages.push(userMessage);
session.isRunning = true;
session.abortController = new AbortController();
// Send initial user message to renderer
sendToRenderer({
type: "message",
message: userMessage,
});
// Save state with base64 images
await this.saveSession(sessionId, session.messages);
try {
// Configure Claude Agent SDK options
const options = {
// model: "claude-sonnet-4-20250514",
model: "claude-opus-4-5-20251101",
systemPrompt: this.getSystemPrompt(),
maxTurns: 20,
cwd: workingDirectory || session.workingDirectory,
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: session.abortController,
};
// Build prompt content with text and images
let promptContent = message;
// If there are images, create a content array
if (imagePaths && imagePaths.length > 0) {
const contentBlocks = [];
// Add text block
if (message && message.trim()) {
contentBlocks.push({
type: "text",
text: message,
});
}
// Add image blocks
const fs = require("fs");
for (const imagePath of imagePaths) {
try {
const imageBuffer = fs.readFileSync(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || "image/png";
contentBlocks.push({
type: "image",
source: {
type: "base64",
media_type: mediaType,
data: base64Data,
},
});
} catch (error) {
console.error(
`[AgentService] Failed to load image ${imagePath}:`,
error
);
}
}
// Use content blocks if we have images
if (
contentBlocks.length > 1 ||
(contentBlocks.length === 1 && contentBlocks[0].type === "image")
) {
promptContent = contentBlocks;
}
}
// Build payload for the SDK
const promptPayload = Array.isArray(promptContent)
? (async function* () {
yield {
type: "user",
session_id: "",
message: {
role: "user",
content: promptContent,
},
parent_tool_use_id: null,
};
})()
: promptContent;
// Send the query via the SDK (conversation state handled by the SDK)
const stream = query({ prompt: promptPayload, options });
let currentAssistantMessage = null;
let responseText = "";
const toolUses = [];
// Stream responses from the SDK
for await (const msg of stream) {
if (msg.type === "assistant") {
if (msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
// Create or update assistant message
if (!currentAssistantMessage) {
currentAssistantMessage = {
id: this.generateId(),
role: "assistant",
content: responseText,
timestamp: new Date().toISOString(),
};
session.messages.push(currentAssistantMessage);
} else {
currentAssistantMessage.content = responseText;
}
// Stream to renderer
sendToRenderer({
type: "stream",
messageId: currentAssistantMessage.id,
content: responseText,
isComplete: false,
});
} else if (block.type === "tool_use") {
const toolUse = {
name: block.name,
input: block.input,
};
toolUses.push(toolUse);
// Send tool use notification
sendToRenderer({
type: "tool_use",
tool: toolUse,
});
}
}
}
} else if (msg.type === "result") {
if (msg.subtype === "success" && msg.result) {
// Use the final result
if (currentAssistantMessage) {
currentAssistantMessage.content = msg.result;
responseText = msg.result;
}
}
// Send completion
sendToRenderer({
type: "complete",
messageId: currentAssistantMessage?.id,
content: responseText,
toolUses,
});
}
}
// Save final state
await this.saveSession(sessionId, session.messages);
session.isRunning = false;
session.abortController = null;
return {
success: true,
message: currentAssistantMessage,
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[AgentService] Query aborted");
session.isRunning = false;
session.abortController = null;
return { success: false, aborted: true };
}
console.error("[AgentService] Error:", error);
session.isRunning = false;
session.abortController = null;
// Add error message
const errorMessage = {
id: this.generateId(),
role: "assistant",
content: `Error: ${error.message}`,
timestamp: new Date().toISOString(),
isError: true,
};
session.messages.push(errorMessage);
await this.saveSession(sessionId, session.messages);
sendToRenderer({
type: "error",
error: error.message,
message: errorMessage,
});
throw error;
}
}
/**
* Get conversation history
*/
getHistory(sessionId) {
const session = this.sessions.get(sessionId);
if (!session) {
return { success: false, error: "Session not found" };
}
return {
success: true,
messages: session.messages,
isRunning: session.isRunning,
};
}
/**
* Stop current agent execution
*/
async stopExecution(sessionId) {
const session = this.sessions.get(sessionId);
if (!session) {
return { success: false, error: "Session not found" };
}
if (session.abortController) {
session.abortController.abort();
session.isRunning = false;
session.abortController = null;
}
return { success: true };
}
/**
* Clear conversation history
*/
async clearSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.messages = [];
session.isRunning = false;
await this.saveSession(sessionId, []);
}
return { success: true };
}
/**
* Load session from disk
*/
async loadSession(sessionId) {
if (!this.stateDir) return [];
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
try {
const data = await fs.readFile(sessionFile, "utf-8");
const parsed = JSON.parse(data);
console.log(
`[AgentService] Loaded ${parsed.length} messages for ${sessionId}`
);
return parsed;
} catch (error) {
// Session doesn't exist yet
return [];
}
}
/**
* Save session to disk
*/
async saveSession(sessionId, messages) {
if (!this.stateDir) return;
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
try {
await fs.writeFile(
sessionFile,
JSON.stringify(messages, null, 2),
"utf-8"
);
console.log(
`[AgentService] Saved ${messages.length} messages for ${sessionId}`
);
// Update timestamp
await this.updateSessionTimestamp(sessionId);
} catch (error) {
console.error("[AgentService] Failed to save session:", error);
}
}
/**
* Get system prompt
*/
getSystemPrompt() {
return `You are an AI assistant helping users build software. You are part of the Automaker application,
which is designed to help developers plan, design, and implement software projects autonomously.
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
Use the UpdateFeatureStatus tool to manage features, not direct file edits.
Your role is to:
- Help users define their project requirements and specifications
- Ask clarifying questions to better understand their needs
- Suggest technical approaches and architectures
- Guide them through the development process
- Be conversational and helpful
- Write, edit, and modify code files as requested
- Execute commands and tests
- Search and analyze the codebase
When discussing projects, help users think through:
- Core functionality and features
- Technical stack choices
- Data models and architecture
- User experience considerations
- Testing strategies
You have full access to the codebase and can:
- Read files to understand existing code
- Write new files
- Edit existing files
- Run bash commands
- Search for code patterns
- Execute tests and builds
IMPORTANT: When making file changes, be aware that the Next.js development server may restart.
This is normal and expected. Your conversation state is preserved across these restarts.`;
}
/**
* Generate unique ID
*/
generateId() {
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}
// ============================================================================
// Session Management
// ============================================================================
/**
* Load all session metadata
*/
async loadMetadata() {
if (!this.metadataFile) return {};
try {
const data = await fs.readFile(this.metadataFile, "utf-8");
return JSON.parse(data);
} catch (error) {
return {};
}
}
/**
* Save session metadata
*/
async saveMetadata(metadata) {
if (!this.metadataFile) return;
try {
await fs.writeFile(
this.metadataFile,
JSON.stringify(metadata, null, 2),
"utf-8"
);
} catch (error) {
console.error("[AgentService] Failed to save metadata:", error);
}
}
/**
* List all sessions
*/
async listSessions({ includeArchived = false } = {}) {
const metadata = await this.loadMetadata();
const sessions = [];
for (const [sessionId, meta] of Object.entries(metadata)) {
if (!includeArchived && meta.isArchived) continue;
const messages = await this.loadSession(sessionId);
const lastMessage = messages[messages.length - 1];
sessions.push({
id: sessionId,
name: meta.name || sessionId,
projectPath: meta.projectPath || "",
createdAt: meta.createdAt,
updatedAt: meta.updatedAt,
messageCount: messages.length,
isArchived: meta.isArchived || false,
tags: meta.tags || [],
preview: lastMessage?.content.substring(0, 100) || "",
});
}
// Sort by most recently updated
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
return sessions;
}
/**
* Create a new session
*/
async createSession({ name, projectPath, workingDirectory }) {
const sessionId = `session_${Date.now()}_${Math.random()
.toString(36)
.substring(2, 11)}`;
const metadata = await this.loadMetadata();
metadata[sessionId] = {
name,
projectPath,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isArchived: false,
tags: [],
};
await this.saveMetadata(metadata);
this.sessions.set(sessionId, {
messages: [],
isRunning: false,
abortController: null,
workingDirectory: workingDirectory || projectPath,
});
await this.saveSession(sessionId, []);
return {
success: true,
sessionId,
session: metadata[sessionId],
};
}
/**
* Update session metadata
*/
async updateSession({ sessionId, name, tags }) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
if (name !== undefined) metadata[sessionId].name = name;
if (tags !== undefined) metadata[sessionId].tags = tags;
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
return { success: true };
}
/**
* Archive a session
*/
async archiveSession(sessionId) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
metadata[sessionId].isArchived = true;
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
return { success: true };
}
/**
* Unarchive a session
*/
async unarchiveSession(sessionId) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
metadata[sessionId].isArchived = false;
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
return { success: true };
}
/**
* Delete a session permanently
*/
async deleteSession(sessionId) {
const metadata = await this.loadMetadata();
if (!metadata[sessionId]) {
return { success: false, error: "Session not found" };
}
// Remove from metadata
delete metadata[sessionId];
await this.saveMetadata(metadata);
// Remove from memory
this.sessions.delete(sessionId);
// Delete session file
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
try {
await fs.unlink(sessionFile);
} catch (error) {
console.warn("[AgentService] Failed to delete session file:", error);
}
return { success: true };
}
/**
* Update session metadata when messages change
*/
async updateSessionTimestamp(sessionId) {
const metadata = await this.loadMetadata();
if (metadata[sessionId]) {
metadata[sessionId].updatedAt = new Date().toISOString();
await this.saveMetadata(metadata);
}
}
}
// Export singleton instance
module.exports = new AgentService();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,398 +0,0 @@
const { contextBridge, ipcRenderer } = require("electron");
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld("electronAPI", {
// IPC test
ping: () => ipcRenderer.invoke("ping"),
// Shell APIs
openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
// Dialog APIs
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
// File system APIs
readFile: (filePath) => ipcRenderer.invoke("fs:readFile", filePath),
writeFile: (filePath, content) =>
ipcRenderer.invoke("fs:writeFile", filePath, content),
mkdir: (dirPath) => ipcRenderer.invoke("fs:mkdir", dirPath),
readdir: (dirPath) => ipcRenderer.invoke("fs:readdir", dirPath),
exists: (filePath) => ipcRenderer.invoke("fs:exists", filePath),
stat: (filePath) => ipcRenderer.invoke("fs:stat", filePath),
deleteFile: (filePath) => ipcRenderer.invoke("fs:deleteFile", filePath),
trashItem: (filePath) => ipcRenderer.invoke("fs:trashItem", filePath),
// App APIs
getPath: (name) => ipcRenderer.invoke("app:getPath", name),
saveImageToTemp: (data, filename, mimeType, projectPath) =>
ipcRenderer.invoke("app:saveImageToTemp", {
data,
filename,
mimeType,
projectPath,
}),
// Agent APIs
agent: {
// Start or resume a conversation
start: (sessionId, workingDirectory) =>
ipcRenderer.invoke("agent:start", { sessionId, workingDirectory }),
// Send a message to the agent
send: (sessionId, message, workingDirectory, imagePaths) =>
ipcRenderer.invoke("agent:send", {
sessionId,
message,
workingDirectory,
imagePaths,
}),
// Get conversation history
getHistory: (sessionId) =>
ipcRenderer.invoke("agent:getHistory", { sessionId }),
// Stop current execution
stop: (sessionId) => ipcRenderer.invoke("agent:stop", { sessionId }),
// Clear conversation
clear: (sessionId) => ipcRenderer.invoke("agent:clear", { sessionId }),
// Subscribe to streaming events
onStream: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("agent:stream", subscription);
// Return unsubscribe function
return () => ipcRenderer.removeListener("agent:stream", subscription);
},
},
// Session Management APIs
sessions: {
// List all sessions
list: (includeArchived) =>
ipcRenderer.invoke("sessions:list", { includeArchived }),
// Create a new session
create: (name, projectPath, workingDirectory) =>
ipcRenderer.invoke("sessions:create", {
name,
projectPath,
workingDirectory,
}),
// Update session metadata
update: (sessionId, name, tags) =>
ipcRenderer.invoke("sessions:update", { sessionId, name, tags }),
// Archive a session
archive: (sessionId) =>
ipcRenderer.invoke("sessions:archive", { sessionId }),
// Unarchive a session
unarchive: (sessionId) =>
ipcRenderer.invoke("sessions:unarchive", { sessionId }),
// Delete a session permanently
delete: (sessionId) => ipcRenderer.invoke("sessions:delete", { sessionId }),
},
// Auto Mode API
autoMode: {
// Start auto mode for a specific project
start: (projectPath, maxConcurrency) =>
ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }),
// Stop auto mode for a specific project
stop: (projectPath) => ipcRenderer.invoke("auto-mode:stop", { projectPath }),
// Get auto mode status (optionally for a specific project)
status: (projectPath) => ipcRenderer.invoke("auto-mode:status", { projectPath }),
// Run a specific feature
runFeature: (projectPath, featureId, useWorktrees) =>
ipcRenderer.invoke("auto-mode:run-feature", {
projectPath,
featureId,
useWorktrees,
}),
// Verify a specific feature by running its tests
verifyFeature: (projectPath, featureId) =>
ipcRenderer.invoke("auto-mode:verify-feature", {
projectPath,
featureId,
}),
// Resume a specific feature with previous context
resumeFeature: (projectPath, featureId) =>
ipcRenderer.invoke("auto-mode:resume-feature", {
projectPath,
featureId,
}),
// Check if context file exists for a feature
contextExists: (projectPath, featureId) =>
ipcRenderer.invoke("auto-mode:context-exists", {
projectPath,
featureId,
}),
// Analyze a new project - kicks off an agent to analyze codebase
analyzeProject: (projectPath) =>
ipcRenderer.invoke("auto-mode:analyze-project", { projectPath }),
// Stop a specific feature
stopFeature: (featureId) =>
ipcRenderer.invoke("auto-mode:stop-feature", { featureId }),
// Follow-up on a feature with additional prompt
followUpFeature: (projectPath, featureId, prompt, imagePaths) =>
ipcRenderer.invoke("auto-mode:follow-up-feature", {
projectPath,
featureId,
prompt,
imagePaths,
}),
// Commit changes for a feature
commitFeature: (projectPath, featureId) =>
ipcRenderer.invoke("auto-mode:commit-feature", {
projectPath,
featureId,
}),
// Listen for auto mode events
onEvent: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("auto-mode:event", subscription);
// Return unsubscribe function
return () => {
ipcRenderer.removeListener("auto-mode:event", subscription);
};
},
},
// Claude CLI Detection API
checkClaudeCli: () => ipcRenderer.invoke("claude:check-cli"),
// Codex CLI Detection API
checkCodexCli: () => ipcRenderer.invoke("codex:check-cli"),
// Model Management APIs
model: {
// Get all available models from all providers
getAvailable: () => ipcRenderer.invoke("model:get-available"),
// Check all provider installation status
checkProviders: () => ipcRenderer.invoke("model:check-providers"),
},
// OpenAI API
testOpenAIConnection: (apiKey) =>
ipcRenderer.invoke("openai:test-connection", { apiKey }),
// Worktree Management APIs
worktree: {
// Revert feature changes by removing the worktree
revertFeature: (projectPath, featureId) =>
ipcRenderer.invoke("worktree:revert-feature", { projectPath, featureId }),
// Merge feature worktree changes back to main branch
mergeFeature: (projectPath, featureId, options) =>
ipcRenderer.invoke("worktree:merge-feature", {
projectPath,
featureId,
options,
}),
// Get worktree info for a feature
getInfo: (projectPath, featureId) =>
ipcRenderer.invoke("worktree:get-info", { projectPath, featureId }),
// Get worktree status (changed files, commits)
getStatus: (projectPath, featureId) =>
ipcRenderer.invoke("worktree:get-status", { projectPath, featureId }),
// List all feature worktrees
list: (projectPath) => ipcRenderer.invoke("worktree:list", { projectPath }),
// Get file diffs for a feature worktree
getDiffs: (projectPath, featureId) =>
ipcRenderer.invoke("worktree:get-diffs", { projectPath, featureId }),
// Get diff for a specific file in a worktree
getFileDiff: (projectPath, featureId, filePath) =>
ipcRenderer.invoke("worktree:get-file-diff", {
projectPath,
featureId,
filePath,
}),
},
// Git Operations APIs (for non-worktree operations)
git: {
// Get file diffs for the main project
getDiffs: (projectPath) =>
ipcRenderer.invoke("git:get-diffs", { projectPath }),
// Get diff for a specific file in the main project
getFileDiff: (projectPath, filePath) =>
ipcRenderer.invoke("git:get-file-diff", { projectPath, filePath }),
},
// Feature Suggestions API
suggestions: {
// Generate feature suggestions
// suggestionType can be: "features", "refactoring", "security", "performance"
generate: (projectPath, suggestionType = "features") =>
ipcRenderer.invoke("suggestions:generate", { projectPath, suggestionType }),
// Stop generating suggestions
stop: () => ipcRenderer.invoke("suggestions:stop"),
// Get suggestions status
status: () => ipcRenderer.invoke("suggestions:status"),
// Listen for suggestions events
onEvent: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("suggestions:event", subscription);
// Return unsubscribe function
return () => {
ipcRenderer.removeListener("suggestions:event", subscription);
};
},
},
// Spec Regeneration API
specRegeneration: {
// Create initial app spec for a new project
create: (projectPath, projectOverview, generateFeatures = true) =>
ipcRenderer.invoke("spec-regeneration:create", {
projectPath,
projectOverview,
generateFeatures,
}),
// Regenerate the app spec
generate: (projectPath, projectDefinition) =>
ipcRenderer.invoke("spec-regeneration:generate", {
projectPath,
projectDefinition,
}),
// Stop regenerating spec
stop: () => ipcRenderer.invoke("spec-regeneration:stop"),
// Get regeneration status
status: () => ipcRenderer.invoke("spec-regeneration:status"),
// Listen for regeneration events
onEvent: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("spec-regeneration:event", subscription);
// Return unsubscribe function
return () => {
ipcRenderer.removeListener("spec-regeneration:event", subscription);
};
},
},
// Setup & CLI Management API
setup: {
// Get comprehensive Claude CLI status
getClaudeStatus: () => ipcRenderer.invoke("setup:claude-status"),
// Get comprehensive Codex CLI status
getCodexStatus: () => ipcRenderer.invoke("setup:codex-status"),
// Install Claude CLI
installClaude: () => ipcRenderer.invoke("setup:install-claude"),
// Install Codex CLI
installCodex: () => ipcRenderer.invoke("setup:install-codex"),
// Authenticate Claude CLI
authClaude: () => ipcRenderer.invoke("setup:auth-claude"),
// Authenticate Codex CLI with optional API key
authCodex: (apiKey) => ipcRenderer.invoke("setup:auth-codex", { apiKey }),
// Store API key securely
storeApiKey: (provider, apiKey) =>
ipcRenderer.invoke("setup:store-api-key", { provider, apiKey }),
// Get stored API keys status
getApiKeys: () => ipcRenderer.invoke("setup:get-api-keys"),
// Configure Codex MCP server for a project
configureCodexMcp: (projectPath) =>
ipcRenderer.invoke("setup:configure-codex-mcp", { projectPath }),
// Get platform information
getPlatform: () => ipcRenderer.invoke("setup:get-platform"),
// Listen for installation progress
onInstallProgress: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("setup:install-progress", subscription);
return () => {
ipcRenderer.removeListener("setup:install-progress", subscription);
};
},
// Listen for auth progress
onAuthProgress: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("setup:auth-progress", subscription);
return () => {
ipcRenderer.removeListener("setup:auth-progress", subscription);
};
},
},
// Features API
features: {
// Get all features for a project
getAll: (projectPath) =>
ipcRenderer.invoke("features:getAll", { projectPath }),
// Get a single feature by ID
get: (projectPath, featureId) =>
ipcRenderer.invoke("features:get", { projectPath, featureId }),
// Create a new feature
create: (projectPath, feature) =>
ipcRenderer.invoke("features:create", { projectPath, feature }),
// Update a feature (partial updates supported)
update: (projectPath, featureId, updates) =>
ipcRenderer.invoke("features:update", {
projectPath,
featureId,
updates,
}),
// Delete a feature and its folder
delete: (projectPath, featureId) =>
ipcRenderer.invoke("features:delete", { projectPath, featureId }),
// Get agent output for a feature
getAgentOutput: (projectPath, featureId) =>
ipcRenderer.invoke("features:getAgentOutput", { projectPath, featureId }),
},
// Running Agents API
runningAgents: {
// Get all running agents across all projects
getAll: () => ipcRenderer.invoke("running-agents:getAll"),
},
});
// Also expose a flag to detect if we're in Electron
contextBridge.exposeInMainWorld("isElectron", true);

View File

@@ -1,518 +0,0 @@
const { execSync, spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");
/**
* Claude CLI Detector
*
* Authentication options:
* 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token to the app
* 2. API Key (Pay-per-use): User provides their Anthropic API key directly
*/
class ClaudeCliDetector {
/**
* Check if Claude Code CLI is installed and accessible
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'none' }
*/
/**
* Try to get updated PATH from shell config files
* This helps detect CLI installations that modify shell config but haven't updated the current process PATH
*/
static getUpdatedPathFromShellConfig() {
const homeDir = os.homedir();
const shell = process.env.SHELL || "/bin/bash";
const shellName = path.basename(shell);
// Common shell config files
const configFiles = [];
if (shellName.includes("zsh")) {
configFiles.push(path.join(homeDir, ".zshrc"));
configFiles.push(path.join(homeDir, ".zshenv"));
configFiles.push(path.join(homeDir, ".zprofile"));
} else if (shellName.includes("bash")) {
configFiles.push(path.join(homeDir, ".bashrc"));
configFiles.push(path.join(homeDir, ".bash_profile"));
configFiles.push(path.join(homeDir, ".profile"));
}
// Also check common locations
const commonPaths = [
path.join(homeDir, ".local", "bin"),
path.join(homeDir, ".cargo", "bin"),
"/usr/local/bin",
"/opt/homebrew/bin",
path.join(homeDir, "bin"),
];
// Try to extract PATH additions from config files
for (const configFile of configFiles) {
if (fs.existsSync(configFile)) {
try {
const content = fs.readFileSync(configFile, "utf-8");
// Look for PATH exports that might include claude installation paths
const pathMatches = content.match(
/export\s+PATH=["']?([^"'\n]+)["']?/g
);
if (pathMatches) {
for (const match of pathMatches) {
const pathValue = match
.replace(/export\s+PATH=["']?/, "")
.replace(/["']?$/, "");
const paths = pathValue
.split(":")
.filter((p) => p && !p.includes("$"));
commonPaths.push(...paths);
}
}
} catch (error) {
// Ignore errors reading config files
}
}
}
return [...new Set(commonPaths)]; // Remove duplicates
}
static detectClaudeInstallation() {
console.log("[ClaudeCliDetector] Detecting Claude installation...");
try {
// Method 1: Check if 'claude' command is in PATH (Unix)
if (process.platform !== "win32") {
try {
const claudePath = execSync("which claude 2>/dev/null", {
encoding: "utf-8",
}).trim();
if (claudePath) {
const version = this.getClaudeVersion(claudePath);
console.log(
"[ClaudeCliDetector] Found claude at:",
claudePath,
"version:",
version
);
return {
installed: true,
path: claudePath,
version: version,
method: "cli",
};
}
} catch (error) {
// CLI not in PATH, continue checking other locations
}
}
// Method 2: Check Windows path
if (process.platform === "win32") {
try {
const claudePath = execSync("where claude 2>nul", {
encoding: "utf-8",
})
.trim()
.split("\n")[0];
if (claudePath) {
const version = this.getClaudeVersion(claudePath);
console.log(
"[ClaudeCliDetector] Found claude at:",
claudePath,
"version:",
version
);
return {
installed: true,
path: claudePath,
version: version,
method: "cli",
};
}
} catch (error) {
// Not found on Windows
}
}
// Method 3: Check for local installation
const localClaudePath = path.join(
os.homedir(),
".claude",
"local",
"claude"
);
if (fs.existsSync(localClaudePath)) {
const version = this.getClaudeVersion(localClaudePath);
console.log(
"[ClaudeCliDetector] Found local claude at:",
localClaudePath,
"version:",
version
);
return {
installed: true,
path: localClaudePath,
version: version,
method: "cli-local",
};
}
// Method 4: Check common installation locations (including those from shell config)
const commonPaths = this.getUpdatedPathFromShellConfig();
const binaryNames = ["claude", "claude-code"];
for (const basePath of commonPaths) {
for (const binaryName of binaryNames) {
const claudePath = path.join(basePath, binaryName);
if (fs.existsSync(claudePath)) {
try {
const version = this.getClaudeVersion(claudePath);
console.log(
"[ClaudeCliDetector] Found claude at:",
claudePath,
"version:",
version
);
return {
installed: true,
path: claudePath,
version: version,
method: "cli",
};
} catch (error) {
// File exists but can't get version, might not be executable
}
}
}
}
// Method 5: Try to source shell config and check PATH again (for Unix)
if (process.platform !== "win32") {
try {
const shell = process.env.SHELL || "/bin/bash";
const shellName = path.basename(shell);
const homeDir = os.homedir();
let sourceCmd = "";
if (shellName.includes("zsh")) {
sourceCmd = `source ${homeDir}/.zshrc 2>/dev/null && which claude`;
} else if (shellName.includes("bash")) {
sourceCmd = `source ${homeDir}/.bashrc 2>/dev/null && which claude`;
}
if (sourceCmd) {
const claudePath = execSync(`bash -c "${sourceCmd}"`, {
encoding: "utf-8",
timeout: 2000,
}).trim();
if (claudePath && claudePath.startsWith("/")) {
const version = this.getClaudeVersion(claudePath);
console.log(
"[ClaudeCliDetector] Found claude via shell config at:",
claudePath,
"version:",
version
);
return {
installed: true,
path: claudePath,
version: version,
method: "cli",
};
}
}
} catch (error) {
// Failed to source shell config or find claude
}
}
console.log("[ClaudeCliDetector] Claude CLI not found");
return {
installed: false,
path: null,
version: null,
method: "none",
};
} catch (error) {
console.error(
"[ClaudeCliDetector] Error detecting Claude installation:",
error
);
return {
installed: false,
path: null,
version: null,
method: "none",
error: error.message,
};
}
}
/**
* Get Claude CLI version
* @param {string} claudePath Path to claude executable
* @returns {string|null} Version string or null
*/
static getClaudeVersion(claudePath) {
try {
const version = execSync(`"${claudePath}" --version 2>/dev/null`, {
encoding: "utf-8",
timeout: 5000,
}).trim();
return version || null;
} catch (error) {
return null;
}
}
/**
* Get authentication status
* Checks for:
* 1. OAuth token stored in app's credentials (from `claude setup-token`)
* 2. API key stored in app's credentials
* 3. API key in environment variable
*
* @param {string} appCredentialsPath Path to app's credentials.json
* @returns {Object} Authentication status
*/
static getAuthStatus(appCredentialsPath) {
console.log("[ClaudeCliDetector] Checking auth status...");
const envApiKey = process.env.ANTHROPIC_API_KEY;
console.log("[ClaudeCliDetector] Env ANTHROPIC_API_KEY:", !!envApiKey);
// Check app's stored credentials
let storedOAuthToken = null;
let storedApiKey = null;
if (appCredentialsPath && fs.existsSync(appCredentialsPath)) {
try {
const content = fs.readFileSync(appCredentialsPath, "utf-8");
const credentials = JSON.parse(content);
storedOAuthToken = credentials.anthropic_oauth_token || null;
storedApiKey =
credentials.anthropic || credentials.anthropic_api_key || null;
console.log("[ClaudeCliDetector] App credentials:", {
hasOAuthToken: !!storedOAuthToken,
hasApiKey: !!storedApiKey,
});
} catch (error) {
console.error(
"[ClaudeCliDetector] Error reading app credentials:",
error
);
}
}
// Determine authentication method
// Priority: Stored OAuth Token > Stored API Key > Env API Key
let authenticated = false;
let method = "none";
if (storedOAuthToken) {
authenticated = true;
method = "oauth_token";
console.log(
"[ClaudeCliDetector] Using stored OAuth token (subscription)"
);
} else if (storedApiKey) {
authenticated = true;
method = "api_key";
console.log("[ClaudeCliDetector] Using stored API key");
} else if (envApiKey) {
authenticated = true;
method = "api_key_env";
console.log("[ClaudeCliDetector] Using environment API key");
} else {
console.log("[ClaudeCliDetector] No authentication found");
}
const result = {
authenticated,
method,
hasStoredOAuthToken: !!storedOAuthToken,
hasStoredApiKey: !!storedApiKey,
hasEnvApiKey: !!envApiKey,
};
console.log("[ClaudeCliDetector] Auth status result:", result);
return result;
}
/**
* Get installation info (installation status only, no auth)
* @returns {Object} Installation info with status property
*/
static getInstallationInfo() {
const installation = this.detectClaudeInstallation();
return {
status: installation.installed ? "installed" : "not_installed",
installed: installation.installed,
path: installation.path,
version: installation.version,
method: installation.method,
};
}
/**
* Get full status including installation and auth
* @param {string} appCredentialsPath Path to app's credentials.json
* @returns {Object} Full status
*/
static getFullStatus(appCredentialsPath) {
const installation = this.detectClaudeInstallation();
const auth = this.getAuthStatus(appCredentialsPath);
return {
success: true,
status: installation.installed ? "installed" : "not_installed",
installed: installation.installed,
path: installation.path,
version: installation.version,
method: installation.method,
auth,
};
}
/**
* Get installation info and recommendations
* @returns {Object} Installation status and recommendations
*/
static getInstallationInfo() {
const detection = this.detectClaudeInstallation();
if (detection.installed) {
return {
status: 'installed',
method: detection.method,
version: detection.version,
path: detection.path,
recommendation: 'Claude Code CLI is ready for ultrathink'
};
}
return {
status: 'not_installed',
recommendation: 'Install Claude Code CLI for optimal ultrathink performance',
installCommands: this.getInstallCommands()
};
}
/**
* Get installation commands for different platforms
* @returns {Object} Installation commands
*/
static getInstallCommands() {
return {
macos: "curl -fsSL https://claude.ai/install.sh | bash",
windows: "irm https://claude.ai/install.ps1 | iex",
linux: "curl -fsSL https://claude.ai/install.sh | bash",
};
}
/**
* Install Claude CLI using the official script
* @param {Function} onProgress Callback for progress updates
* @returns {Promise<Object>} Installation result
*/
static async installCli(onProgress) {
return new Promise((resolve, reject) => {
const platform = process.platform;
let command, args;
if (platform === "win32") {
command = "powershell";
args = ["-Command", "irm https://claude.ai/install.ps1 | iex"];
} else {
command = "bash";
args = ["-c", "curl -fsSL https://claude.ai/install.sh | bash"];
}
console.log("[ClaudeCliDetector] Installing Claude CLI...");
const proc = spawn(command, args, {
stdio: ["pipe", "pipe", "pipe"],
shell: false,
});
let output = "";
let errorOutput = "";
proc.stdout.on("data", (data) => {
const text = data.toString();
output += text;
if (onProgress) {
onProgress({ type: "stdout", data: text });
}
});
proc.stderr.on("data", (data) => {
const text = data.toString();
errorOutput += text;
if (onProgress) {
onProgress({ type: "stderr", data: text });
}
});
proc.on("close", (code) => {
if (code === 0) {
console.log(
"[ClaudeCliDetector] Installation completed successfully"
);
resolve({
success: true,
output,
message: "Claude CLI installed successfully",
});
} else {
console.error(
"[ClaudeCliDetector] Installation failed with code:",
code
);
reject({
success: false,
error: errorOutput || `Installation failed with code ${code}`,
output,
});
}
});
proc.on("error", (error) => {
console.error("[ClaudeCliDetector] Installation error:", error);
reject({
success: false,
error: error.message,
output,
});
});
});
}
/**
* Get instructions for setup-token command
* @returns {Object} Setup token instructions
*/
static getSetupTokenInstructions() {
const detection = this.detectClaudeInstallation();
if (!detection.installed) {
return {
success: false,
error: "Claude CLI is not installed. Please install it first.",
installCommands: this.getInstallCommands(),
};
}
return {
success: true,
command: "claude setup-token",
instructions: [
"1. Open your terminal",
"2. Run: claude setup-token",
"3. Follow the prompts to authenticate",
"4. Copy the token that is displayed",
"5. Paste the token in the field below",
],
note: "This token is from your Claude subscription and allows you to use Claude without API charges.",
};
}
}
module.exports = ClaudeCliDetector;

View File

@@ -1,675 +0,0 @@
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
/**
* Codex CLI Detector - Checks if OpenAI Codex CLI is installed
*
* Codex CLI is OpenAI's agent CLI tool that allows users to use
* GPT-5.1 Codex models (gpt-5.1-codex-max, gpt-5.1-codex, etc.)
* for code generation and agentic tasks.
*/
class CodexCliDetector {
/**
* Get the path to Codex config directory
* @returns {string} Path to .codex directory
*/
static getConfigDir() {
return path.join(os.homedir(), '.codex');
}
/**
* Get the path to Codex auth file
* @returns {string} Path to auth.json
*/
static getAuthPath() {
return path.join(this.getConfigDir(), 'auth.json');
}
/**
* Check Codex authentication status
* @returns {Object} Authentication status
*/
static checkAuth() {
console.log('[CodexCliDetector] Checking auth status...');
try {
const authPath = this.getAuthPath();
const envApiKey = process.env.OPENAI_API_KEY;
console.log('[CodexCliDetector] Auth path:', authPath);
console.log('[CodexCliDetector] Has env API key:', !!envApiKey);
// First, try to verify authentication using codex CLI command if available
try {
const detection = this.detectCodexInstallation();
if (detection.installed) {
try {
// Use 'codex login status' to verify authentication
const statusOutput = execSync(`"${detection.path || 'codex'}" login status 2>/dev/null`, {
encoding: 'utf-8',
timeout: 5000
});
// If command succeeds and shows logged in status
if (statusOutput && (statusOutput.includes('Logged in') || statusOutput.includes('Authenticated'))) {
const result = {
authenticated: true,
method: 'cli_verified',
hasAuthFile: fs.existsSync(authPath),
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (cli_verified):', result);
return result;
}
} catch (statusError) {
// status command failed, continue with file-based check
}
}
} catch (verifyError) {
// CLI verification failed, continue with file-based check
}
// Check if auth file exists
if (fs.existsSync(authPath)) {
console.log('[CodexCliDetector] Auth file exists, reading content...');
let auth = null;
try {
const content = fs.readFileSync(authPath, 'utf-8');
auth = JSON.parse(content);
console.log('[CodexCliDetector] Auth file content keys:', Object.keys(auth));
console.log('[CodexCliDetector] Auth file has token object:', !!auth.token);
if (auth.token) {
console.log('[CodexCliDetector] Token object keys:', Object.keys(auth.token));
}
// Check for token object structure (from codex auth login)
// Structure: { token: { Id_token, access_token, refresh_token }, last_refresh: ... }
if (auth.token && typeof auth.token === 'object') {
const token = auth.token;
if (token.Id_token || token.access_token || token.refresh_token || token.id_token) {
const result = {
authenticated: true,
method: 'cli_tokens', // Distinguish token-based auth from API key auth
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (cli_tokens):', result);
return result;
}
}
// Check for tokens at root level (alternative structure)
if (auth.access_token || auth.refresh_token || auth.Id_token || auth.id_token) {
const result = {
authenticated: true,
method: 'cli_tokens', // These are tokens, not API keys
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (cli_tokens - root level):', result);
return result;
}
// Check for various possible API key fields that codex might use
// Note: access_token is NOT an API key, it's a token, so we check for it above
if (auth.api_key || auth.openai_api_key || auth.apiKey) {
const result = {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (auth_file - API key):', result);
return result;
}
} catch (error) {
console.error('[CodexCliDetector] Error reading/parsing auth file:', error.message);
// If we can't parse the file, we can't determine auth status
return {
authenticated: false,
method: 'none',
hasAuthFile: false,
hasEnvKey: !!envApiKey,
authPath
};
}
// Also check if the file has any meaningful content (non-empty object)
// This is a fallback - but we should still try to detect if it's tokens
if (!auth) {
// File exists but couldn't be parsed
return {
authenticated: false,
method: 'none',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
const keys = Object.keys(auth);
console.log('[CodexCliDetector] File has content, keys:', keys);
if (keys.length > 0) {
// Check again for tokens in case we missed them (maybe nested differently)
const hasTokens = keys.some(key =>
key.toLowerCase().includes('token') ||
key.toLowerCase().includes('refresh') ||
(auth[key] && typeof auth[key] === 'object' && (
auth[key].access_token || auth[key].refresh_token || auth[key].Id_token || auth[key].id_token
))
);
if (hasTokens) {
const result = {
authenticated: true,
method: 'cli_tokens',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (cli_tokens - fallback detection):', result);
return result;
}
// File exists and has content, likely authenticated
// Try to verify by checking if codex command works
try {
const detection = this.detectCodexInstallation();
if (detection.installed) {
// Try to verify auth by running a simple command
try {
execSync(`"${detection.path || 'codex'}" --version 2>/dev/null`, {
encoding: 'utf-8',
timeout: 3000
});
// If command succeeds, assume authenticated
// But check if it's likely tokens vs API key based on file structure
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
const result = {
authenticated: true,
method: likelyTokens ? 'cli_tokens' : 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (verified via CLI, method:', result.method, '):', result);
return result;
} catch (cmdError) {
// Command failed, but file exists - might still be authenticated
// Check if it's likely tokens
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
const result = {
authenticated: true,
method: likelyTokens ? 'cli_tokens' : 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (file exists, method:', result.method, '):', result);
return result;
}
}
} catch (verifyError) {
// Verification failed, but file exists with content
// Check if it's likely tokens
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
const result = {
authenticated: true,
method: likelyTokens ? 'cli_tokens' : 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
console.log('[CodexCliDetector] Auth result (fallback, method:', result.method, '):', result);
return result;
}
}
}
// Check environment variable
if (envApiKey) {
const result = {
authenticated: true,
method: 'env_var',
hasAuthFile: false,
hasEnvKey: true,
authPath
};
console.log('[CodexCliDetector] Auth result (env_var):', result);
return result;
}
// If auth file exists but we didn't find standard keys,
// check if codex CLI is installed and try to verify auth
if (fs.existsSync(authPath)) {
try {
const detection = this.detectCodexInstallation();
if (detection.installed) {
// Auth file exists and CLI is installed - likely authenticated
// The file existing is a good indicator that login was successful
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
} catch (verifyError) {
// Verification attempt failed, but file exists
// Assume authenticated if file exists
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
}
const result = {
authenticated: false,
method: 'none',
hasAuthFile: false,
hasEnvKey: false,
authPath
};
console.log('[CodexCliDetector] Auth result (not authenticated):', result);
return result;
} catch (error) {
console.error('[CodexCliDetector] Error checking auth:', error);
const result = {
authenticated: false,
method: 'none',
error: error.message
};
console.log('[CodexCliDetector] Auth result (error):', result);
return result;
}
}
/**
* Check if Codex CLI is installed and accessible
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'npm'|'brew'|'none' }
*/
static detectCodexInstallation() {
try {
// Method 1: Check if 'codex' command is in PATH
try {
const codexPath = execSync('which codex 2>/dev/null', { encoding: 'utf-8' }).trim();
if (codexPath) {
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version,
method: 'cli'
};
}
} catch (error) {
// CLI not in PATH, continue checking other methods
}
// Method 2: Check for npm global installation
try {
const npmListOutput = execSync('npm list -g @openai/codex --depth=0 2>/dev/null', { encoding: 'utf-8' });
if (npmListOutput && npmListOutput.includes('@openai/codex')) {
// Get the path from npm bin
const npmBinPath = execSync('npm bin -g', { encoding: 'utf-8' }).trim();
const codexPath = path.join(npmBinPath, 'codex');
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version,
method: 'npm'
};
}
} catch (error) {
// npm global not found
}
// Method 3: Check for Homebrew installation on macOS
if (process.platform === 'darwin') {
try {
const brewList = execSync('brew list --formula 2>/dev/null', { encoding: 'utf-8' });
if (brewList.includes('codex')) {
const brewPrefixOutput = execSync('brew --prefix codex 2>/dev/null', { encoding: 'utf-8' }).trim();
const codexPath = path.join(brewPrefixOutput, 'bin', 'codex');
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version,
method: 'brew'
};
}
} catch (error) {
// Homebrew not found or codex not installed via brew
}
}
// Method 4: Check Windows path
if (process.platform === 'win32') {
try {
const codexPath = execSync('where codex 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0];
if (codexPath) {
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version,
method: 'cli'
};
}
} catch (error) {
// Not found on Windows
}
}
// Method 5: Check common installation paths
const commonPaths = [
path.join(os.homedir(), '.local', 'bin', 'codex'),
path.join(os.homedir(), '.npm-global', 'bin', 'codex'),
'/usr/local/bin/codex',
'/opt/homebrew/bin/codex',
];
for (const checkPath of commonPaths) {
if (fs.existsSync(checkPath)) {
const version = this.getCodexVersion(checkPath);
return {
installed: true,
path: checkPath,
version: version,
method: 'cli'
};
}
}
// Method 6: Check if OPENAI_API_KEY is set (can use Codex API directly)
if (process.env.OPENAI_API_KEY) {
return {
installed: false,
path: null,
version: null,
method: 'api-key-only',
hasApiKey: true
};
}
return {
installed: false,
path: null,
version: null,
method: 'none'
};
} catch (error) {
console.error('[CodexCliDetector] Error detecting Codex installation:', error);
return {
installed: false,
path: null,
version: null,
method: 'none',
error: error.message
};
}
}
/**
* Get Codex CLI version from executable path
* @param {string} codexPath Path to codex executable
* @returns {string|null} Version string or null
*/
static getCodexVersion(codexPath) {
try {
const version = execSync(`"${codexPath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim();
return version || null;
} catch (error) {
return null;
}
}
/**
* Get installation info and recommendations
* @returns {Object} Installation status and recommendations
*/
static getInstallationInfo() {
const detection = this.detectCodexInstallation();
if (detection.installed) {
return {
status: 'installed',
method: detection.method,
version: detection.version,
path: detection.path,
recommendation: detection.method === 'cli'
? 'Using Codex CLI - ready for GPT-5.1 Codex models'
: `Using Codex CLI via ${detection.method} - ready for GPT-5.1 Codex models`
};
}
// Not installed but has API key
if (detection.method === 'api-key-only') {
return {
status: 'api_key_only',
method: 'api-key-only',
recommendation: 'OPENAI_API_KEY detected but Codex CLI not installed. Install Codex CLI for full agentic capabilities.',
installCommands: this.getInstallCommands()
};
}
return {
status: 'not_installed',
recommendation: 'Install OpenAI Codex CLI to use GPT-5.1 Codex models for agentic tasks',
installCommands: this.getInstallCommands()
};
}
/**
* Get installation commands for different platforms
* @returns {Object} Installation commands by platform
*/
static getInstallCommands() {
return {
npm: 'npm install -g @openai/codex@latest',
macos: 'brew install codex',
linux: 'npm install -g @openai/codex@latest',
windows: 'npm install -g @openai/codex@latest'
};
}
/**
* Check if Codex CLI supports a specific model
* @param {string} model Model name to check
* @returns {boolean} Whether the model is supported
*/
static isModelSupported(model) {
const supportedModels = [
'gpt-5.1-codex-max',
'gpt-5.1-codex',
'gpt-5.1-codex-mini',
'gpt-5.1'
];
return supportedModels.includes(model);
}
/**
* Get default model for Codex CLI
* @returns {string} Default model name
*/
static getDefaultModel() {
return 'gpt-5.1-codex-max';
}
/**
* Get comprehensive installation info including auth status
* @returns {Object} Full status object
*/
static getFullStatus() {
const installation = this.detectCodexInstallation();
const auth = this.checkAuth();
const info = this.getInstallationInfo();
return {
...info,
auth,
installation
};
}
/**
* Install Codex CLI using npm
* @param {Function} onProgress Callback for progress updates
* @returns {Promise<Object>} Installation result
*/
static async installCli(onProgress) {
return new Promise((resolve, reject) => {
const command = 'npm';
const args = ['install', '-g', '@openai/codex@latest'];
const proc = spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
let output = '';
let errorOutput = '';
proc.stdout.on('data', (data) => {
const text = data.toString();
output += text;
if (onProgress) {
onProgress({ type: 'stdout', data: text });
}
});
proc.stderr.on('data', (data) => {
const text = data.toString();
errorOutput += text;
// npm often outputs progress to stderr
if (onProgress) {
onProgress({ type: 'stderr', data: text });
}
});
proc.on('close', (code) => {
if (code === 0) {
resolve({
success: true,
output,
message: 'Codex CLI installed successfully'
});
} else {
reject({
success: false,
error: errorOutput || `Installation failed with code ${code}`,
output
});
}
});
proc.on('error', (error) => {
reject({
success: false,
error: error.message,
output
});
});
});
}
/**
* Authenticate Codex CLI - opens browser for OAuth or stores API key
* @param {string} apiKey Optional API key to store
* @param {Function} onProgress Callback for progress updates
* @returns {Promise<Object>} Authentication result
*/
static async authenticate(apiKey, onProgress) {
return new Promise((resolve, reject) => {
const detection = this.detectCodexInstallation();
if (!detection.installed) {
reject({
success: false,
error: 'Codex CLI is not installed'
});
return;
}
const codexPath = detection.path || 'codex';
if (apiKey) {
// Store API key directly using codex auth command
const proc = spawn(codexPath, ['auth', 'login', '--api-key', apiKey], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: false
});
let output = '';
let errorOutput = '';
proc.stdout.on('data', (data) => {
const text = data.toString();
output += text;
if (onProgress) {
onProgress({ type: 'stdout', data: text });
}
});
proc.stderr.on('data', (data) => {
const text = data.toString();
errorOutput += text;
if (onProgress) {
onProgress({ type: 'stderr', data: text });
}
});
proc.on('close', (code) => {
if (code === 0) {
resolve({
success: true,
output,
message: 'Codex CLI authenticated successfully'
});
} else {
reject({
success: false,
error: errorOutput || `Authentication failed with code ${code}`,
output
});
}
});
proc.on('error', (error) => {
reject({
success: false,
error: error.message,
output
});
});
} else {
// Require manual authentication
if (onProgress) {
onProgress({
type: 'info',
data: 'Please run the following command in your terminal to authenticate:\n\ncodex auth login\n\nThen return here to continue setup.'
});
}
resolve({
success: true,
requiresManualAuth: true,
command: `${codexPath} auth login`,
message: 'Please authenticate Codex CLI manually'
});
}
});
}
}
module.exports = CodexCliDetector;

View File

@@ -1,353 +0,0 @@
/**
* Codex TOML Configuration Manager
*
* Manages Codex CLI's TOML configuration file to add/update MCP server settings.
* Codex CLI looks for config at:
* - ~/.codex/config.toml (user-level)
* - .codex/config.toml (project-level, takes precedence)
*/
const fs = require('fs/promises');
const path = require('path');
const os = require('os');
class CodexConfigManager {
constructor() {
this.userConfigPath = path.join(os.homedir(), '.codex', 'config.toml');
this.projectConfigPath = null; // Will be set per project
}
/**
* Set the project path for project-level config
*/
setProjectPath(projectPath) {
this.projectConfigPath = path.join(projectPath, '.codex', 'config.toml');
}
/**
* Get the effective config path (project-level if exists, otherwise user-level)
*/
async getConfigPath() {
if (this.projectConfigPath) {
try {
await fs.access(this.projectConfigPath);
return this.projectConfigPath;
} catch (e) {
// Project config doesn't exist, fall back to user config
}
}
// Ensure user config directory exists
const userConfigDir = path.dirname(this.userConfigPath);
try {
await fs.mkdir(userConfigDir, { recursive: true });
} catch (e) {
// Directory might already exist
}
return this.userConfigPath;
}
/**
* Read existing TOML config (simple parser for our needs)
*/
async readConfig(configPath) {
try {
const content = await fs.readFile(configPath, 'utf-8');
return this.parseToml(content);
} catch (e) {
if (e.code === 'ENOENT') {
return {};
}
throw e;
}
}
/**
* Simple TOML parser for our specific use case
* This is a minimal parser that handles the MCP server config structure
*/
parseToml(content) {
const config = {};
let currentSection = null;
let currentSubsection = null;
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
// Section header: [section]
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
const sectionName = sectionMatch[1];
const parts = sectionName.split('.');
if (parts.length === 1) {
currentSection = parts[0];
currentSubsection = null;
if (!config[currentSection]) {
config[currentSection] = {};
}
} else if (parts.length === 2) {
currentSection = parts[0];
currentSubsection = parts[1];
if (!config[currentSection]) {
config[currentSection] = {};
}
if (!config[currentSection][currentSubsection]) {
config[currentSection][currentSubsection] = {};
}
}
continue;
}
// Key-value pair: key = value
const kvMatch = trimmed.match(/^([^=]+)=(.+)$/);
if (kvMatch) {
const key = kvMatch[1].trim();
let value = kvMatch[2].trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
// Parse boolean
if (value === 'true') value = true;
else if (value === 'false') value = false;
// Parse number
else if (/^-?\d+$/.test(value)) value = parseInt(value, 10);
else if (/^-?\d+\.\d+$/.test(value)) value = parseFloat(value);
if (currentSubsection) {
if (!config[currentSection][currentSubsection]) {
config[currentSection][currentSubsection] = {};
}
config[currentSection][currentSubsection][key] = value;
} else if (currentSection) {
if (!config[currentSection]) {
config[currentSection] = {};
}
config[currentSection][key] = value;
} else {
config[key] = value;
}
}
}
return config;
}
/**
* Convert config object back to TOML format
*/
stringifyToml(config, indent = 0) {
const indentStr = ' '.repeat(indent);
let result = '';
for (const [key, value] of Object.entries(config)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Section
result += `${indentStr}[${key}]\n`;
result += this.stringifyToml(value, indent);
} else {
// Key-value
let valueStr = value;
if (typeof value === 'string') {
// Escape quotes and wrap in quotes if needed
if (value.includes('"') || value.includes("'") || value.includes(' ')) {
valueStr = `"${value.replace(/"/g, '\\"')}"`;
}
} else if (typeof value === 'boolean') {
valueStr = value.toString();
}
result += `${indentStr}${key} = ${valueStr}\n`;
}
}
return result;
}
/**
* Configure the automaker-tools MCP server
*/
async configureMcpServer(projectPath, mcpServerScriptPath) {
this.setProjectPath(projectPath);
const configPath = await this.getConfigPath();
// Read existing config
const config = await this.readConfig(configPath);
// Ensure mcp_servers section exists
if (!config.mcp_servers) {
config.mcp_servers = {};
}
// Configure automaker-tools server
config.mcp_servers['automaker-tools'] = {
command: 'node',
args: [mcpServerScriptPath],
env: {
AUTOMAKER_PROJECT_PATH: projectPath
},
startup_timeout_sec: 10,
tool_timeout_sec: 60,
enabled_tools: ['UpdateFeatureStatus']
};
// Ensure experimental_use_rmcp_client is enabled (if needed)
if (!config.experimental_use_rmcp_client) {
config.experimental_use_rmcp_client = true;
}
// Write config back
await this.writeConfig(configPath, config);
console.log(`[CodexConfigManager] Configured automaker-tools MCP server in ${configPath}`);
return configPath;
}
/**
* Write config to TOML file
*/
async writeConfig(configPath, config) {
let content = '';
// Write top-level keys first (preserve existing non-MCP config)
for (const [key, value] of Object.entries(config)) {
if (key === 'mcp_servers' || key === 'experimental_use_rmcp_client') {
continue; // Handle these separately
}
if (typeof value !== 'object') {
content += `${key} = ${this.formatValue(value)}\n`;
}
}
// Write experimental flag if enabled
if (config.experimental_use_rmcp_client) {
if (content && !content.endsWith('\n\n')) {
content += '\n';
}
content += `experimental_use_rmcp_client = true\n`;
}
// Write mcp_servers section
if (config.mcp_servers && Object.keys(config.mcp_servers).length > 0) {
if (content && !content.endsWith('\n\n')) {
content += '\n';
}
for (const [serverName, serverConfig] of Object.entries(config.mcp_servers)) {
content += `\n[mcp_servers.${serverName}]\n`;
// Write command first
if (serverConfig.command) {
content += `command = "${this.escapeTomlString(serverConfig.command)}"\n`;
}
// Write args
if (serverConfig.args && Array.isArray(serverConfig.args)) {
const argsStr = serverConfig.args.map(a => `"${this.escapeTomlString(a)}"`).join(', ');
content += `args = [${argsStr}]\n`;
}
// Write timeouts (must be before env subsection)
if (serverConfig.startup_timeout_sec !== undefined) {
content += `startup_timeout_sec = ${serverConfig.startup_timeout_sec}\n`;
}
if (serverConfig.tool_timeout_sec !== undefined) {
content += `tool_timeout_sec = ${serverConfig.tool_timeout_sec}\n`;
}
// Write enabled_tools (must be before env subsection - at server level, not env level)
if (serverConfig.enabled_tools && Array.isArray(serverConfig.enabled_tools)) {
const toolsStr = serverConfig.enabled_tools.map(t => `"${this.escapeTomlString(t)}"`).join(', ');
content += `enabled_tools = [${toolsStr}]\n`;
}
// Write env section last (as a separate subsection)
// IMPORTANT: In TOML, once we start [mcp_servers.server_name.env],
// everything after belongs to that subsection until a new section starts
if (serverConfig.env && typeof serverConfig.env === 'object' && Object.keys(serverConfig.env).length > 0) {
content += `\n[mcp_servers.${serverName}.env]\n`;
for (const [envKey, envValue] of Object.entries(serverConfig.env)) {
content += `${envKey} = "${this.escapeTomlString(String(envValue))}"\n`;
}
}
}
}
// Ensure directory exists
const configDir = path.dirname(configPath);
await fs.mkdir(configDir, { recursive: true });
// Write file
await fs.writeFile(configPath, content, 'utf-8');
}
/**
* Escape special characters in TOML strings
*/
escapeTomlString(str) {
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
/**
* Format a value for TOML output
*/
formatValue(value) {
if (typeof value === 'string') {
// Escape quotes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `"${escaped}"`;
} else if (typeof value === 'boolean') {
return value.toString();
} else if (typeof value === 'number') {
return value.toString();
}
return `"${String(value)}"`;
}
/**
* Remove automaker-tools MCP server configuration
*/
async removeMcpServer(projectPath) {
this.setProjectPath(projectPath);
const configPath = await this.getConfigPath();
try {
const config = await this.readConfig(configPath);
if (config.mcp_servers && config.mcp_servers['automaker-tools']) {
delete config.mcp_servers['automaker-tools'];
// If no more MCP servers, remove the section
if (Object.keys(config.mcp_servers).length === 0) {
delete config.mcp_servers;
}
await this.writeConfig(configPath, config);
console.log(`[CodexConfigManager] Removed automaker-tools MCP server from ${configPath}`);
}
} catch (e) {
console.error(`[CodexConfigManager] Error removing MCP server config:`, e);
}
}
}
module.exports = new CodexConfigManager();

View File

@@ -1,610 +0,0 @@
/**
* Codex CLI Execution Wrapper
*
* This module handles spawning and managing Codex CLI processes
* for executing OpenAI model queries.
*/
const { spawn } = require('child_process');
const { EventEmitter } = require('events');
const readline = require('readline');
const path = require('path');
const CodexCliDetector = require('./codex-cli-detector');
const codexConfigManager = require('./codex-config-manager');
/**
* Message types from Codex CLI JSON output
*/
const CODEX_EVENT_TYPES = {
THREAD_STARTED: 'thread.started',
ITEM_STARTED: 'item.started',
ITEM_COMPLETED: 'item.completed',
THREAD_COMPLETED: 'thread.completed',
ERROR: 'error'
};
/**
* Codex Executor - Manages Codex CLI process execution
*/
class CodexExecutor extends EventEmitter {
constructor() {
super();
this.currentProcess = null;
this.codexPath = null;
}
/**
* Find and cache the Codex CLI path
* @returns {string|null} Path to codex executable
*/
findCodexPath() {
if (this.codexPath) {
return this.codexPath;
}
const installation = CodexCliDetector.detectCodexInstallation();
if (installation.installed && installation.path) {
this.codexPath = installation.path;
return this.codexPath;
}
return null;
}
/**
* Execute a Codex CLI query
* @param {Object} options Execution options
* @param {string} options.prompt The prompt to execute
* @param {string} options.model Model to use (default: gpt-5.1-codex-max)
* @param {string} options.cwd Working directory
* @param {string} options.systemPrompt System prompt (optional, will be prepended to prompt)
* @param {number} options.maxTurns Not used - Codex CLI doesn't support this parameter
* @param {string[]} options.allowedTools Not used - Codex CLI doesn't support this parameter
* @param {Object} options.env Environment variables
* @param {Object} options.mcpServers MCP servers configuration (for configuring Codex TOML)
* @returns {AsyncGenerator} Generator yielding messages
*/
async *execute(options) {
const {
prompt,
model = 'gpt-5.1-codex-max',
cwd = process.cwd(),
systemPrompt,
maxTurns, // Not used by Codex CLI
allowedTools, // Not used by Codex CLI
env = {},
mcpServers = null
} = options;
const codexPath = this.findCodexPath();
if (!codexPath) {
yield {
type: 'error',
error: 'Codex CLI not found. Please install it with: npm install -g @openai/codex@latest'
};
return;
}
// Configure MCP server if provided
if (mcpServers && mcpServers['automaker-tools']) {
try {
// Get the absolute path to the MCP server script
const mcpServerScriptPath = path.resolve(__dirname, 'mcp-server-stdio.js');
// Verify the script exists
const fs = require('fs');
if (!fs.existsSync(mcpServerScriptPath)) {
console.warn(`[CodexExecutor] MCP server script not found at ${mcpServerScriptPath}, skipping MCP configuration`);
} else {
// Configure Codex TOML to use the MCP server
await codexConfigManager.configureMcpServer(cwd, mcpServerScriptPath);
console.log('[CodexExecutor] Configured automaker-tools MCP server for Codex CLI');
}
} catch (error) {
console.error('[CodexExecutor] Failed to configure MCP server:', error);
// Continue execution even if MCP config fails - Codex will work without MCP tools
}
}
// Combine system prompt with main prompt if provided
// Codex CLI doesn't support --system-prompt argument, so we prepend it to the prompt
let combinedPrompt = prompt;
console.log('[CodexExecutor] Original prompt length:', prompt?.length || 0);
if (systemPrompt) {
combinedPrompt = `${systemPrompt}\n\n---\n\n${prompt}`;
console.log('[CodexExecutor] System prompt prepended to main prompt');
console.log('[CodexExecutor] System prompt length:', systemPrompt.length);
console.log('[CodexExecutor] Combined prompt length:', combinedPrompt.length);
}
// Build command arguments
// Note: maxTurns and allowedTools are not supported by Codex CLI
console.log('[CodexExecutor] Building command arguments...');
const args = this.buildArgs({
prompt: combinedPrompt,
model
});
console.log('[CodexExecutor] Executing command:', codexPath);
console.log('[CodexExecutor] Number of args:', args.length);
console.log('[CodexExecutor] Args (without prompt):', args.slice(0, -1).join(' '));
console.log('[CodexExecutor] Prompt length in args:', args[args.length - 1]?.length || 0);
console.log('[CodexExecutor] Prompt preview (first 200 chars):', args[args.length - 1]?.substring(0, 200));
console.log('[CodexExecutor] Working directory:', cwd);
// Spawn the process
const processEnv = {
...process.env,
...env,
// Ensure OPENAI_API_KEY is available
OPENAI_API_KEY: env.OPENAI_API_KEY || process.env.OPENAI_API_KEY
};
// Log API key status (without exposing the key)
if (processEnv.OPENAI_API_KEY) {
console.log('[CodexExecutor] OPENAI_API_KEY is set (length:', processEnv.OPENAI_API_KEY.length, ')');
} else {
console.warn('[CodexExecutor] WARNING: OPENAI_API_KEY is not set!');
}
console.log('[CodexExecutor] Spawning process...');
const proc = spawn(codexPath, args, {
cwd,
env: processEnv,
stdio: ['pipe', 'pipe', 'pipe']
});
this.currentProcess = proc;
console.log('[CodexExecutor] Process spawned with PID:', proc.pid);
// Track process events
proc.on('error', (error) => {
console.error('[CodexExecutor] Process error:', error);
});
proc.on('spawn', () => {
console.log('[CodexExecutor] Process spawned successfully');
});
// Collect stderr output as it comes in
let stderr = '';
let hasOutput = false;
let stdoutChunks = [];
let stderrChunks = [];
proc.stderr.on('data', (data) => {
const errorText = data.toString();
stderr += errorText;
stderrChunks.push(errorText);
hasOutput = true;
console.error('[CodexExecutor] stderr chunk received (', data.length, 'bytes):', errorText.substring(0, 200));
});
proc.stderr.on('end', () => {
console.log('[CodexExecutor] stderr stream ended. Total chunks:', stderrChunks.length, 'Total length:', stderr.length);
});
proc.stdout.on('data', (data) => {
const text = data.toString();
stdoutChunks.push(text);
hasOutput = true;
console.log('[CodexExecutor] stdout chunk received (', data.length, 'bytes):', text.substring(0, 200));
});
proc.stdout.on('end', () => {
console.log('[CodexExecutor] stdout stream ended. Total chunks:', stdoutChunks.length);
});
// Create readline interface for parsing JSONL output
console.log('[CodexExecutor] Creating readline interface...');
const rl = readline.createInterface({
input: proc.stdout,
crlfDelay: Infinity
});
// Track accumulated content for converting to Claude format
let accumulatedText = '';
let toolUses = [];
let lastOutputTime = Date.now();
const OUTPUT_TIMEOUT = 30000; // 30 seconds timeout for no output
let lineCount = 0;
let jsonParseErrors = 0;
// Set up timeout check
const checkTimeout = setInterval(() => {
const timeSinceLastOutput = Date.now() - lastOutputTime;
if (timeSinceLastOutput > OUTPUT_TIMEOUT && !hasOutput) {
console.warn('[CodexExecutor] No output received for', timeSinceLastOutput, 'ms. Process still alive:', !proc.killed);
}
}, 5000);
console.log('[CodexExecutor] Starting to read lines from stdout...');
// Process stdout line by line (JSONL format)
try {
for await (const line of rl) {
hasOutput = true;
lastOutputTime = Date.now();
lineCount++;
console.log('[CodexExecutor] Line', lineCount, 'received (length:', line.length, '):', line.substring(0, 100));
if (!line.trim()) {
console.log('[CodexExecutor] Skipping empty line');
continue;
}
try {
const event = JSON.parse(line);
console.log('[CodexExecutor] Successfully parsed JSON event. Type:', event.type, 'Keys:', Object.keys(event));
const convertedMsg = this.convertToClaudeFormat(event);
console.log('[CodexExecutor] Converted message:', convertedMsg ? { type: convertedMsg.type } : 'null');
if (convertedMsg) {
// Accumulate text content
if (convertedMsg.type === 'assistant' && convertedMsg.message?.content) {
for (const block of convertedMsg.message.content) {
if (block.type === 'text') {
accumulatedText += block.text;
console.log('[CodexExecutor] Accumulated text block (total length:', accumulatedText.length, ')');
} else if (block.type === 'tool_use') {
toolUses.push(block);
console.log('[CodexExecutor] Tool use detected:', block.name);
}
}
}
console.log('[CodexExecutor] Yielding message of type:', convertedMsg.type);
yield convertedMsg;
} else {
console.log('[CodexExecutor] Converted message is null, skipping');
}
} catch (parseError) {
jsonParseErrors++;
// Non-JSON output, yield as text
console.log('[CodexExecutor] JSON parse error (', jsonParseErrors, 'total):', parseError.message);
console.log('[CodexExecutor] Non-JSON line content:', line.substring(0, 200));
yield {
type: 'assistant',
message: {
content: [{ type: 'text', text: line + '\n' }]
}
};
}
}
console.log('[CodexExecutor] Finished reading all lines. Total lines:', lineCount, 'JSON errors:', jsonParseErrors);
} catch (readError) {
console.error('[CodexExecutor] Error reading from readline:', readError);
throw readError;
} finally {
clearInterval(checkTimeout);
console.log('[CodexExecutor] Cleaned up timeout checker');
}
// Handle process completion
console.log('[CodexExecutor] Waiting for process to close...');
const exitCode = await new Promise((resolve) => {
proc.on('close', (code, signal) => {
console.log('[CodexExecutor] Process closed with code:', code, 'signal:', signal);
resolve(code);
});
});
this.currentProcess = null;
console.log('[CodexExecutor] Process completed. Exit code:', exitCode, 'Has output:', hasOutput, 'Stderr length:', stderr.length);
// Wait a bit for any remaining stderr data to be collected
console.log('[CodexExecutor] Waiting 200ms for any remaining stderr data...');
await new Promise(resolve => setTimeout(resolve, 200));
console.log('[CodexExecutor] Final stderr length:', stderr.length, 'Final stdout chunks:', stdoutChunks.length);
if (exitCode !== 0) {
const errorMessage = stderr.trim()
? `Codex CLI exited with code ${exitCode}.\n\nError output:\n${stderr}`
: `Codex CLI exited with code ${exitCode}. No error output captured.`;
console.error('[CodexExecutor] Process failed with exit code', exitCode);
console.error('[CodexExecutor] Error message:', errorMessage);
console.error('[CodexExecutor] Stderr chunks:', stderrChunks.length, 'Stdout chunks:', stdoutChunks.length);
yield {
type: 'error',
error: errorMessage
};
} else if (!hasOutput && !stderr) {
// Process exited successfully but produced no output - might be API key issue
const warningMessage = 'Codex CLI completed but produced no output. This might indicate:\n' +
'- Missing or invalid OPENAI_API_KEY\n' +
'- Codex CLI configuration issue\n' +
'- The process completed without generating any response\n\n' +
`Debug info: Exit code ${exitCode}, stdout chunks: ${stdoutChunks.length}, stderr chunks: ${stderrChunks.length}, lines read: ${lineCount}`;
console.warn('[CodexExecutor] No output detected:', warningMessage);
console.warn('[CodexExecutor] Stdout chunks:', stdoutChunks);
console.warn('[CodexExecutor] Stderr chunks:', stderrChunks);
yield {
type: 'error',
error: warningMessage
};
} else {
console.log('[CodexExecutor] Process completed successfully. Exit code:', exitCode, 'Lines processed:', lineCount);
}
}
/**
* Build command arguments for Codex CLI
* Only includes supported arguments based on Codex CLI help:
* - --model: Model to use
* - --json: JSON output format
* - --full-auto: Non-interactive automatic execution
*
* Note: Codex CLI does NOT support:
* - --system-prompt (system prompt is prepended to main prompt)
* - --max-turns (not available in CLI)
* - --tools (not available in CLI)
*
* @param {Object} options Options
* @returns {string[]} Command arguments
*/
buildArgs(options) {
const { prompt, model } = options;
console.log('[CodexExecutor] buildArgs called with model:', model, 'prompt length:', prompt?.length || 0);
const args = ['exec'];
// Add model (required for most use cases)
if (model) {
args.push('--model', model);
console.log('[CodexExecutor] Added model argument:', model);
}
// Add JSON output flag for structured parsing
args.push('--json');
console.log('[CodexExecutor] Added --json flag');
// Add full-auto mode (non-interactive)
// This enables automatic execution with workspace-write sandbox
args.push('--full-auto');
console.log('[CodexExecutor] Added --full-auto flag');
// Add the prompt at the end
args.push(prompt);
console.log('[CodexExecutor] Added prompt (length:', prompt?.length || 0, ')');
console.log('[CodexExecutor] Final args count:', args.length);
return args;
}
/**
* Map Claude tool names to Codex tool names
* @param {string[]} tools Array of tool names
* @returns {string[]} Mapped tool names
*/
mapToolsToCodex(tools) {
const toolMap = {
'Read': 'read',
'Write': 'write',
'Edit': 'edit',
'Bash': 'bash',
'Glob': 'glob',
'Grep': 'grep',
'WebSearch': 'web-search',
'WebFetch': 'web-fetch'
};
return tools
.map(tool => toolMap[tool] || tool.toLowerCase())
.filter(tool => tool); // Remove undefined
}
/**
* Convert Codex JSONL event to Claude SDK message format
* @param {Object} event Codex event object
* @returns {Object|null} Claude-format message or null
*/
convertToClaudeFormat(event) {
console.log('[CodexExecutor] Converting event:', JSON.stringify(event).substring(0, 200));
const { type, data, item, thread_id } = event;
switch (type) {
case CODEX_EVENT_TYPES.THREAD_STARTED:
case 'thread.started':
// Session initialization
return {
type: 'session_start',
sessionId: thread_id || data?.thread_id || event.thread_id
};
case CODEX_EVENT_TYPES.ITEM_COMPLETED:
case 'item.completed':
// Codex uses 'item' field, not 'data'
return this.convertItemCompleted(item || data);
case CODEX_EVENT_TYPES.ITEM_STARTED:
case 'item.started':
// Convert item.started events - these indicate tool/command usage
const startedItem = item || data;
if (startedItem?.type === 'command_execution' && startedItem?.command) {
return {
type: 'assistant',
message: {
content: [{
type: 'tool_use',
name: 'bash',
input: { command: startedItem.command }
}]
}
};
}
// For other item.started types, return null (we'll show the completed version)
return null;
case CODEX_EVENT_TYPES.THREAD_COMPLETED:
case 'thread.completed':
return {
type: 'complete',
sessionId: thread_id || data?.thread_id || event.thread_id
};
case CODEX_EVENT_TYPES.ERROR:
case 'error':
return {
type: 'error',
error: data?.message || item?.message || event.message || 'Unknown error from Codex CLI'
};
case 'turn.started':
// Turn started - just a marker, no need to convert
return null;
default:
// Pass through other events
console.log('[CodexExecutor] Unhandled event type:', type);
return null;
}
}
/**
* Convert item.completed event to Claude format
* @param {Object} item Event item data
* @returns {Object|null} Claude-format message
*/
convertItemCompleted(item) {
if (!item) {
console.log('[CodexExecutor] convertItemCompleted: item is null/undefined');
return null;
}
const itemType = item.type || item.item_type;
console.log('[CodexExecutor] convertItemCompleted: itemType =', itemType, 'item keys:', Object.keys(item));
switch (itemType) {
case 'reasoning':
// Thinking/reasoning output - Codex uses 'text' field
const reasoningText = item.text || item.content || '';
console.log('[CodexExecutor] Converting reasoning, text length:', reasoningText.length);
return {
type: 'assistant',
message: {
content: [{
type: 'thinking',
thinking: reasoningText
}]
}
};
case 'agent_message':
case 'message':
// Assistant text message
const messageText = item.content || item.text || '';
console.log('[CodexExecutor] Converting message, text length:', messageText.length);
return {
type: 'assistant',
message: {
content: [{
type: 'text',
text: messageText
}]
}
};
case 'command_execution':
// Command execution - show both the command and its output
const command = item.command || '';
const output = item.aggregated_output || item.output || '';
console.log('[CodexExecutor] Converting command_execution, command:', command.substring(0, 50), 'output length:', output.length);
// Return as text message showing the command and output
return {
type: 'assistant',
message: {
content: [{
type: 'text',
text: `\`\`\`bash\n${command}\n\`\`\`\n\n${output}`
}]
}
};
case 'tool_use':
// Tool use
return {
type: 'assistant',
message: {
content: [{
type: 'tool_use',
name: item.tool || item.command || 'unknown',
input: item.input || item.args || {}
}]
}
};
case 'tool_result':
// Tool result
return {
type: 'tool_result',
tool_use_id: item.tool_use_id,
content: item.output || item.result
};
case 'todo_list':
// Todo list - convert to text format
const todos = item.items || [];
const todoText = todos.map((t, i) => `${i + 1}. ${t.text || t}`).join('\n');
console.log('[CodexExecutor] Converting todo_list, items:', todos.length);
return {
type: 'assistant',
message: {
content: [{
type: 'text',
text: `**Todo List:**\n${todoText}`
}]
}
};
default:
// Generic text output
const text = item.text || item.content || item.aggregated_output;
if (text) {
console.log('[CodexExecutor] Converting default item type, text length:', text.length);
return {
type: 'assistant',
message: {
content: [{
type: 'text',
text: String(text)
}]
}
};
}
console.log('[CodexExecutor] convertItemCompleted: No text content found, returning null');
return null;
}
}
/**
* Abort current execution
*/
abort() {
if (this.currentProcess) {
console.log('[CodexExecutor] Aborting current process');
this.currentProcess.kill('SIGTERM');
this.currentProcess = null;
}
}
/**
* Check if execution is in progress
* @returns {boolean} Whether execution is in progress
*/
isRunning() {
return this.currentProcess !== null;
}
}
// Singleton instance
const codexExecutor = new CodexExecutor();
module.exports = codexExecutor;

View File

@@ -1,452 +0,0 @@
const path = require("path");
const fs = require("fs/promises");
/**
* Context Manager - Handles reading, writing, and deleting context files for features
*/
class ContextManager {
/**
* Write output to feature context file
*/
async writeToContextFile(projectPath, featureId, content) {
if (!projectPath) return;
try {
const featureDir = path.join(
projectPath,
".automaker",
"features",
featureId
);
// Ensure feature directory exists
try {
await fs.access(featureDir);
} catch {
await fs.mkdir(featureDir, { recursive: true });
}
const filePath = path.join(featureDir, "agent-output.md");
// Append to existing file or create new one
try {
const existing = await fs.readFile(filePath, "utf-8");
await fs.writeFile(filePath, existing + content, "utf-8");
} catch {
await fs.writeFile(filePath, content, "utf-8");
}
} catch (error) {
console.error("[ContextManager] Failed to write to context file:", error);
}
}
/**
* Read context file for a feature
*/
async readContextFile(projectPath, featureId) {
try {
const contextPath = path.join(
projectPath,
".automaker",
"features",
featureId,
"agent-output.md"
);
const content = await fs.readFile(contextPath, "utf-8");
return content;
} catch (error) {
console.log(`[ContextManager] No context file found for ${featureId}`);
return null;
}
}
/**
* Delete agent context file for a feature
*/
async deleteContextFile(projectPath, featureId) {
if (!projectPath) return;
try {
const contextPath = path.join(
projectPath,
".automaker",
"features",
featureId,
"agent-output.md"
);
await fs.unlink(contextPath);
console.log(
`[ContextManager] Deleted agent context for feature ${featureId}`
);
} catch (error) {
// File might not exist, which is fine
if (error.code !== "ENOENT") {
console.error("[ContextManager] Failed to delete context file:", error);
}
}
}
/**
* Read the memory.md file containing lessons learned and common issues
* Returns formatted string to inject into prompts
*/
async getMemoryContent(projectPath) {
if (!projectPath) return "";
try {
const memoryPath = path.join(projectPath, ".automaker", "memory.md");
// Check if file exists
try {
await fs.access(memoryPath);
} catch {
// File doesn't exist, return empty string
return "";
}
const content = await fs.readFile(memoryPath, "utf-8");
if (!content.trim()) {
return "";
}
return `
**🧠 Agent Memory - Previous Lessons Learned:**
The following memory file contains lessons learned from previous agent runs, including common issues and their solutions. Review this carefully to avoid repeating past mistakes.
<agent-memory>
${content}
</agent-memory>
**IMPORTANT:** If you encounter a new issue that took significant debugging effort to resolve, add it to the memory file at \`.automaker/memory.md\` in a concise format:
- Issue title
- Problem description (1-2 sentences)
- Solution/fix (with code example if helpful)
This helps future agent runs avoid the same pitfalls.
`;
} catch (error) {
console.error("[ContextManager] Failed to read memory file:", error);
return "";
}
}
/**
* List context files from .automaker/context/ directory and get previews
* Returns a formatted string with file names and first 50 lines of each file
*/
async getContextFilesPreview(projectPath) {
if (!projectPath) return "";
try {
const contextDir = path.join(projectPath, ".automaker", "context");
// Check if directory exists
try {
await fs.access(contextDir);
} catch {
// Directory doesn't exist, return empty string
return "";
}
// Read directory contents
const entries = await fs.readdir(contextDir, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort();
if (files.length === 0) {
return "";
}
// Build preview string
const previews = [];
previews.push(`\n**📁 Context Files Available:**\n`);
previews.push(
`The following context files are available in \`.automaker/context/\` directory.`
);
previews.push(
`These files contain additional context that may be relevant to your work.`
);
previews.push(
`You can read them in full using the Read tool if needed.\n`
);
for (const fileName of files) {
try {
const filePath = path.join(contextDir, fileName);
const content = await fs.readFile(filePath, "utf-8");
const lines = content.split("\n");
const previewLines = lines.slice(0, 50);
const preview = previewLines.join("\n");
const hasMore = lines.length > 50;
previews.push(`\n**File: ${fileName}**`);
if (hasMore) {
previews.push(
`(Showing first 50 of ${lines.length} lines - use Read tool to see full content)`
);
}
previews.push(`\`\`\``);
previews.push(preview);
previews.push(`\`\`\`\n`);
} catch (error) {
console.error(
`[ContextManager] Failed to read context file ${fileName}:`,
error
);
previews.push(`\n**File: ${fileName}** (Error reading file)\n`);
}
}
return previews.join("\n");
} catch (error) {
console.error("[ContextManager] Failed to list context files:", error);
return "";
}
}
/**
* Save the initial git state before a feature starts executing
* This captures all files that were already modified before the AI agent started
* @param {string} projectPath - Path to the project
* @param {string} featureId - Feature ID
* @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[]}>}
*/
async saveInitialGitState(projectPath, featureId) {
if (!projectPath) return { modifiedFiles: [], untrackedFiles: [] };
try {
const { execSync } = require("child_process");
const featureDir = path.join(
projectPath,
".automaker",
"features",
featureId
);
// Ensure feature directory exists
try {
await fs.access(featureDir);
} catch {
await fs.mkdir(featureDir, { recursive: true });
}
// Get list of modified files (both staged and unstaged)
let modifiedFiles = [];
try {
const modifiedOutput = execSync("git diff --name-only HEAD", {
cwd: projectPath,
encoding: "utf-8",
}).trim();
if (modifiedOutput) {
modifiedFiles = modifiedOutput.split("\n").filter(Boolean);
}
} catch (error) {
console.log(
"[ContextManager] No modified files or git error:",
error.message
);
}
// Get list of untracked files
let untrackedFiles = [];
try {
const untrackedOutput = execSync(
"git ls-files --others --exclude-standard",
{
cwd: projectPath,
encoding: "utf-8",
}
).trim();
if (untrackedOutput) {
untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
}
} catch (error) {
console.log(
"[ContextManager] Error getting untracked files:",
error.message
);
}
// Save the initial state to a JSON file
const stateFile = path.join(featureDir, "git-state.json");
const state = {
timestamp: new Date().toISOString(),
modifiedFiles,
untrackedFiles,
};
await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8");
console.log(
`[ContextManager] Saved initial git state for ${featureId}:`,
{
modifiedCount: modifiedFiles.length,
untrackedCount: untrackedFiles.length,
}
);
return state;
} catch (error) {
console.error(
"[ContextManager] Failed to save initial git state:",
error
);
return { modifiedFiles: [], untrackedFiles: [] };
}
}
/**
* Get the initial git state saved before a feature started executing
* @param {string} projectPath - Path to the project
* @param {string} featureId - Feature ID
* @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[], timestamp: string} | null>}
*/
async getInitialGitState(projectPath, featureId) {
if (!projectPath) return null;
try {
const stateFile = path.join(
projectPath,
".automaker",
"features",
featureId,
"git-state.json"
);
const content = await fs.readFile(stateFile, "utf-8");
return JSON.parse(content);
} catch (error) {
console.log(
`[ContextManager] No initial git state found for ${featureId}`
);
return null;
}
}
/**
* Delete the git state file for a feature
* @param {string} projectPath - Path to the project
* @param {string} featureId - Feature ID
*/
async deleteGitStateFile(projectPath, featureId) {
if (!projectPath) return;
try {
const stateFile = path.join(
projectPath,
".automaker",
"features",
featureId,
"git-state.json"
);
await fs.unlink(stateFile);
console.log(`[ContextManager] Deleted git state file for ${featureId}`);
} catch (error) {
// File might not exist, which is fine
if (error.code !== "ENOENT") {
console.error(
"[ContextManager] Failed to delete git state file:",
error
);
}
}
}
/**
* Calculate which files were changed during the AI session
* by comparing current git state with the saved initial state
* @param {string} projectPath - Path to the project
* @param {string} featureId - Feature ID
* @returns {Promise<{newFiles: string[], modifiedFiles: string[]}>}
*/
async getFilesChangedDuringSession(projectPath, featureId) {
if (!projectPath) return { newFiles: [], modifiedFiles: [] };
try {
const { execSync } = require("child_process");
// Get initial state
const initialState = await this.getInitialGitState(
projectPath,
featureId
);
// Get current state
let currentModified = [];
try {
const modifiedOutput = execSync("git diff --name-only HEAD", {
cwd: projectPath,
encoding: "utf-8",
}).trim();
if (modifiedOutput) {
currentModified = modifiedOutput.split("\n").filter(Boolean);
}
} catch (error) {
console.log("[ContextManager] No modified files or git error");
}
let currentUntracked = [];
try {
const untrackedOutput = execSync(
"git ls-files --others --exclude-standard",
{
cwd: projectPath,
encoding: "utf-8",
}
).trim();
if (untrackedOutput) {
currentUntracked = untrackedOutput.split("\n").filter(Boolean);
}
} catch (error) {
console.log("[ContextManager] Error getting untracked files");
}
if (!initialState) {
// No initial state - all current changes are considered from this session
console.log(
"[ContextManager] No initial state found, returning all current changes"
);
return {
newFiles: currentUntracked,
modifiedFiles: currentModified,
};
}
// Calculate files that are new since the session started
const initialModifiedSet = new Set(initialState.modifiedFiles || []);
const initialUntrackedSet = new Set(initialState.untrackedFiles || []);
// New files = current untracked - initial untracked
const newFiles = currentUntracked.filter(
(f) => !initialUntrackedSet.has(f)
);
// Modified files = current modified - initial modified
const modifiedFiles = currentModified.filter(
(f) => !initialModifiedSet.has(f)
);
console.log(
`[ContextManager] Files changed during session for ${featureId}:`,
{
newFilesCount: newFiles.length,
modifiedFilesCount: modifiedFiles.length,
newFiles,
modifiedFiles,
}
);
return { newFiles, modifiedFiles };
} catch (error) {
console.error(
"[ContextManager] Failed to calculate changed files:",
error
);
return { newFiles: [], modifiedFiles: [] };
}
}
}
module.exports = new ContextManager();

File diff suppressed because it is too large Load Diff

View File

@@ -1,413 +0,0 @@
const path = require("path");
const fs = require("fs/promises");
/**
* Feature Loader - Handles loading and managing features from individual feature folders
* Each feature is stored in .automaker/features/{featureId}/feature.json
*/
class FeatureLoader {
/**
* Get the features directory path
*/
getFeaturesDir(projectPath) {
return path.join(projectPath, ".automaker", "features");
}
/**
* Get the path to a specific feature folder
*/
getFeatureDir(projectPath, featureId) {
return path.join(this.getFeaturesDir(projectPath), featureId);
}
/**
* Get the path to a feature's feature.json file
*/
getFeatureJsonPath(projectPath, featureId) {
return path.join(
this.getFeatureDir(projectPath, featureId),
"feature.json"
);
}
/**
* Get the path to a feature's agent-output.md file
*/
getAgentOutputPath(projectPath, featureId) {
return path.join(
this.getFeatureDir(projectPath, featureId),
"agent-output.md"
);
}
/**
* Generate a new feature ID
*/
generateFeatureId() {
return `feature-${Date.now()}-${Math.random()
.toString(36)
.substring(2, 11)}`;
}
/**
* Ensure all image paths for a feature are stored within the feature directory
*/
async ensureFeatureImages(projectPath, featureId, feature) {
if (
!feature ||
!Array.isArray(feature.imagePaths) ||
feature.imagePaths.length === 0
) {
return;
}
const featureDir = this.getFeatureDir(projectPath, featureId);
const featureImagesDir = path.join(featureDir, "images");
await fs.mkdir(featureImagesDir, { recursive: true });
const updatedImagePaths = [];
for (const entry of feature.imagePaths) {
const isStringEntry = typeof entry === "string";
const currentPathValue = isStringEntry ? entry : entry.path;
if (!currentPathValue) {
updatedImagePaths.push(entry);
continue;
}
let resolvedCurrentPath = currentPathValue;
if (!path.isAbsolute(resolvedCurrentPath)) {
resolvedCurrentPath = path.join(projectPath, resolvedCurrentPath);
}
resolvedCurrentPath = path.normalize(resolvedCurrentPath);
// Skip if file doesn't exist
try {
await fs.access(resolvedCurrentPath);
} catch {
console.warn(
`[FeatureLoader] Image file missing for ${featureId}: ${resolvedCurrentPath}`
);
updatedImagePaths.push(entry);
continue;
}
const relativeToFeatureImages = path.relative(
featureImagesDir,
resolvedCurrentPath
);
const alreadyInFeatureDir =
relativeToFeatureImages === "" ||
(!relativeToFeatureImages.startsWith("..") &&
!path.isAbsolute(relativeToFeatureImages));
let finalPath = resolvedCurrentPath;
if (!alreadyInFeatureDir) {
const originalName = path.basename(resolvedCurrentPath);
let targetPath = path.join(featureImagesDir, originalName);
// Avoid overwriting files by appending a counter if needed
let counter = 1;
while (true) {
try {
await fs.access(targetPath);
const parsed = path.parse(originalName);
targetPath = path.join(
featureImagesDir,
`${parsed.name}-${counter}${parsed.ext}`
);
counter += 1;
} catch {
break;
}
}
try {
await fs.rename(resolvedCurrentPath, targetPath);
finalPath = targetPath;
} catch (error) {
console.warn(
`[FeatureLoader] Failed to move image ${resolvedCurrentPath}: ${error.message}`
);
updatedImagePaths.push(entry);
continue;
}
}
updatedImagePaths.push(
isStringEntry ? finalPath : { ...entry, path: finalPath }
);
}
feature.imagePaths = updatedImagePaths;
}
/**
* Get all features for a project
*/
async getAll(projectPath) {
try {
const featuresDir = this.getFeaturesDir(projectPath);
// Check if features directory exists
try {
await fs.access(featuresDir);
} catch {
// Directory doesn't exist, return empty array
return [];
}
// Read all feature directories
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
const featureDirs = entries.filter((entry) => entry.isDirectory());
// Load each feature
const features = [];
for (const dir of featureDirs) {
const featureId = dir.name;
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
try {
const content = await fs.readFile(featureJsonPath, "utf-8");
const feature = JSON.parse(content);
features.push(feature);
} catch (error) {
console.error(
`[FeatureLoader] Failed to load feature ${featureId}:`,
error
);
// Continue loading other features
}
}
// Sort by creation order (feature IDs contain timestamp)
features.sort((a, b) => {
const aTime = a.id ? parseInt(a.id.split("-")[1] || "0") : 0;
const bTime = b.id ? parseInt(b.id.split("-")[1] || "0") : 0;
return aTime - bTime;
});
return features;
} catch (error) {
console.error("[FeatureLoader] Failed to get all features:", error);
return [];
}
}
/**
* Get a single feature by ID
*/
async get(projectPath, featureId) {
try {
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
const content = await fs.readFile(featureJsonPath, "utf-8");
return JSON.parse(content);
} catch (error) {
if (error.code === "ENOENT") {
return null;
}
console.error(
`[FeatureLoader] Failed to get feature ${featureId}:`,
error
);
throw error;
}
}
/**
* Create a new feature
*/
async create(projectPath, featureData) {
const featureId = featureData.id || this.generateFeatureId();
const featureDir = this.getFeatureDir(projectPath, featureId);
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
// Ensure features directory exists
const featuresDir = this.getFeaturesDir(projectPath);
await fs.mkdir(featuresDir, { recursive: true });
// Create feature directory
await fs.mkdir(featureDir, { recursive: true });
// Ensure feature has an ID
const feature = { ...featureData, id: featureId };
// Move any uploaded images into the feature directory
await this.ensureFeatureImages(projectPath, featureId, feature);
// Write feature.json
await fs.writeFile(
featureJsonPath,
JSON.stringify(feature, null, 2),
"utf-8"
);
console.log(`[FeatureLoader] Created feature ${featureId}`);
return feature;
}
/**
* Update a feature (partial updates supported)
*/
async update(projectPath, featureId, updates) {
try {
const feature = await this.get(projectPath, featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
// Merge updates
const updatedFeature = { ...feature, ...updates };
// Move any new images into the feature directory
await this.ensureFeatureImages(projectPath, featureId, updatedFeature);
// Write back to file
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
await fs.writeFile(
featureJsonPath,
JSON.stringify(updatedFeature, null, 2),
"utf-8"
);
console.log(`[FeatureLoader] Updated feature ${featureId}`);
return updatedFeature;
} catch (error) {
console.error(
`[FeatureLoader] Failed to update feature ${featureId}:`,
error
);
throw error;
}
}
/**
* Delete a feature and its entire folder
*/
async delete(projectPath, featureId) {
try {
const featureDir = this.getFeatureDir(projectPath, featureId);
await fs.rm(featureDir, { recursive: true, force: true });
console.log(`[FeatureLoader] Deleted feature ${featureId}`);
} catch (error) {
if (error.code === "ENOENT") {
// Feature doesn't exist, that's fine
return;
}
console.error(
`[FeatureLoader] Failed to delete feature ${featureId}:`,
error
);
throw error;
}
}
/**
* Get agent output for a feature
*/
async getAgentOutput(projectPath, featureId) {
try {
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
const content = await fs.readFile(agentOutputPath, "utf-8");
return content;
} catch (error) {
if (error.code === "ENOENT") {
return null;
}
console.error(
`[FeatureLoader] Failed to get agent output for ${featureId}:`,
error
);
return null;
}
}
// ============================================================================
// Legacy methods for backward compatibility (used by backend services)
// ============================================================================
/**
* Load all features for a project (legacy API)
* Features are stored in .automaker/features/{id}/feature.json
*/
async loadFeatures(projectPath) {
return await this.getAll(projectPath);
}
/**
* Update feature status (legacy API)
* Features are stored in .automaker/features/{id}/feature.json
* @param {string} featureId - The ID of the feature to update
* @param {string} status - The new status
* @param {string} projectPath - Path to the project
* @param {string} [summary] - Optional summary of what was done
* @param {string} [error] - Optional error message if feature errored
*/
async updateFeatureStatus(featureId, status, projectPath, summary, error) {
const updates = { status };
if (summary !== undefined) {
updates.summary = summary;
}
if (error !== undefined) {
updates.error = error;
} else {
// Clear error if not provided
const feature = await this.get(projectPath, featureId);
if (feature && feature.error) {
updates.error = undefined;
}
}
await this.update(projectPath, featureId, updates);
console.log(
`[FeatureLoader] Updated feature ${featureId}: status=${status}${
summary ? `, summary="${summary}"` : ""
}`
);
}
/**
* Select the next feature to implement
* Prioritizes: earlier features in the list that are not verified or waiting_approval
*/
selectNextFeature(features) {
// Find first feature that is in backlog or in_progress status
// Skip verified and waiting_approval (which needs user input)
return features.find(
(f) => f.status !== "verified" && f.status !== "waiting_approval"
);
}
/**
* Update worktree info for a feature (legacy API)
* Features are stored in .automaker/features/{id}/feature.json
* @param {string} featureId - The ID of the feature to update
* @param {string} projectPath - Path to the project
* @param {string|null} worktreePath - Path to the worktree (null to clear)
* @param {string|null} branchName - Name of the feature branch (null to clear)
*/
async updateFeatureWorktree(
featureId,
projectPath,
worktreePath,
branchName
) {
const updates = {};
if (worktreePath) {
updates.worktreePath = worktreePath;
updates.branchName = branchName;
} else {
updates.worktreePath = null;
updates.branchName = null;
}
await this.update(projectPath, featureId, updates);
console.log(
`[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}`
);
}
}
module.exports = new FeatureLoader();

View File

@@ -1,379 +0,0 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const promptBuilder = require("./prompt-builder");
/**
* Feature Suggestions Service - Analyzes project and generates feature suggestions
*/
class FeatureSuggestionsService {
constructor() {
this.runningAnalysis = null;
}
/**
* Generate feature suggestions by analyzing the project
* @param {string} projectPath - Path to the project
* @param {Function} sendToRenderer - Function to send events to renderer
* @param {Object} execution - Execution context with abort controller
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
*/
async generateSuggestions(projectPath, sendToRenderer, execution, suggestionType = "features") {
console.log(
`[FeatureSuggestions] Generating ${suggestionType} suggestions for: ${projectPath}`
);
try {
const abortController = new AbortController();
execution.abortController = abortController;
const options = {
model: "claude-sonnet-4-20250514",
systemPrompt: this.getSystemPrompt(suggestionType),
maxTurns: 50,
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep", "Bash"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
const prompt = this.buildAnalysisPrompt(suggestionType);
sendToRenderer({
type: "suggestions_progress",
content: "Starting project analysis...\n",
});
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let fullResponse = "";
for await (const msg of currentQuery) {
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
fullResponse += block.text;
sendToRenderer({
type: "suggestions_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
sendToRenderer({
type: "suggestions_tool",
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
// Parse the suggestions from the response
const suggestions = this.parseSuggestions(fullResponse);
sendToRenderer({
type: "suggestions_complete",
suggestions: suggestions,
});
return {
success: true,
suggestions: suggestions,
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[FeatureSuggestions] Analysis aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
success: false,
message: "Analysis aborted",
suggestions: [],
};
}
console.error(
"[FeatureSuggestions] Error generating suggestions:",
error
);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
/**
* Parse suggestions from the LLM response
* Looks for JSON array in the response
*/
parseSuggestions(response) {
try {
// Try to find JSON array in the response
// Look for ```json ... ``` blocks first
const jsonBlockMatch = response.match(/```json\s*([\s\S]*?)```/);
if (jsonBlockMatch) {
const parsed = JSON.parse(jsonBlockMatch[1].trim());
if (Array.isArray(parsed)) {
return this.validateSuggestions(parsed);
}
}
// Try to find a raw JSON array
const jsonArrayMatch = response.match(/\[\s*\{[\s\S]*\}\s*\]/);
if (jsonArrayMatch) {
const parsed = JSON.parse(jsonArrayMatch[0]);
if (Array.isArray(parsed)) {
return this.validateSuggestions(parsed);
}
}
console.warn(
"[FeatureSuggestions] Could not parse suggestions from response"
);
return [];
} catch (error) {
console.error("[FeatureSuggestions] Error parsing suggestions:", error);
return [];
}
}
/**
* Validate and normalize suggestions
*/
validateSuggestions(suggestions) {
return suggestions
.filter((s) => s && typeof s === "object")
.map((s, index) => ({
id: `suggestion-${Date.now()}-${index}`,
category: s.category || "Uncategorized",
description: s.description || s.title || "No description",
steps: Array.isArray(s.steps) ? s.steps : [],
priority: typeof s.priority === "number" ? s.priority : index + 1,
reasoning: s.reasoning || "",
}))
.sort((a, b) => a.priority - b.priority);
}
/**
* Get the system prompt for feature suggestion analysis
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
*/
getSystemPrompt(suggestionType = "features") {
const basePrompt = `You are an expert software architect. Your job is to analyze a codebase and provide actionable suggestions.
You have access to file reading and search tools. Use them to understand the codebase.
When analyzing, look at:
- README files and documentation
- Package.json, cargo.toml, or similar config files for tech stack
- Source code structure and organization
- Existing code patterns and implementation styles`;
switch (suggestionType) {
case "refactoring":
return `${basePrompt}
Your specific focus is on **refactoring suggestions**. You should:
1. Identify code smells and areas that need cleanup
2. Find duplicated code that could be consolidated
3. Spot overly complex functions or classes that should be broken down
4. Look for inconsistent naming conventions or coding patterns
5. Find opportunities to improve code organization and modularity
6. Identify violations of SOLID principles or common design patterns
7. Look for dead code or unused dependencies
Prioritize suggestions by:
- Impact on maintainability
- Risk level (lower risk refactorings first)
- Complexity of the refactoring`;
case "security":
return `${basePrompt}
Your specific focus is on **security vulnerabilities and improvements**. You should:
1. Identify potential security vulnerabilities (OWASP Top 10)
2. Look for hardcoded secrets, API keys, or credentials
3. Check for proper input validation and sanitization
4. Identify SQL injection, XSS, or command injection risks
5. Review authentication and authorization patterns
6. Check for secure communication (HTTPS, encryption)
7. Look for insecure dependencies or outdated packages
8. Review error handling that might leak sensitive information
9. Check for proper session management
10. Identify insecure file handling or path traversal risks
Prioritize by severity:
- Critical: Exploitable vulnerabilities with high impact
- High: Security issues that could lead to data exposure
- Medium: Best practice violations that weaken security
- Low: Minor improvements to security posture`;
case "performance":
return `${basePrompt}
Your specific focus is on **performance issues and optimizations**. You should:
1. Identify N+1 query problems or inefficient database access
2. Look for unnecessary re-renders in React/frontend code
3. Find opportunities for caching or memoization
4. Identify large bundle sizes or unoptimized imports
5. Look for blocking operations that could be async
6. Find memory leaks or inefficient memory usage
7. Identify slow algorithms or data structure choices
8. Look for missing indexes in database schemas
9. Find opportunities for lazy loading or code splitting
10. Identify unnecessary network requests or API calls
Prioritize by:
- Impact on user experience
- Frequency of the slow path
- Ease of implementation`;
default: // "features"
return `${basePrompt}
Your specific focus is on **missing features and improvements**. You should:
1. Identify what the application does and what features it currently has
2. Look at the .automaker/app_spec.txt file if it exists
3. Generate a comprehensive list of missing features that would be valuable to users
4. Consider user experience improvements
5. Consider developer experience improvements
6. Look at common patterns in similar applications
Prioritize features by:
- Impact on users
- Alignment with project goals
- Complexity of implementation`;
}
}
/**
* Build the prompt for analyzing the project
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
*/
buildAnalysisPrompt(suggestionType = "features") {
const commonIntro = `Analyze this project and generate a list of actionable suggestions.
**Your Task:**
1. First, explore the project structure:
- Read README.md, package.json, or similar config files
- Scan the source code directory structure
- Identify the tech stack and frameworks used
- Look at existing code and how it's implemented
2. Identify what the application does:
- What is the main purpose?
- What patterns and conventions are used?
`;
const commonOutput = `
**CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this:
\`\`\`json
[
{
"category": "Category Name",
"description": "Clear description of the suggestion",
"steps": [
"Step 1 to implement",
"Step 2 to implement",
"Step 3 to implement"
],
"priority": 1,
"reasoning": "Why this is important"
}
]
\`\`\`
**Important Guidelines:**
- Generate at least 10-15 suggestions
- Order them by priority (1 = highest priority)
- Each suggestion should have clear, actionable steps
- Be specific about what files might need to be modified
- Consider the existing tech stack and patterns
Begin by exploring the project structure.`;
switch (suggestionType) {
case "refactoring":
return `${commonIntro}
3. Look for refactoring opportunities:
- Find code duplication across the codebase
- Identify functions or classes that are too long or complex
- Look for inconsistent patterns or naming conventions
- Find tightly coupled code that should be decoupled
- Identify opportunities to extract reusable utilities
- Look for dead code or unused exports
- Check for proper separation of concerns
Categories to use: "Code Smell", "Duplication", "Complexity", "Architecture", "Naming", "Dead Code", "Coupling", "Testing"
${commonOutput}`;
case "security":
return `${commonIntro}
3. Look for security issues:
- Check for hardcoded secrets or API keys
- Look for potential injection vulnerabilities (SQL, XSS, command)
- Review authentication and authorization code
- Check input validation and sanitization
- Look for insecure dependencies
- Review error handling for information leakage
- Check for proper HTTPS/TLS usage
- Look for insecure file operations
Categories to use: "Critical", "High", "Medium", "Low" (based on severity)
${commonOutput}`;
case "performance":
return `${commonIntro}
3. Look for performance issues:
- Find N+1 queries or inefficient database access patterns
- Look for unnecessary re-renders in React components
- Identify missing memoization opportunities
- Check bundle size and import patterns
- Look for synchronous operations that could be async
- Find potential memory leaks
- Identify slow algorithms or data structures
- Look for missing caching opportunities
- Check for unnecessary network requests
Categories to use: "Database", "Rendering", "Memory", "Bundle Size", "Caching", "Algorithm", "Network"
${commonOutput}`;
default: // "features"
return `${commonIntro}
3. Generate feature suggestions:
- Think about what's missing compared to similar applications
- Consider user experience improvements
- Consider developer experience improvements
- Think about performance, security, and reliability
- Consider testing and documentation improvements
Categories to use: "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc.
${commonOutput}`;
}
}
/**
* Stop the current analysis
*/
stop() {
if (this.runningAnalysis && this.runningAnalysis.abortController) {
this.runningAnalysis.abortController.abort();
}
this.runningAnalysis = null;
}
}
module.exports = new FeatureSuggestionsService();

View File

@@ -1,185 +0,0 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const promptBuilder = require("./prompt-builder");
const contextManager = require("./context-manager");
const featureLoader = require("./feature-loader");
const mcpServerFactory = require("./mcp-server-factory");
/**
* Feature Verifier - Handles feature verification by running tests
*/
class FeatureVerifier {
/**
* Verify feature tests (runs tests and checks if they pass)
*/
async verifyFeatureTests(feature, projectPath, sendToRenderer, execution) {
console.log(
`[FeatureVerifier] Verifying tests for: ${feature.description}`
);
try {
const verifyMsg = `\n✅ Verifying tests for: ${feature.description}\n`;
await contextManager.writeToContextFile(
projectPath,
feature.id,
verifyMsg
);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying tests for: ${feature.description}`,
});
const abortController = new AbortController();
execution.abortController = abortController;
// Create custom MCP server with UpdateFeatureStatus tool
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
featureLoader.updateFeatureStatus.bind(featureLoader),
projectPath
);
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: await promptBuilder.getVerificationPrompt(projectPath),
maxTurns: 1000,
cwd: projectPath,
mcpServers: {
"automaker-tools": featureToolsServer,
},
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"mcp__automaker-tools__UpdateFeatureStatus",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
const prompt = await promptBuilder.buildVerificationPrompt(
feature,
projectPath
);
const runningTestsMsg =
"Running Playwright tests to verify feature implementation...\n";
await contextManager.writeToContextFile(
projectPath,
feature.id,
runningTestsMsg
);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: runningTestsMsg,
});
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let responseText = "";
for await (const msg of currentQuery) {
// Check if this specific feature was aborted
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
await contextManager.writeToContextFile(
projectPath,
feature.id,
block.text
);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await contextManager.writeToContextFile(
projectPath,
feature.id,
toolMsg
);
sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
// Re-load features to check if it was marked as verified or waiting_approval (for skipTests)
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
// For skipTests features, waiting_approval is also considered a success
const passes =
updatedFeature?.status === "verified" ||
(updatedFeature?.skipTests &&
updatedFeature?.status === "waiting_approval");
const finalMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Tests failed or not all passing - feature remains in progress\n";
await contextManager.writeToContextFile(
projectPath,
feature.id,
finalMsg
);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: finalMsg,
});
return {
passes,
message: responseText.substring(0, 500),
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[FeatureVerifier] Verification aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
passes: false,
message: "Verification aborted",
};
}
console.error("[FeatureVerifier] Error verifying feature:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
}
module.exports = new FeatureVerifier();

View File

@@ -1,76 +0,0 @@
const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk");
const { z } = require("zod");
const featureLoader = require("./feature-loader");
/**
* MCP Server Factory - Creates custom MCP servers with tools
*/
class McpServerFactory {
/**
* Create a custom MCP server with the UpdateFeatureStatus tool
* This tool allows Claude Code to safely update feature status without
* directly modifying feature files, preventing race conditions
* and accidental state corruption.
*/
createFeatureToolsServer(updateFeatureStatusCallback, projectPath) {
return createSdkMcpServer({
name: "automaker-tools",
version: "1.0.0",
tools: [
tool(
"UpdateFeatureStatus",
"Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.",
{
featureId: z.string().describe("The ID of the feature to update"),
status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically."),
summary: z.string().optional().describe("A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: 'Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx'")
},
async (args) => {
try {
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`);
// Load the feature to check skipTests flag
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === args.featureId);
if (!feature) {
throw new Error(`Feature ${args.featureId} not found`);
}
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
let finalStatus = args.status;
if (args.status === "verified" && feature.skipTests === true) {
console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`);
finalStatus = "waiting_approval";
}
// Call the provided callback to update feature status with summary
await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath, args.summary);
const statusMessage = finalStatus !== args.status
? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)${args.summary ? ` with summary: "${args.summary}"` : ""}`
: `Successfully updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` with summary: "${args.summary}"` : ""}`;
return {
content: [{
type: "text",
text: statusMessage
}]
};
} catch (error) {
console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error);
return {
content: [{
type: "text",
text: `Failed to update feature status: ${error.message}`
}]
};
}
}
)
]
});
}
}
module.exports = new McpServerFactory();

View File

@@ -1,349 +0,0 @@
#!/usr/bin/env node
/**
* Standalone STDIO MCP Server for Automaker Tools
*
* This script runs as a standalone process and communicates via JSON-RPC 2.0
* over stdin/stdout. It implements the MCP protocol to expose the UpdateFeatureStatus
* tool to Codex CLI.
*
* Environment variables:
* - AUTOMAKER_PROJECT_PATH: Path to the project directory
* - AUTOMAKER_IPC_CHANNEL: IPC channel name for callback communication (optional, uses default)
*/
const readline = require('readline');
const path = require('path');
// Redirect all console.log output to stderr to avoid polluting MCP stdout
const originalConsoleLog = console.log;
console.log = (...args) => {
console.error(...args);
};
// Set up readline interface for line-by-line JSON-RPC input
// IMPORTANT: Use a separate output stream for readline to avoid interfering with JSON-RPC stdout
// We'll write JSON-RPC responses directly to stdout, not through readline
const rl = readline.createInterface({
input: process.stdin,
output: null, // Don't use stdout for readline output
terminal: false
});
let initialized = false;
let projectPath = null;
let ipcChannel = null;
// Get configuration from environment
projectPath = process.env.AUTOMAKER_PROJECT_PATH || process.cwd();
ipcChannel = process.env.AUTOMAKER_IPC_CHANNEL || 'mcp:update-feature-status';
// Load dependencies (these will be available in the Electron app context)
let featureLoader;
let electron;
// Try to load Electron IPC if available (when running from Electron app)
try {
// In Electron, we can use IPC directly
if (typeof require !== 'undefined') {
// Check if we're in Electron context
const electronModule = require('electron');
if (electronModule && electronModule.ipcMain) {
electron = electronModule;
}
}
} catch (e) {
// Not in Electron context, will use alternative method
}
// Load feature loader
// Try multiple paths since this script might be run from different contexts
try {
// First try relative path (when run from electron/services/)
featureLoader = require('./feature-loader');
} catch (e) {
try {
// Try absolute path resolution
const featureLoaderPath = path.resolve(__dirname, 'feature-loader.js');
delete require.cache[require.resolve(featureLoaderPath)];
featureLoader = require(featureLoaderPath);
} catch (e2) {
// If still fails, try from parent directory
try {
featureLoader = require(path.join(__dirname, '..', 'services', 'feature-loader'));
} catch (e3) {
console.error('[McpServerStdio] Error loading feature-loader:', e3.message);
console.error('[McpServerStdio] Tried paths:', [
'./feature-loader',
path.resolve(__dirname, 'feature-loader.js'),
path.join(__dirname, '..', 'services', 'feature-loader')
]);
process.exit(1);
}
}
}
/**
* Send JSON-RPC response
* CRITICAL: Must write directly to stdout, not via console.log
* MCP protocol requires ONLY JSON-RPC messages on stdout
*/
function sendResponse(id, result, error = null) {
const response = {
jsonrpc: '2.0',
id
};
if (error) {
response.error = error;
} else {
response.result = result;
}
// Write directly to stdout with newline (MCP uses line-delimited JSON)
process.stdout.write(JSON.stringify(response) + '\n');
}
/**
* Send JSON-RPC notification
* CRITICAL: Must write directly to stdout, not via console.log
*/
function sendNotification(method, params) {
const notification = {
jsonrpc: '2.0',
method,
params
};
// Write directly to stdout with newline (MCP uses line-delimited JSON)
process.stdout.write(JSON.stringify(notification) + '\n');
}
/**
* Handle MCP initialize request
*/
async function handleInitialize(params, id) {
initialized = true;
sendResponse(id, {
protocolVersion: '2024-11-05',
capabilities: {
tools: {}
},
serverInfo: {
name: 'automaker-tools',
version: '1.0.0'
}
});
}
/**
* Handle tools/list request
*/
async function handleToolsList(params, id) {
sendResponse(id, {
tools: [
{
name: 'UpdateFeatureStatus',
description: 'Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'The ID of the feature to update'
},
status: {
type: 'string',
enum: ['backlog', 'in_progress', 'verified'],
description: 'The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically.'
},
summary: {
type: 'string',
description: 'A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: "Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx"'
}
},
required: ['featureId', 'status']
}
}
]
});
}
/**
* Handle tools/call request
*/
async function handleToolsCall(params, id) {
const { name, arguments: args } = params;
if (name !== 'UpdateFeatureStatus') {
sendResponse(id, null, {
code: -32601,
message: `Unknown tool: ${name}`
});
return;
}
try {
const { featureId, status, summary } = args;
if (!featureId || !status) {
sendResponse(id, null, {
code: -32602,
message: 'Missing required parameters: featureId and status are required'
});
return;
}
// Load the feature to check skipTests flag
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
sendResponse(id, null, {
code: -32602,
message: `Feature ${featureId} not found`
});
return;
}
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
let finalStatus = status;
if (status === 'verified' && feature.skipTests === true) {
finalStatus = 'waiting_approval';
}
// Call the update callback via IPC or direct call
// Since we're in a separate process, we need to use IPC to communicate back
// For now, we'll call the feature loader directly since it has the update method
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, summary);
const statusMessage = finalStatus !== status
? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}`
: `Successfully updated feature ${featureId} to status "${finalStatus}"${summary ? ` with summary: "${summary}"` : ''}`;
sendResponse(id, {
content: [
{
type: 'text',
text: statusMessage
}
]
});
} catch (error) {
console.error('[McpServerStdio] UpdateFeatureStatus error:', error);
sendResponse(id, null, {
code: -32603,
message: `Failed to update feature status: ${error.message}`
});
}
}
/**
* Handle JSON-RPC request
*/
async function handleRequest(line) {
let request;
try {
request = JSON.parse(line);
} catch (e) {
sendResponse(null, null, {
code: -32700,
message: 'Parse error'
});
return;
}
// Validate JSON-RPC 2.0 structure
if (request.jsonrpc !== '2.0') {
sendResponse(request.id || null, null, {
code: -32600,
message: 'Invalid Request'
});
return;
}
const { method, params, id } = request;
// Handle notifications (no id)
if (id === undefined) {
// Handle notifications if needed
return;
}
// Handle requests
try {
switch (method) {
case 'initialize':
await handleInitialize(params, id);
break;
case 'tools/list':
if (!initialized) {
sendResponse(id, null, {
code: -32002,
message: 'Server not initialized'
});
return;
}
await handleToolsList(params, id);
break;
case 'tools/call':
if (!initialized) {
sendResponse(id, null, {
code: -32002,
message: 'Server not initialized'
});
return;
}
await handleToolsCall(params, id);
break;
default:
sendResponse(id, null, {
code: -32601,
message: `Method not found: ${method}`
});
}
} catch (error) {
console.error('[McpServerStdio] Error handling request:', error);
sendResponse(id, null, {
code: -32603,
message: `Internal error: ${error.message}`
});
}
}
// Process stdin line by line
rl.on('line', async (line) => {
if (!line.trim()) {
return;
}
await handleRequest(line);
});
// Handle errors
rl.on('error', (error) => {
console.error('[McpServerStdio] Readline error:', error);
process.exit(1);
});
// Handle process termination
process.on('SIGTERM', () => {
rl.close();
process.exit(0);
});
process.on('SIGINT', () => {
rl.close();
process.exit(0);
});
// Log startup
console.error('[McpServerStdio] Starting MCP server for automaker-tools');
console.error(`[McpServerStdio] Project path: ${projectPath}`);
console.error(`[McpServerStdio] IPC channel: ${ipcChannel}`);

View File

@@ -1,524 +0,0 @@
/**
* Model Provider Abstraction Layer
*
* This module provides an abstract interface for model providers (Claude, Codex, etc.)
* allowing the application to use different AI models through a unified API.
*/
/**
* Base class for model providers
* Concrete implementations should extend this class
*/
class ModelProvider {
constructor(config = {}) {
this.config = config;
this.name = 'base';
}
/**
* Get provider name
* @returns {string} Provider name
*/
getName() {
return this.name;
}
/**
* Execute a query with the model provider
* @param {Object} options Query options
* @param {string} options.prompt The prompt to send
* @param {string} options.model The model to use
* @param {string} options.systemPrompt System prompt
* @param {string} options.cwd Working directory
* @param {number} options.maxTurns Maximum turns
* @param {string[]} options.allowedTools Allowed tools
* @param {Object} options.mcpServers MCP servers configuration
* @param {AbortController} options.abortController Abort controller
* @param {Object} options.thinking Thinking configuration
* @returns {AsyncGenerator} Async generator yielding messages
*/
async *executeQuery(options) {
throw new Error('executeQuery must be implemented by subclass');
}
/**
* Detect if this provider's CLI/SDK is installed
* @returns {Promise<Object>} Installation status
*/
async detectInstallation() {
throw new Error('detectInstallation must be implemented by subclass');
}
/**
* Get list of available models for this provider
* @returns {Array<Object>} Array of model definitions
*/
getAvailableModels() {
throw new Error('getAvailableModels must be implemented by subclass');
}
/**
* Validate provider configuration
* @returns {Object} Validation result { valid: boolean, errors: string[] }
*/
validateConfig() {
throw new Error('validateConfig must be implemented by subclass');
}
/**
* Get the full model string for a model key
* @param {string} modelKey Short model key (e.g., 'opus', 'gpt-5.1-codex')
* @returns {string} Full model string
*/
getModelString(modelKey) {
throw new Error('getModelString must be implemented by subclass');
}
/**
* Check if provider supports a specific feature
* @param {string} feature Feature name (e.g., 'thinking', 'tools', 'streaming')
* @returns {boolean} Whether the feature is supported
*/
supportsFeature(feature) {
return false;
}
}
/**
* Claude Provider - Uses Anthropic Claude Agent SDK
*/
class ClaudeProvider extends ModelProvider {
constructor(config = {}) {
super(config);
this.name = 'claude';
this.sdk = null;
}
/**
* Try to load credentials from the app's own credentials.json file.
* This is where we store OAuth tokens and API keys that users enter in the setup wizard.
* Returns { oauthToken, apiKey } or null values if not found.
*/
loadTokenFromAppCredentials() {
try {
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
const credentialsPath = path.join(app.getPath('userData'), 'credentials.json');
if (!fs.existsSync(credentialsPath)) {
console.log('[ClaudeProvider] App credentials file does not exist:', credentialsPath);
return { oauthToken: null, apiKey: null };
}
const raw = fs.readFileSync(credentialsPath, 'utf-8');
const parsed = JSON.parse(raw);
// Check for OAuth token first (from claude setup-token), then API key
const oauthToken = parsed.anthropic_oauth_token || null;
const apiKey = parsed.anthropic || parsed.anthropic_api_key || null;
console.log('[ClaudeProvider] App credentials check - OAuth token:', !!oauthToken, ', API key:', !!apiKey);
return { oauthToken, apiKey };
} catch (err) {
console.warn('[ClaudeProvider] Failed to read app credentials:', err?.message);
return { oauthToken: null, apiKey: null };
}
}
/**
* Try to load a Claude OAuth token from the local CLI config (~/.claude/config.json).
* Returns the token string or null if not found.
* NOTE: Claude's credentials.json is encrypted, so we only try config.json
*/
loadTokenFromCliConfig() {
try {
const fs = require('fs');
const path = require('path');
const configPath = path.join(require('os').homedir(), '.claude', 'config.json');
if (!fs.existsSync(configPath)) {
return null;
}
const raw = fs.readFileSync(configPath, 'utf-8');
const parsed = JSON.parse(raw);
// CLI config stores token as oauth_token (newer) or token (older)
return parsed.oauth_token || parsed.token || null;
} catch (err) {
console.warn('[ClaudeProvider] Failed to read CLI config token:', err?.message);
return null;
}
}
ensureAuthEnv() {
// If API key or token already present in environment, keep as-is.
if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN) {
console.log('[ClaudeProvider] Auth already present in environment');
return true;
}
// Priority 1: Try to load from app's own credentials (setup wizard)
const appCredentials = this.loadTokenFromAppCredentials();
if (appCredentials.oauthToken) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = appCredentials.oauthToken;
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from app credentials');
return true;
}
if (appCredentials.apiKey) {
process.env.ANTHROPIC_API_KEY = appCredentials.apiKey;
console.log('[ClaudeProvider] Loaded ANTHROPIC_API_KEY from app credentials');
return true;
}
// Priority 2: Try to hydrate from CLI login config (legacy)
const token = this.loadTokenFromCliConfig();
if (token) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from ~/.claude/config.json');
return true;
}
// Check if CLI is installed but not logged in
try {
const claudeCliDetector = require('./claude-cli-detector');
const detection = claudeCliDetector.detectClaudeInstallation();
if (detection.installed && detection.method === 'cli') {
console.error('[ClaudeProvider] Claude CLI is installed but not authenticated. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.');
} else {
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
}
} catch (err) {
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
}
return false;
}
/**
* Lazily load the Claude SDK
*/
loadSdk() {
if (!this.sdk) {
this.sdk = require('@anthropic-ai/claude-agent-sdk');
}
return this.sdk;
}
async *executeQuery(options) {
// Ensure we have auth; fall back to app credentials or CLI login token if available.
if (!this.ensureAuthEnv()) {
// Check if CLI is installed to provide better error message
let msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.';
try {
const claudeCliDetector = require('./claude-cli-detector');
const detection = claudeCliDetector.detectClaudeInstallation();
if (detection.installed && detection.method === 'cli') {
msg = 'Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.';
} else {
msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.';
}
} catch (err) {
// Fallback to default message
}
console.error(`[ClaudeProvider] ${msg}`);
yield { type: 'error', error: msg };
return;
}
const { query } = this.loadSdk();
const sdkOptions = {
model: options.model,
systemPrompt: options.systemPrompt,
maxTurns: options.maxTurns || 1000,
cwd: options.cwd,
mcpServers: options.mcpServers,
allowedTools: options.allowedTools,
permissionMode: options.permissionMode || 'acceptEdits',
sandbox: options.sandbox,
abortController: options.abortController,
};
// Add thinking configuration if enabled
if (options.thinking) {
sdkOptions.thinking = options.thinking;
}
const currentQuery = query({ prompt: options.prompt, options: sdkOptions });
for await (const msg of currentQuery) {
yield msg;
}
}
async detectInstallation() {
const claudeCliDetector = require('./claude-cli-detector');
return claudeCliDetector.getFullStatus();
}
getAvailableModels() {
return [
{
id: 'haiku',
name: 'Claude Haiku',
modelString: 'claude-haiku-4-5',
provider: 'claude',
description: 'Fast and efficient for simple tasks',
tier: 'basic'
},
{
id: 'sonnet',
name: 'Claude Sonnet',
modelString: 'claude-sonnet-4-20250514',
provider: 'claude',
description: 'Balanced performance and capabilities',
tier: 'standard'
},
{
id: 'opus',
name: 'Claude Opus 4.5',
modelString: 'claude-opus-4-5-20251101',
provider: 'claude',
description: 'Most capable model for complex tasks',
tier: 'premium'
}
];
}
validateConfig() {
const errors = [];
// Ensure auth is available (try to auto-load from app credentials or CLI config)
this.ensureAuthEnv();
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN && !process.env.ANTHROPIC_API_KEY) {
errors.push('No Claude authentication found. Go to Settings > Setup to configure your subscription token or API key.');
}
return {
valid: errors.length === 0,
errors
};
}
getModelString(modelKey) {
const modelMap = {
haiku: 'claude-haiku-4-5',
sonnet: 'claude-sonnet-4-20250514',
opus: 'claude-opus-4-5-20251101'
};
return modelMap[modelKey] || modelMap.opus;
}
supportsFeature(feature) {
const supportedFeatures = ['thinking', 'tools', 'streaming', 'mcp'];
return supportedFeatures.includes(feature);
}
}
/**
* Codex Provider - Uses OpenAI Codex CLI
*/
class CodexProvider extends ModelProvider {
constructor(config = {}) {
super(config);
this.name = 'codex';
}
async *executeQuery(options) {
const codexExecutor = require('./codex-executor');
// Validate that we're not receiving a Claude model string
if (options.model && options.model.startsWith('claude-')) {
const errorMsg = `Codex provider cannot use Claude model '${options.model}'. Codex only supports OpenAI models (gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5.1).`;
console.error(`[CodexProvider] ${errorMsg}`);
yield {
type: 'error',
error: errorMsg
};
return;
}
const executeOptions = {
prompt: options.prompt,
model: options.model,
cwd: options.cwd,
systemPrompt: options.systemPrompt,
maxTurns: options.maxTurns || 20,
allowedTools: options.allowedTools,
mcpServers: options.mcpServers, // Pass MCP servers config to executor
env: {
...process.env,
OPENAI_API_KEY: process.env.OPENAI_API_KEY
}
};
// Execute and yield results
const generator = codexExecutor.execute(executeOptions);
for await (const msg of generator) {
yield msg;
}
}
async detectInstallation() {
const codexCliDetector = require('./codex-cli-detector');
return codexCliDetector.getInstallationInfo();
}
getAvailableModels() {
return [
{
id: 'gpt-5.1-codex-max',
name: 'GPT-5.1 Codex Max',
modelString: 'gpt-5.1-codex-max',
provider: 'codex',
description: 'Latest flagship - deep and fast reasoning for coding',
tier: 'premium',
default: true
},
{
id: 'gpt-5.1-codex',
name: 'GPT-5.1 Codex',
modelString: 'gpt-5.1-codex',
provider: 'codex',
description: 'Optimized for code generation',
tier: 'standard'
},
{
id: 'gpt-5.1-codex-mini',
name: 'GPT-5.1 Codex Mini',
modelString: 'gpt-5.1-codex-mini',
provider: 'codex',
description: 'Faster and cheaper option',
tier: 'basic'
},
{
id: 'gpt-5.1',
name: 'GPT-5.1',
modelString: 'gpt-5.1',
provider: 'codex',
description: 'Broad world knowledge with strong reasoning',
tier: 'standard'
}
];
}
validateConfig() {
const errors = [];
const codexCliDetector = require('./codex-cli-detector');
const installation = codexCliDetector.detectCodexInstallation();
if (!installation.installed && !process.env.OPENAI_API_KEY) {
errors.push('Codex CLI not installed and no OPENAI_API_KEY found.');
}
return {
valid: errors.length === 0,
errors
};
}
getModelString(modelKey) {
// Codex models use the key directly as the model string
const modelMap = {
'gpt-5.1-codex-max': 'gpt-5.1-codex-max',
'gpt-5.1-codex': 'gpt-5.1-codex',
'gpt-5.1-codex-mini': 'gpt-5.1-codex-mini',
'gpt-5.1': 'gpt-5.1'
};
return modelMap[modelKey] || 'gpt-5.1-codex-max';
}
supportsFeature(feature) {
const supportedFeatures = ['tools', 'streaming'];
return supportedFeatures.includes(feature);
}
}
/**
* Model Provider Factory
* Creates the appropriate provider based on model or provider name
*/
class ModelProviderFactory {
static providers = {
claude: ClaudeProvider,
codex: CodexProvider
};
/**
* Get provider for a specific model
* @param {string} modelId Model ID (e.g., 'opus', 'gpt-5.1-codex')
* @returns {ModelProvider} Provider instance
*/
static getProviderForModel(modelId) {
// Check if it's a Claude model
const claudeModels = ['haiku', 'sonnet', 'opus'];
if (claudeModels.includes(modelId)) {
return new ClaudeProvider();
}
// Check if it's a Codex/OpenAI model
const codexModels = [
'gpt-5.1-codex-max', 'gpt-5.1-codex', 'gpt-5.1-codex-mini', 'gpt-5.1'
];
if (codexModels.includes(modelId)) {
return new CodexProvider();
}
// Default to Claude
return new ClaudeProvider();
}
/**
* Get provider by name
* @param {string} providerName Provider name ('claude' or 'codex')
* @returns {ModelProvider} Provider instance
*/
static getProvider(providerName) {
const ProviderClass = this.providers[providerName];
if (!ProviderClass) {
throw new Error(`Unknown provider: ${providerName}`);
}
return new ProviderClass();
}
/**
* Get all available providers
* @returns {string[]} List of provider names
*/
static getAvailableProviders() {
return Object.keys(this.providers);
}
/**
* Get all available models across all providers
* @returns {Array<Object>} All available models
*/
static getAllModels() {
const allModels = [];
for (const providerName of this.getAvailableProviders()) {
const provider = this.getProvider(providerName);
const models = provider.getAvailableModels();
allModels.push(...models);
}
return allModels;
}
/**
* Check installation status for all providers
* @returns {Promise<Object>} Installation status for each provider
*/
static async checkAllProviders() {
const status = {};
for (const providerName of this.getAvailableProviders()) {
const provider = this.getProvider(providerName);
status[providerName] = await provider.detectInstallation();
}
return status;
}
}
module.exports = {
ModelProvider,
ClaudeProvider,
CodexProvider,
ModelProviderFactory
};

View File

@@ -1,320 +0,0 @@
/**
* Model Registry - Centralized model definitions and metadata
*
* This module provides a central registry of all available models
* across different providers (Claude, Codex/OpenAI).
*/
/**
* Model Categories
*/
const MODEL_CATEGORIES = {
CLAUDE: 'claude',
OPENAI: 'openai',
CODEX: 'codex'
};
/**
* Model Tiers (capability levels)
*/
const MODEL_TIERS = {
BASIC: 'basic', // Fast, cheap, simple tasks
STANDARD: 'standard', // Balanced performance
PREMIUM: 'premium' // Most capable, complex tasks
};
const CODEX_MODEL_IDS = [
'gpt-5.1-codex-max',
'gpt-5.1-codex',
'gpt-5.1-codex-mini',
'gpt-5.1'
];
/**
* All available models with full metadata
*/
const MODELS = {
// Claude Models
haiku: {
id: 'haiku',
name: 'Claude Haiku',
modelString: 'claude-haiku-4-5',
provider: 'claude',
category: MODEL_CATEGORIES.CLAUDE,
tier: MODEL_TIERS.BASIC,
description: 'Fast and efficient for simple tasks',
capabilities: ['code', 'text', 'tools'],
maxTokens: 8192,
contextWindow: 200000,
supportsThinking: true,
requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN'
},
sonnet: {
id: 'sonnet',
name: 'Claude Sonnet',
modelString: 'claude-sonnet-4-20250514',
provider: 'claude',
category: MODEL_CATEGORIES.CLAUDE,
tier: MODEL_TIERS.STANDARD,
description: 'Balanced performance and capabilities',
capabilities: ['code', 'text', 'tools', 'analysis'],
maxTokens: 8192,
contextWindow: 200000,
supportsThinking: true,
requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN'
},
opus: {
id: 'opus',
name: 'Claude Opus 4.5',
modelString: 'claude-opus-4-5-20251101',
provider: 'claude',
category: MODEL_CATEGORIES.CLAUDE,
tier: MODEL_TIERS.PREMIUM,
description: 'Most capable model for complex tasks',
capabilities: ['code', 'text', 'tools', 'analysis', 'reasoning'],
maxTokens: 8192,
contextWindow: 200000,
supportsThinking: true,
requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN',
default: true
},
// OpenAI GPT-5.1 Codex Models
'gpt-5.1-codex-max': {
id: 'gpt-5.1-codex-max',
name: 'GPT-5.1 Codex Max',
modelString: 'gpt-5.1-codex-max',
provider: 'codex',
category: MODEL_CATEGORIES.OPENAI,
tier: MODEL_TIERS.PREMIUM,
description: 'Latest flagship - deep and fast reasoning for coding',
capabilities: ['code', 'text', 'tools', 'reasoning'],
maxTokens: 32768,
contextWindow: 128000,
supportsThinking: false,
requiresAuth: 'OPENAI_API_KEY',
codexDefault: true
},
'gpt-5.1-codex': {
id: 'gpt-5.1-codex',
name: 'GPT-5.1 Codex',
modelString: 'gpt-5.1-codex',
provider: 'codex',
category: MODEL_CATEGORIES.OPENAI,
tier: MODEL_TIERS.STANDARD,
description: 'Optimized for code generation',
capabilities: ['code', 'text', 'tools'],
maxTokens: 32768,
contextWindow: 128000,
supportsThinking: false,
requiresAuth: 'OPENAI_API_KEY'
},
'gpt-5.1-codex-mini': {
id: 'gpt-5.1-codex-mini',
name: 'GPT-5.1 Codex Mini',
modelString: 'gpt-5.1-codex-mini',
provider: 'codex',
category: MODEL_CATEGORIES.OPENAI,
tier: MODEL_TIERS.BASIC,
description: 'Faster and cheaper option',
capabilities: ['code', 'text'],
maxTokens: 16384,
contextWindow: 128000,
supportsThinking: false,
requiresAuth: 'OPENAI_API_KEY'
},
'gpt-5.1': {
id: 'gpt-5.1',
name: 'GPT-5.1',
modelString: 'gpt-5.1',
provider: 'codex',
category: MODEL_CATEGORIES.OPENAI,
tier: MODEL_TIERS.STANDARD,
description: 'Broad world knowledge with strong reasoning',
capabilities: ['code', 'text', 'reasoning'],
maxTokens: 32768,
contextWindow: 128000,
supportsThinking: false,
requiresAuth: 'OPENAI_API_KEY'
}
};
/**
* Model Registry class for querying and managing models
*/
class ModelRegistry {
/**
* Get all registered models
* @returns {Object} All models
*/
static getAllModels() {
return MODELS;
}
/**
* Get model by ID
* @param {string} modelId Model ID
* @returns {Object|null} Model definition or null
*/
static getModel(modelId) {
return MODELS[modelId] || null;
}
/**
* Get models by provider
* @param {string} provider Provider name ('claude' or 'codex')
* @returns {Object[]} Array of models for the provider
*/
static getModelsByProvider(provider) {
return Object.values(MODELS).filter(m => m.provider === provider);
}
/**
* Get models by category
* @param {string} category Category name
* @returns {Object[]} Array of models in the category
*/
static getModelsByCategory(category) {
return Object.values(MODELS).filter(m => m.category === category);
}
/**
* Get models by tier
* @param {string} tier Tier name
* @returns {Object[]} Array of models in the tier
*/
static getModelsByTier(tier) {
return Object.values(MODELS).filter(m => m.tier === tier);
}
/**
* Get default model for a provider
* @param {string} provider Provider name
* @returns {Object|null} Default model or null
*/
static getDefaultModel(provider = 'claude') {
const models = this.getModelsByProvider(provider);
if (provider === 'claude') {
return models.find(m => m.default) || models[0];
}
if (provider === 'codex') {
return models.find(m => m.codexDefault) || models[0];
}
return models[0];
}
/**
* Get model string (full model name) for a model ID
* @param {string} modelId Model ID
* @returns {string} Full model string
*/
static getModelString(modelId) {
const model = this.getModel(modelId);
return model ? model.modelString : modelId;
}
/**
* Determine provider for a model ID
* @param {string} modelId Model ID
* @returns {string} Provider name ('claude' or 'codex')
*/
static getProviderForModel(modelId) {
const model = this.getModel(modelId);
if (model) {
return model.provider;
}
// Fallback detection for models not explicitly registered (keeps legacy Codex IDs working)
if (CODEX_MODEL_IDS.includes(modelId)) {
return 'codex';
}
return 'claude';
}
/**
* Check if a model is a Claude model
* @param {string} modelId Model ID
* @returns {boolean} Whether it's a Claude model
*/
static isClaudeModel(modelId) {
return this.getProviderForModel(modelId) === 'claude';
}
/**
* Check if a model is a Codex/OpenAI model
* @param {string} modelId Model ID
* @returns {boolean} Whether it's a Codex model
*/
static isCodexModel(modelId) {
return this.getProviderForModel(modelId) === 'codex';
}
/**
* Get models grouped by provider for UI display
* @returns {Object} Models grouped by provider
*/
static getModelsGroupedByProvider() {
return {
claude: this.getModelsByProvider('claude'),
codex: this.getModelsByProvider('codex')
};
}
/**
* Get all model IDs as an array
* @returns {string[]} Array of model IDs
*/
static getAllModelIds() {
return Object.keys(MODELS);
}
/**
* Check if model supports a specific capability
* @param {string} modelId Model ID
* @param {string} capability Capability name
* @returns {boolean} Whether the model supports the capability
*/
static modelSupportsCapability(modelId, capability) {
const model = this.getModel(modelId);
return model ? model.capabilities.includes(capability) : false;
}
/**
* Check if model supports extended thinking
* @param {string} modelId Model ID
* @returns {boolean} Whether the model supports thinking
*/
static modelSupportsThinking(modelId) {
const model = this.getModel(modelId);
return model ? model.supportsThinking : false;
}
/**
* Get required authentication for a model
* @param {string} modelId Model ID
* @returns {string|null} Required auth env variable name
*/
static getRequiredAuth(modelId) {
const model = this.getModel(modelId);
return model ? model.requiresAuth : null;
}
/**
* Check if authentication is available for a model
* @param {string} modelId Model ID
* @returns {boolean} Whether auth is available
*/
static hasAuthForModel(modelId) {
const authVar = this.getRequiredAuth(modelId);
if (!authVar) return false;
return !!process.env[authVar];
}
}
module.exports = {
MODEL_CATEGORIES,
MODEL_TIERS,
MODELS,
ModelRegistry
};

View File

@@ -1,112 +0,0 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const promptBuilder = require("./prompt-builder");
/**
* Project Analyzer - Scans codebase and updates app_spec.txt
*/
class ProjectAnalyzer {
/**
* Run the project analysis using Claude Agent SDK
*/
async runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution) {
console.log(`[ProjectAnalyzer] Running project analysis for: ${projectPath}`);
try {
sendToRenderer({
type: "auto_mode_phase",
featureId: analysisId,
phase: "planning",
message: "Scanning project structure...",
});
const abortController = new AbortController();
execution.abortController = abortController;
const options = {
model: "claude-sonnet-4-20250514",
systemPrompt: promptBuilder.getProjectAnalysisSystemPrompt(),
maxTurns: 50,
cwd: projectPath,
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
const prompt = promptBuilder.buildProjectAnalysisPrompt(projectPath);
sendToRenderer({
type: "auto_mode_progress",
featureId: analysisId,
content: "Starting project analysis...\n",
});
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let responseText = "";
for await (const msg of currentQuery) {
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
sendToRenderer({
type: "auto_mode_progress",
featureId: analysisId,
content: block.text,
});
} else if (block.type === "tool_use") {
sendToRenderer({
type: "auto_mode_tool",
featureId: analysisId,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
sendToRenderer({
type: "auto_mode_phase",
featureId: analysisId,
phase: "verification",
message: "Project analysis complete",
});
return {
success: true,
message: "Project analyzed successfully",
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[ProjectAnalyzer] Project analysis aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
success: false,
message: "Analysis aborted",
};
}
console.error("[ProjectAnalyzer] Error in project analysis:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
}
module.exports = new ProjectAnalyzer();

View File

@@ -1,787 +0,0 @@
const contextManager = require("./context-manager");
/**
* Prompt Builder - Generates prompts for different agent tasks
*/
class PromptBuilder {
/**
* Build the prompt for implementing a specific feature
*/
async buildFeaturePrompt(feature, projectPath) {
const skipTestsNote = feature.skipTests
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
: "";
let imagesNote = "";
if (feature.imagePaths && feature.imagePaths.length > 0) {
const imagesList = feature.imagePaths
.map(
(img, idx) =>
` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${
img.path
}`
)
.join("\n");
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
${imagesList}
You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`;
}
// Get context files preview
const contextFilesPreview = await contextManager.getContextFilesPreview(
projectPath
);
// Get memory content (lessons learned from previous runs)
const memoryContent = await contextManager.getMemoryContent(projectPath);
// Build mode header for this feature
const modeHeader = feature.skipTests
? `**🔨 MODE: Manual Review (No Automated Tests)**
This feature is set for manual review - focus on clean implementation without automated tests.`
: `**🧪 MODE: Test-Driven Development (TDD)**
This feature requires automated Playwright tests to verify the implementation.`;
return `You are working on a feature implementation task.
${modeHeader}
${memoryContent}
**Current Feature to Implement:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
${skipTestsNote}${imagesNote}${contextFilesPreview}
**Steps to Complete:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the project files to understand the current codebase structure
2. Implement the feature according to the description and steps
${
feature.skipTests
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
: "3. Write Playwright tests to verify the feature works correctly\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"
}
${
feature.skipTests ? "4" : "6"
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified**
${
feature.skipTests
? "5. **DO NOT commit changes** - the user will review and commit manually"
: "7. Commit your changes with git"
}
**IMPORTANT - Updating Feature Status:**
When you have completed the feature${
feature.skipTests ? "" : " and all tests pass"
}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
- Call the tool with: featureId="${feature.id}" and status="verified"
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
- **DO NOT manually edit feature files** - this can cause race conditions
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior
**IMPORTANT - Feature Summary (REQUIRED):**
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
- What files were modified/created
- What functionality was added or changed
- Any notable implementation decisions
Example:
\`\`\`
UpdateFeatureStatus(featureId="${
feature.id
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
\`\`\`
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
**Important Guidelines:**
- Focus ONLY on implementing this specific feature
- Write clean, production-quality code
- Add proper error handling
${
feature.skipTests
? "- Skip automated testing (skipTests=true) - user will manually verify"
: "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle"
}
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly**
- **CRITICAL: Always include a summary when marking feature as verified**
${
feature.skipTests
? "- **DO NOT commit changes** - user will review and commit manually"
: "- Make a git commit when complete"
}
**Testing Utilities (CRITICAL):**
1. **Create/maintain tests/utils.ts** - Add helper functions for finding elements and common test operations
2. **Use utilities in tests** - Import and use helper functions instead of repeating selectors
3. **Add utilities as needed** - When you write a test, if you need a new helper, add it to utils.ts
4. **Update utilities when functionality changes** - If you modify components, update corresponding utilities
Example utilities to add:
- getByTestId(page, testId) - Find elements by data-testid
- getButtonByText(page, text) - Find buttons by text
- clickElement(page, testId) - Click an element by test ID
- fillForm(page, formData) - Fill form fields
- waitForElement(page, testId) - Wait for element to appear
This makes future tests easier to write and maintain!
**Test Deletion Policy:**
After tests pass, delete them immediately:
\`\`\`bash
rm tests/[feature-name].spec.ts
\`\`\`
Begin by reading the project structure and then implementing the feature.`;
}
/**
* Build the prompt for verifying a specific feature
*/
async buildVerificationPrompt(feature, projectPath) {
const skipTestsNote = feature.skipTests
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
: "";
let imagesNote = "";
if (feature.imagePaths && feature.imagePaths.length > 0) {
const imagesList = feature.imagePaths
.map(
(img, idx) =>
` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${
img.path
}`
)
.join("\n");
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
${imagesList}
You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`;
}
// Get context files preview
const contextFilesPreview = await contextManager.getContextFilesPreview(
projectPath
);
// Get memory content (lessons learned from previous runs)
const memoryContent = await contextManager.getMemoryContent(projectPath);
// Build mode header for this feature
const modeHeader = feature.skipTests
? `**🔨 MODE: Manual Review (No Automated Tests)**
This feature is set for manual review - focus on completing implementation without automated tests.`
: `**🧪 MODE: Test-Driven Development (TDD)**
This feature requires automated Playwright tests to verify the implementation.`;
return `You are implementing and verifying a feature until it is complete and working correctly.
${modeHeader}
${memoryContent}
**Feature to Implement/Verify:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
Current Status: ${feature.status}
${skipTestsNote}${imagesNote}${contextFilesPreview}
**Steps that should be implemented:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the project files to understand the current implementation
2. If the feature is not fully implemented, continue implementing it
${
feature.skipTests
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
: `3. Write or update Playwright tests to verify the feature works correctly
4. Run the Playwright tests: npx playwright test tests/[feature-name].spec.ts
5. Check if all tests pass
6. **If ANY tests fail:**
- Analyze the test failures and error messages
- Fix the implementation code to make the tests pass
- Update test utilities in tests/utils.ts if needed
- Re-run the tests to verify the fixes
- **REPEAT this process until ALL tests pass**
7. **If ALL tests pass:**
- **DELETE the test file(s) for this feature** - tests are only for immediate verification`
}
${
feature.skipTests ? "4" : "8"
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified**
${
feature.skipTests
? "5. **DO NOT commit changes** - the user will review and commit manually"
: "9. Explain what was implemented/fixed and that all tests passed\n10. Commit your changes with git"
}
**IMPORTANT - Updating Feature Status:**
When you have completed the feature${
feature.skipTests ? "" : " and all tests pass"
}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
- Call the tool with: featureId="${feature.id}" and status="verified"
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
- **DO NOT manually edit feature files** - this can cause race conditions
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior
**IMPORTANT - Feature Summary (REQUIRED):**
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
- What files were modified/created
- What functionality was added or changed
- Any notable implementation decisions
Example:
\`\`\`
UpdateFeatureStatus(featureId="${
feature.id
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
\`\`\`
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
**Testing Utilities:**
- Check if tests/utils.ts exists and is being used
- If utilities are outdated due to functionality changes, update them
- Add new utilities as needed for this feature's tests
- Ensure test utilities stay in sync with code changes
**Test Deletion Policy:**
After tests pass, delete them immediately:
\`\`\`bash
rm tests/[feature-name].spec.ts
\`\`\`
**Important:**
${
feature.skipTests
? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually"
: "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues"
}
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly**
- **CRITICAL: Always include a summary when marking feature as verified**
Begin by reading the project structure and understanding what needs to be implemented or fixed.`;
}
/**
* Build prompt for resuming feature with previous context
*/
async buildResumePrompt(feature, previousContext, projectPath) {
const skipTestsNote = feature.skipTests
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
: "";
// For resume, check both followUpImages and imagePaths
const imagePaths = feature.followUpImages || feature.imagePaths;
let imagesNote = "";
if (imagePaths && imagePaths.length > 0) {
const imagesList = imagePaths
.map((img, idx) => {
// Handle both FeatureImagePath objects and simple path strings
const path = typeof img === "string" ? img : img.path;
const filename =
typeof img === "string" ? path.split("/").pop() : img.filename;
const mimeType = typeof img === "string" ? "image/*" : img.mimeType;
return ` ${
idx + 1
}. ${filename} (${mimeType})\n Path: ${path}`;
})
.join("\n");
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
${imagesList}
You can use the Read tool to view these images at any time. Review them carefully.\n`;
}
// Get context files preview
const contextFilesPreview = await contextManager.getContextFilesPreview(
projectPath
);
// Get memory content (lessons learned from previous runs)
const memoryContent = await contextManager.getMemoryContent(projectPath);
// Build mode header for this feature
const modeHeader = feature.skipTests
? `**🔨 MODE: Manual Review (No Automated Tests)**
This feature is set for manual review - focus on clean implementation without automated tests.`
: `**🧪 MODE: Test-Driven Development (TDD)**
This feature requires automated Playwright tests to verify the implementation.`;
return `You are resuming work on a feature implementation that was previously started.
${modeHeader}
${memoryContent}
**Current Feature:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
${skipTestsNote}${imagesNote}${contextFilesPreview}
**Steps to Complete:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Previous Work Context:**
${previousContext || "No previous context available - this is a fresh start."}
**Your Task:**
Continue where you left off and complete the feature implementation:
1. Review the previous work context above to understand what has been done
2. Continue implementing the feature according to the description and steps
${
feature.skipTests
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
: "3. Write Playwright tests to verify the feature works correctly (if not already done)\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"
}
${
feature.skipTests ? "4" : "6"
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified**
${
feature.skipTests
? "5. **DO NOT commit changes** - the user will review and commit manually"
: "7. Commit your changes with git"
}
**IMPORTANT - Updating Feature Status:**
When you have completed the feature${
feature.skipTests ? "" : " and all tests pass"
}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
- Call the tool with: featureId="${feature.id}" and status="verified"
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
- **DO NOT manually edit feature files** - this can cause race conditions
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior
**IMPORTANT - Feature Summary (REQUIRED):**
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
- What files were modified/created
- What functionality was added or changed
- Any notable implementation decisions
Example:
\`\`\`
UpdateFeatureStatus(featureId="${
feature.id
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
\`\`\`
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
**Important Guidelines:**
- Review what was already done in the previous context
- Don't redo work that's already complete - continue from where it left off
- Focus on completing any remaining tasks
${
feature.skipTests
? "- Skip automated testing (skipTests=true) - user will manually verify"
: "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**"
}
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly**
- **CRITICAL: Always include a summary when marking feature as verified**
${
feature.skipTests
? "- **DO NOT commit changes** - user will review and commit manually"
: "- Make a git commit when complete"
}
Begin by assessing what's been done and what remains to be completed.`;
}
/**
* Build the prompt for project analysis
*/
buildProjectAnalysisPrompt(projectPath) {
return `You are analyzing a new project that was just opened in Automaker, an autonomous AI development studio.
**Your Task:**
Analyze this project's codebase and update the .automaker/app_spec.txt file with accurate information about:
1. **Project Name** - Detect the name from package.json, README, or directory name
2. **Overview** - Brief description of what the project does
3. **Technology Stack** - Languages, frameworks, libraries detected
4. **Core Capabilities** - Main features and functionality
5. **Implemented Features** - What features are already built
6. **Implementation Roadmap** - Break down remaining work into phases with individual features
**Steps to Follow:**
1. First, explore the project structure:
- Look at package.json, cargo.toml, go.mod, requirements.txt, etc. for tech stack
- Check README.md for project description
- List key directories (src, lib, components, etc.)
2. Identify the tech stack:
- Frontend framework (React, Vue, Next.js, etc.)
- Backend framework (Express, FastAPI, etc.)
- Database (if any config files exist)
- Testing framework
- Build tools
3. Update .automaker/app_spec.txt with your findings in this format:
\`\`\`xml
<project_specification>
<project_name>Detected Name</project_name>
<overview>
Clear description of what this project does based on your analysis.
</overview>
<technology_stack>
<frontend>
<framework>Framework Name</framework>
<!-- Add detected technologies -->
</frontend>
<backend>
<!-- If applicable -->
</backend>
<database>
<!-- If applicable -->
</database>
<testing>
<!-- Testing frameworks detected -->
</testing>
</technology_stack>
<core_capabilities>
<!-- List main features/capabilities you found -->
</core_capabilities>
<implemented_features>
<!-- List specific features that appear to be implemented -->
</implemented_features>
<implementation_roadmap>
<phase_1_foundation>
<!-- List foundational features to build first -->
</phase_1_foundation>
<phase_2_core_logic>
<!-- List core logic features -->
</phase_2_core_logic>
<phase_3_polish>
<!-- List polish and enhancement features -->
</phase_3_polish>
</implementation_roadmap>
</project_specification>
\`\`\`
4. Ensure .automaker/context/ directory exists
5. Ensure .automaker/features/ directory exists
**Important:**
- Be concise but accurate
- Only include information you can verify from the codebase
- If unsure about something, note it as "to be determined"
- Don't make up features that don't exist
- Features are stored in .automaker/features/{id}/feature.json - each feature gets its own folder
Begin by exploring the project structure.`;
}
/**
* Get the system prompt for coding agent
* @param {string} projectPath - Path to the project
* @param {boolean} isTDD - Whether this is Test-Driven Development mode (skipTests=false)
*/
async getCodingPrompt(projectPath, isTDD = true) {
// Get context files preview
const contextFilesPreview = projectPath
? await contextManager.getContextFilesPreview(projectPath)
: "";
// Get memory content (lessons learned from previous runs)
const memoryContent = projectPath
? await contextManager.getMemoryContent(projectPath)
: "";
// Build mode-specific instructions
const modeHeader = isTDD
? `**🧪 MODE: Test-Driven Development (TDD)**
You are implementing features using TDD methodology. This means:
- Write Playwright tests BEFORE or alongside implementation
- Run tests frequently to verify your work
- Tests are your validation mechanism
- Delete tests after they pass (they're for immediate verification only)`
: `**🔨 MODE: Manual Review (No Automated Tests)**
You are implementing features for manual user review. This means:
- Focus on clean, working implementation
- NO automated test writing required
- User will manually verify the implementation
- DO NOT commit changes - user will review and commit`;
return `You are an AI coding agent working autonomously to implement features.
${modeHeader}
${memoryContent}
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
**THE ONLY WAY to update features:**
Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters.
Do NOT manually edit feature.json files directly.
${contextFilesPreview}
Your role is to:
- Implement features exactly as specified
- Write production-quality code
- Check if feature.skipTests is true - if so, skip automated testing and don't commit
- Create comprehensive Playwright tests using testing utilities (only if skipTests is false)
- Ensure all tests pass before marking features complete (only if skipTests is false)
- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false)
- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files
- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done
- Commit working code to git (only if skipTests is false - skipTests features require manual review)
- Be thorough and detail-oriented
**IMPORTANT - Manual Testing Mode (skipTests=true):**
If a feature has skipTests=true:
- DO NOT write automated tests
- DO NOT commit changes - the user will review and commit manually
- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review
- The user will manually verify and commit the changes
**IMPORTANT - UpdateFeatureStatus Tool:**
You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status:
- Call with featureId, status="verified", and summary="Description of what was done"
- **DO NOT manually edit feature files** - this can cause race conditions and restore old state
- The tool safely updates the status without corrupting other feature data
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct
**IMPORTANT - Feature Summary (REQUIRED):**
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
- What files were modified/created
- What functionality was added or changed
- Any notable implementation decisions
Example: summary="Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx. Created useTheme hook."
The summary will be displayed on the Kanban card so the user can quickly see what was done.
**Testing Utilities (CRITICAL):**
- **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations
- **Always use utilities in tests** instead of repeating selectors
- **Add new utilities as you write tests** - if you need a helper, add it to utils.ts
- **Update utilities when functionality changes** - keep helpers in sync with code changes
This makes future tests easier to write and more maintainable!
**Test Deletion Policy:**
Tests should NOT accumulate. After a feature is verified:
1. Run the tests to ensure they pass
2. Delete the test file for that feature
3. Use UpdateFeatureStatus tool to mark the feature as "verified"
This prevents test brittleness as the app changes rapidly.
You have full access to:
- Read and write files
- Run bash commands
- Execute tests
- Delete files (rm command)
- Make git commits
- Search and analyze the codebase
- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status
**🧠 Learning from Errors - Memory System:**
If you encounter an error or issue that:
- Took multiple attempts to debug
- Was caused by a non-obvious codebase quirk
- Required understanding something specific about this project
- Could trip up future agent runs
**ADD IT TO MEMORY** by appending to \`.automaker/memory.md\`:
\`\`\`markdown
### Issue: [Brief Title]
**Problem:** [1-2 sentence description of the issue]
**Fix:** [Concise explanation of the solution]
\`\`\`
Keep entries concise - focus on the essential information needed to avoid the issue in the future. This helps both you and other agents learn from mistakes.
Focus on one feature at a time and complete it fully before finishing. Always delete tests after they pass and use the UpdateFeatureStatus tool.`;
}
/**
* Get the system prompt for verification agent
* @param {string} projectPath - Path to the project
* @param {boolean} isTDD - Whether this is Test-Driven Development mode (skipTests=false)
*/
async getVerificationPrompt(projectPath, isTDD = true) {
// Get context files preview
const contextFilesPreview = projectPath
? await contextManager.getContextFilesPreview(projectPath)
: "";
// Get memory content (lessons learned from previous runs)
const memoryContent = projectPath
? await contextManager.getMemoryContent(projectPath)
: "";
// Build mode-specific instructions
const modeHeader = isTDD
? `**🧪 MODE: Test-Driven Development (TDD)**
You are verifying/completing features using TDD methodology. This means:
- Run Playwright tests to verify implementation
- Fix failing tests by updating code
- Tests are your validation mechanism
- Delete tests after they pass (they're for immediate verification only)`
: `**🔨 MODE: Manual Review (No Automated Tests)**
You are completing features for manual user review. This means:
- Focus on clean, working implementation
- NO automated test writing required
- User will manually verify the implementation
- DO NOT commit changes - user will review and commit`;
return `You are an AI implementation and verification agent focused on completing features and ensuring they work.
${modeHeader}
${memoryContent}
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
**THE ONLY WAY to update features:**
Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters.
Do NOT manually edit feature.json files directly.
${contextFilesPreview}
Your role is to:
- **Continue implementing features until they are complete** - don't stop at the first failure
- Check if feature.skipTests is true - if so, skip automated testing and don't commit
- Write or update code to fix failing tests (only if skipTests is false)
- Run Playwright tests to verify feature implementations (only if skipTests is false)
- If tests fail, analyze errors and fix the implementation (only if skipTests is false)
- If other tests fail, verify if those tests are still accurate or should be updated or deleted (only if skipTests is false)
- Continue rerunning tests and fixing issues until ALL tests pass (only if skipTests is false)
- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false)
- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files
- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done
- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code (only if skipTests is false)
- Commit working code to git (only if skipTests is false - skipTests features require manual review)
**IMPORTANT - Manual Testing Mode (skipTests=true):**
If a feature has skipTests=true:
- DO NOT write automated tests
- DO NOT commit changes - the user will review and commit manually
- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review
- The user will manually verify and commit the changes
**IMPORTANT - UpdateFeatureStatus Tool:**
You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status:
- Call with featureId, status="verified", and summary="Description of what was done"
- **DO NOT manually edit feature files** - this can cause race conditions and restore old state
- The tool safely updates the status without corrupting other feature data
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct
**IMPORTANT - Feature Summary (REQUIRED):**
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
- What files were modified/created
- What functionality was added or changed
- Any notable implementation decisions
Example: summary="Fixed login validation. Modified: auth.ts, login-form.tsx. Added password strength check."
The summary will be displayed on the Kanban card so the user can quickly see what was done.
**Testing Utilities:**
- Check if tests/utils.ts needs updates based on code changes
- If a component's selectors or behavior changed, update the corresponding utility functions
- Add new utilities as needed for the feature's tests
- Ensure utilities remain accurate and helpful for future tests
**Test Deletion Policy:**
Tests should NOT accumulate. After a feature is verified:
1. Delete the test file for that feature
2. Use UpdateFeatureStatus tool to mark the feature as "verified"
This prevents test brittleness as the app changes rapidly.
You have access to:
- Read and edit files
- Write new code or modify existing code
- Run bash commands (especially Playwright tests)
- Delete files (rm command)
- Analyze test output
- Make git commits
- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status
**🧠 Learning from Errors - Memory System:**
If you encounter an error or issue that:
- Took multiple attempts to debug
- Was caused by a non-obvious codebase quirk
- Required understanding something specific about this project
- Could trip up future agent runs
**ADD IT TO MEMORY** by appending to \`.automaker/memory.md\`:
\`\`\`markdown
### Issue: [Brief Title]
**Problem:** [1-2 sentence description of the issue]
**Fix:** [Concise explanation of the solution]
\`\`\`
Keep entries concise - focus on the essential information needed to avoid the issue in the future. This helps both you and other agents learn from mistakes.
**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool with a summary, and commit your work.`;
}
/**
* Get system prompt for project analysis agent
*/
getProjectAnalysisSystemPrompt() {
return `You are a project analysis agent that examines codebases to understand their structure, tech stack, and implemented features.
Your goal is to:
- Quickly scan and understand project structure
- Identify programming languages, frameworks, and libraries
- Detect existing features and capabilities
- Update the .automaker/app_spec.txt with accurate information
- Ensure all required .automaker files and directories exist
Be efficient - don't read every file, focus on:
- Configuration files (package.json, tsconfig.json, etc.)
- Main entry points
- Directory structure
- README and documentation
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
Use the UpdateFeatureStatus tool to manage features, not direct file edits.
You have access to Read, Write, Edit, Glob, Grep, and Bash tools. Use them to explore the structure and write the necessary files.`;
}
}
module.exports = new PromptBuilder();

View File

@@ -1,467 +0,0 @@
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const fs = require("fs/promises");
const path = require("path");
/**
* XML template for app_spec.txt
*/
const APP_SPEC_XML_TEMPLATE = `<project_specification>
<project_name></project_name>
<overview>
</overview>
<technology_stack>
<frontend>
<framework></framework>
<ui_library></ui_library>
<styling></styling>
<state_management></state_management>
<drag_drop></drag_drop>
<icons></icons>
</frontend>
<desktop_shell>
<framework></framework>
<language></language>
<inter_process_communication></inter_process_communication>
<file_system></file_system>
</desktop_shell>
<ai_engine>
<logic_model></logic_model>
<design_model></design_model>
<orchestration></orchestration>
</ai_engine>
<testing>
<framework></framework>
<unit></unit>
</testing>
</technology_stack>
<core_capabilities>
<project_management>
</project_management>
<intelligent_analysis>
</intelligent_analysis>
<kanban_workflow>
</kanban_workflow>
<autonomous_agent_engine>
</autonomous_agent_engine>
<extensibility>
</extensibility>
</core_capabilities>
<ui_layout>
<window_structure>
</window_structure>
<theme>
</theme>
</ui_layout>
<development_workflow>
<local_testing>
</local_testing>
</development_workflow>
<implementation_roadmap>
<phase_1_foundation>
</phase_1_foundation>
<phase_2_core_logic>
</phase_2_core_logic>
<phase_3_kanban_and_interaction>
</phase_3_kanban_and_interaction>
<phase_4_polish>
</phase_4_polish>
</implementation_roadmap>
</project_specification>`;
/**
* Spec Regeneration Service - Regenerates app spec based on project description and tech stack
*/
class SpecRegenerationService {
constructor() {
this.runningRegeneration = null;
}
/**
* Create initial app spec for a new project
* @param {string} projectPath - Path to the project
* @param {string} projectOverview - User's project description
* @param {Function} sendToRenderer - Function to send events to renderer
* @param {Object} execution - Execution context with abort controller
* @param {boolean} generateFeatures - Whether to generate feature entries in features folder
*/
async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) {
console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`);
try {
const abortController = new AbortController();
execution.abortController = abortController;
const options = {
model: "claude-sonnet-4-20250514",
systemPrompt: this.getInitialCreationSystemPrompt(generateFeatures),
maxTurns: 50,
cwd: projectPath,
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures);
sendToRenderer({
type: "spec_regeneration_progress",
content: "Starting project analysis and spec creation...\n",
});
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let fullResponse = "";
for await (const msg of currentQuery) {
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
fullResponse += block.text;
sendToRenderer({
type: "spec_regeneration_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
sendToRenderer({
type: "spec_regeneration_tool",
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
sendToRenderer({
type: "spec_regeneration_complete",
message: "Initial spec creation complete!",
});
return {
success: true,
message: "Initial spec creation complete",
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[SpecRegeneration] Creation aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
success: false,
message: "Creation aborted",
};
}
console.error("[SpecRegeneration] Error creating initial spec:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
/**
* Get the system prompt for initial spec creation
* @param {boolean} generateFeatures - Whether features should be generated
*/
getInitialCreationSystemPrompt(generateFeatures = true) {
return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project overview.
You should:
1. First, thoroughly analyze the project structure to understand the existing tech stack
2. Read key configuration files (package.json, tsconfig.json, Cargo.toml, requirements.txt, etc.) to understand dependencies and frameworks
3. Understand the current architecture and patterns used
4. Based on the user's project overview, create a comprehensive app specification
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
6. Use the XML template format provided
7. Write the specification to .automaker/app_spec.txt
When analyzing, look at:
- package.json, cargo.toml, requirements.txt or similar config files for tech stack
- Source code structure and organization
- Framework-specific patterns (Next.js, React, Django, etc.)
- Database configurations and schemas
- API structures and patterns
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features.
You CAN and SHOULD modify:
- .automaker/app_spec.txt (this is your primary target)
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
}
/**
* Build the prompt for initial spec creation
* @param {string} projectOverview - User's project description
* @param {boolean} generateFeatures - Whether to generate feature entries in features folder
*/
buildInitialCreationPrompt(projectOverview, generateFeatures = true) {
return `I need you to create an initial application specification for my project. I haven't set up an app_spec.txt yet, so this will be the first one.
**My Project Overview:**
${projectOverview}
**Your Task:**
1. First, explore the project to understand the existing tech stack:
- Read package.json, Cargo.toml, requirements.txt, or similar config files
- Identify all frameworks and libraries being used
- Understand the current project structure and architecture
- Note any database, authentication, or other infrastructure in use
2. Based on my project overview and the existing tech stack, create a comprehensive app specification using this XML template:
\`\`\`xml
${APP_SPEC_XML_TEMPLATE}
\`\`\`
3. Fill out the template with:
- **project_name**: Extract from the project or derive from overview
- **overview**: A clear description based on my project overview
- **technology_stack**: All technologies you discover in the project (fill out the relevant sections, remove irrelevant ones)
- **core_capabilities**: List all the major capabilities the app should have based on my overview
- **ui_layout**: Describe the UI structure if relevant
- **development_workflow**: Note any testing or development patterns
- **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built
4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
**Guidelines:**
- Be comprehensive! Include ALL features needed for a complete application
- Only include technology_stack sections that are relevant (e.g., skip desktop_shell if it's a web-only app)
- Add new sections to core_capabilities as needed for the specific project
- The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually
- Consider user flows, error states, and edge cases when defining features
- Each phase should have multiple specific, actionable features
Begin by exploring the project structure.`;
}
/**
* Regenerate the app spec based on user's project definition
*/
async regenerateSpec(projectPath, projectDefinition, sendToRenderer, execution) {
console.log(`[SpecRegeneration] Regenerating spec for: ${projectPath}`);
try {
const abortController = new AbortController();
execution.abortController = abortController;
const options = {
model: "claude-sonnet-4-20250514",
systemPrompt: this.getSystemPrompt(),
maxTurns: 50,
cwd: projectPath,
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
const prompt = this.buildRegenerationPrompt(projectDefinition);
sendToRenderer({
type: "spec_regeneration_progress",
content: "Starting spec regeneration...\n",
});
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let fullResponse = "";
for await (const msg of currentQuery) {
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
fullResponse += block.text;
sendToRenderer({
type: "spec_regeneration_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
sendToRenderer({
type: "spec_regeneration_tool",
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
sendToRenderer({
type: "spec_regeneration_complete",
message: "Spec regeneration complete!",
});
return {
success: true,
message: "Spec regeneration complete",
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[SpecRegeneration] Regeneration aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
success: false,
message: "Regeneration aborted",
};
}
console.error("[SpecRegeneration] Error regenerating spec:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
/**
* Get the system prompt for spec regeneration
*/
getSystemPrompt() {
return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project definition.
You should:
1. First, thoroughly analyze the project structure to understand the existing tech stack
2. Read key configuration files (package.json, tsconfig.json, etc.) to understand dependencies and frameworks
3. Understand the current architecture and patterns used
4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
6. Write the specification to .automaker/app_spec.txt
When analyzing, look at:
- package.json, cargo.toml, or similar config files for tech stack
- Source code structure and organization
- Framework-specific patterns (Next.js, React, etc.)
- Database configurations and schemas
- API structures and patterns
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features.
You CAN and SHOULD modify:
- .automaker/app_spec.txt (this is your primary target)
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
}
/**
* Build the prompt for regenerating the spec
*/
buildRegenerationPrompt(projectDefinition) {
return `I need you to regenerate my application specification based on the following project definition. Be very comprehensive and liberal when defining features - I want a complete, polished application.
**My Project Definition:**
${projectDefinition}
**Your Task:**
1. First, explore the project to understand the existing tech stack:
- Read package.json or similar config files
- Identify all frameworks and libraries being used
- Understand the current project structure and architecture
- Note any database, authentication, or other infrastructure in use
2. Based on my project definition and the existing tech stack, create a comprehensive app specification that includes:
- Product Overview: A clear description of what the app does
- Tech Stack: All technologies currently in use
- Features: A COMPREHENSIVE list of all features needed to realize my vision
- Be liberal! Include all features that would make this a complete, production-ready application
- Include core features, supporting features, and nice-to-have features
- Think about user experience, error handling, edge cases, etc.
- Architecture Notes: Any important architectural decisions or patterns
3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
**Format Guidelines for the Spec:**
Use this general structure:
\`\`\`
# [App Name] - Application Specification
## Product Overview
[Description of what the app does and its purpose]
## Tech Stack
- Frontend: [frameworks, libraries]
- Backend: [frameworks, APIs]
- Database: [if applicable]
- Other: [other relevant tech]
## Features
### [Category 1]
- **[Feature Name]**: [Detailed description of the feature]
- **[Feature Name]**: [Detailed description]
...
### [Category 2]
- **[Feature Name]**: [Detailed description]
...
## Architecture Notes
[Any important architectural notes, patterns, or conventions]
\`\`\`
**Remember:**
- Be comprehensive! Include ALL features needed for a complete application
- Consider user flows, error states, loading states, etc.
- Include authentication, authorization if relevant
- Think about what would make this a polished, production-ready app
- The more detailed and complete the spec, the better
Begin by exploring the project structure.`;
}
/**
* Stop the current regeneration
*/
stop() {
if (this.runningRegeneration && this.runningRegeneration.abortController) {
this.runningRegeneration.abortController.abort();
}
this.runningRegeneration = null;
}
}
module.exports = new SpecRegenerationService();

View File

@@ -1,569 +0,0 @@
const path = require("path");
const fs = require("fs/promises");
const { exec, spawn } = require("child_process");
const { promisify } = require("util");
const execAsync = promisify(exec);
/**
* Worktree Manager - Handles git worktrees for feature isolation
*
* This service creates isolated git worktrees for each feature, allowing:
* - Features to be worked on in isolation without affecting the main branch
* - Easy rollback/revert by simply deleting the worktree
* - Checkpointing - user can see changes in the worktree before merging
*/
class WorktreeManager {
constructor() {
// Cache for worktree info
this.worktreeCache = new Map();
}
/**
* Get the base worktree directory path
*/
getWorktreeBasePath(projectPath) {
return path.join(projectPath, ".automaker", "worktrees");
}
/**
* Generate a safe branch name from feature description
*/
generateBranchName(feature) {
// Create a slug from the description
const slug = feature.description
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "") // Remove special chars
.replace(/\s+/g, "-") // Replace spaces with hyphens
.substring(0, 40); // Limit length
// Add feature ID for uniqueness
const shortId = feature.id.replace("feature-", "").substring(0, 12);
return `feature/${shortId}-${slug}`;
}
/**
* Check if the project is a git repository
*/
async isGitRepo(projectPath) {
try {
await execAsync("git rev-parse --is-inside-work-tree", { cwd: projectPath });
return true;
} catch {
return false;
}
}
/**
* Get the current branch name
*/
async getCurrentBranch(projectPath) {
try {
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: projectPath });
return stdout.trim();
} catch (error) {
console.error("[WorktreeManager] Failed to get current branch:", error);
return null;
}
}
/**
* Check if a branch exists (local or remote)
*/
async branchExists(projectPath, branchName) {
try {
await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath });
return true;
} catch {
return false;
}
}
/**
* List all existing worktrees
*/
async listWorktrees(projectPath) {
try {
const { stdout } = await execAsync("git worktree list --porcelain", { cwd: projectPath });
const worktrees = [];
const lines = stdout.split("\n");
let currentWorktree = null;
for (const line of lines) {
if (line.startsWith("worktree ")) {
if (currentWorktree) {
worktrees.push(currentWorktree);
}
currentWorktree = { path: line.replace("worktree ", "") };
} else if (line.startsWith("branch ") && currentWorktree) {
currentWorktree.branch = line.replace("branch refs/heads/", "");
} else if (line.startsWith("HEAD ") && currentWorktree) {
currentWorktree.head = line.replace("HEAD ", "");
}
}
if (currentWorktree) {
worktrees.push(currentWorktree);
}
return worktrees;
} catch (error) {
console.error("[WorktreeManager] Failed to list worktrees:", error);
return [];
}
}
/**
* Create a worktree for a feature
* @param {string} projectPath - Path to the main project
* @param {object} feature - Feature object with id and description
* @returns {object} - { success, worktreePath, branchName, error }
*/
async createWorktree(projectPath, feature) {
console.log(`[WorktreeManager] Creating worktree for feature: ${feature.id}`);
// Check if project is a git repo
if (!await this.isGitRepo(projectPath)) {
return { success: false, error: "Project is not a git repository" };
}
const branchName = this.generateBranchName(feature);
const worktreeBasePath = this.getWorktreeBasePath(projectPath);
const worktreePath = path.join(worktreeBasePath, branchName.replace("feature/", ""));
try {
// Ensure worktree directory exists
await fs.mkdir(worktreeBasePath, { recursive: true });
// Check if worktree already exists
const worktrees = await this.listWorktrees(projectPath);
const existingWorktree = worktrees.find(
w => w.path === worktreePath || w.branch === branchName
);
if (existingWorktree) {
console.log(`[WorktreeManager] Worktree already exists for feature: ${feature.id}`);
return {
success: true,
worktreePath: existingWorktree.path,
branchName: existingWorktree.branch,
existed: true,
};
}
// Get current branch to base the new branch on
const baseBranch = await this.getCurrentBranch(projectPath);
if (!baseBranch) {
return { success: false, error: "Could not determine current branch" };
}
// Check if branch already exists
const branchExists = await this.branchExists(projectPath, branchName);
if (branchExists) {
// Use existing branch
console.log(`[WorktreeManager] Using existing branch: ${branchName}`);
await execAsync(`git worktree add "${worktreePath}" ${branchName}`, { cwd: projectPath });
} else {
// Create new worktree with new branch
console.log(`[WorktreeManager] Creating new branch: ${branchName} based on ${baseBranch}`);
await execAsync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, { cwd: projectPath });
}
// Copy .automaker directory to worktree (except worktrees directory itself to avoid recursion)
const automakerSrc = path.join(projectPath, ".automaker");
const automakerDst = path.join(worktreePath, ".automaker");
try {
await fs.mkdir(automakerDst, { recursive: true });
// Note: Features are stored in .automaker/features/{id}/feature.json
// These are managed by the main project, not copied to worktrees
// Copy app_spec.txt if it exists
const appSpecSrc = path.join(automakerSrc, "app_spec.txt");
const appSpecDst = path.join(automakerDst, "app_spec.txt");
try {
const content = await fs.readFile(appSpecSrc, "utf-8");
await fs.writeFile(appSpecDst, content, "utf-8");
} catch {
// App spec might not exist yet
}
// Copy categories.json if it exists
const categoriesSrc = path.join(automakerSrc, "categories.json");
const categoriesDst = path.join(automakerDst, "categories.json");
try {
const content = await fs.readFile(categoriesSrc, "utf-8");
await fs.writeFile(categoriesDst, content, "utf-8");
} catch {
// Categories might not exist yet
}
} catch (error) {
console.warn("[WorktreeManager] Failed to copy .automaker directory:", error);
}
// Store worktree info in cache
this.worktreeCache.set(feature.id, {
worktreePath,
branchName,
createdAt: new Date().toISOString(),
baseBranch,
});
console.log(`[WorktreeManager] Worktree created at: ${worktreePath}`);
return {
success: true,
worktreePath,
branchName,
baseBranch,
existed: false,
};
} catch (error) {
console.error("[WorktreeManager] Failed to create worktree:", error);
return { success: false, error: error.message };
}
}
/**
* Get worktree info for a feature
*/
async getWorktreeInfo(projectPath, featureId) {
// Check cache first
if (this.worktreeCache.has(featureId)) {
return { success: true, ...this.worktreeCache.get(featureId) };
}
// Scan worktrees to find matching one
const worktrees = await this.listWorktrees(projectPath);
const worktreeBasePath = this.getWorktreeBasePath(projectPath);
for (const worktree of worktrees) {
// Check if this worktree is in our worktree directory
if (worktree.path.startsWith(worktreeBasePath)) {
// Check if the feature ID is in the branch name
const shortId = featureId.replace("feature-", "").substring(0, 12);
if (worktree.branch && worktree.branch.includes(shortId)) {
const info = {
worktreePath: worktree.path,
branchName: worktree.branch,
head: worktree.head,
};
this.worktreeCache.set(featureId, info);
return { success: true, ...info };
}
}
}
return { success: false, error: "Worktree not found" };
}
/**
* Remove a worktree for a feature
* This effectively reverts all changes made by the agent
*/
async removeWorktree(projectPath, featureId, deleteBranch = false) {
console.log(`[WorktreeManager] Removing worktree for feature: ${featureId}`);
const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId);
if (!worktreeInfo.success) {
console.log(`[WorktreeManager] No worktree found for feature: ${featureId}`);
return { success: true, message: "No worktree to remove" };
}
const { worktreePath, branchName } = worktreeInfo;
try {
// Remove the worktree
await execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath });
console.log(`[WorktreeManager] Worktree removed: ${worktreePath}`);
// Optionally delete the branch too
if (deleteBranch && branchName) {
try {
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
console.log(`[WorktreeManager] Branch deleted: ${branchName}`);
} catch (error) {
console.warn(`[WorktreeManager] Could not delete branch ${branchName}:`, error.message);
}
}
// Remove from cache
this.worktreeCache.delete(featureId);
return { success: true, removedPath: worktreePath, removedBranch: deleteBranch ? branchName : null };
} catch (error) {
console.error("[WorktreeManager] Failed to remove worktree:", error);
return { success: false, error: error.message };
}
}
/**
* Get status of changes in a worktree
*/
async getWorktreeStatus(worktreePath) {
try {
const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath });
const { stdout: diffStat } = await execAsync("git diff --stat", { cwd: worktreePath });
const { stdout: commitLog } = await execAsync("git log --oneline -10", { cwd: worktreePath });
const files = statusOutput.trim().split("\n").filter(Boolean);
const commits = commitLog.trim().split("\n").filter(Boolean);
return {
success: true,
modifiedFiles: files.length,
files: files.slice(0, 20), // Limit to 20 files
diffStat: diffStat.trim(),
recentCommits: commits.slice(0, 5), // Last 5 commits
};
} catch (error) {
console.error("[WorktreeManager] Failed to get worktree status:", error);
return { success: false, error: error.message };
}
}
/**
* Get detailed file diff content for a worktree
* Returns unified diff format for all changes
*/
async getFileDiffs(worktreePath) {
try {
// Get both staged and unstaged diffs
const { stdout: unstagedDiff } = await execAsync("git diff --no-color", {
cwd: worktreePath,
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large diffs
});
const { stdout: stagedDiff } = await execAsync("git diff --cached --no-color", {
cwd: worktreePath,
maxBuffer: 10 * 1024 * 1024
});
// Get list of files with their status
const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath });
const files = statusOutput.trim().split("\n").filter(Boolean);
// Parse file statuses
const fileStatuses = files.map(line => {
const status = line.substring(0, 2);
const filePath = line.substring(3);
return {
status: status.trim() || 'M',
path: filePath,
statusText: this.getStatusText(status)
};
});
// Combine diffs
const combinedDiff = [stagedDiff, unstagedDiff].filter(Boolean).join("\n");
return {
success: true,
diff: combinedDiff,
files: fileStatuses,
hasChanges: files.length > 0
};
} catch (error) {
console.error("[WorktreeManager] Failed to get file diffs:", error);
return { success: false, error: error.message };
}
}
/**
* Get human-readable status text from git status code
*/
getStatusText(status) {
const statusMap = {
'M': 'Modified',
'A': 'Added',
'D': 'Deleted',
'R': 'Renamed',
'C': 'Copied',
'U': 'Updated',
'?': 'Untracked',
'!': 'Ignored'
};
const firstChar = status.charAt(0);
const secondChar = status.charAt(1);
return statusMap[firstChar] || statusMap[secondChar] || 'Changed';
}
/**
* Get diff for a specific file in a worktree
*/
async getFileDiff(worktreePath, filePath) {
try {
// Try to get unstaged diff first, then staged if no unstaged changes
let diff = '';
try {
const { stdout } = await execAsync(`git diff --no-color -- "${filePath}"`, {
cwd: worktreePath,
maxBuffer: 5 * 1024 * 1024
});
diff = stdout;
} catch {
// File might be staged
}
if (!diff) {
try {
const { stdout } = await execAsync(`git diff --cached --no-color -- "${filePath}"`, {
cwd: worktreePath,
maxBuffer: 5 * 1024 * 1024
});
diff = stdout;
} catch {
// File might be untracked, show the content
}
}
// If still no diff, might be an untracked file - show the content
if (!diff) {
try {
const fullPath = path.join(worktreePath, filePath);
const content = await fs.readFile(fullPath, 'utf-8');
diff = `+++ ${filePath} (new file)\n${content.split('\n').map(l => '+' + l).join('\n')}`;
} catch {
diff = '(Unable to read file content)';
}
}
return {
success: true,
diff,
filePath
};
} catch (error) {
console.error(`[WorktreeManager] Failed to get diff for ${filePath}:`, error);
return { success: false, error: error.message };
}
}
/**
* Merge worktree changes back to the main branch
*/
async mergeWorktree(projectPath, featureId, options = {}) {
console.log(`[WorktreeManager] Merging worktree for feature: ${featureId}`);
const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId);
if (!worktreeInfo.success) {
return { success: false, error: "Worktree not found" };
}
const { branchName, worktreePath } = worktreeInfo;
const baseBranch = await this.getCurrentBranch(projectPath);
try {
// First commit any uncommitted changes in the worktree
const { stdout: status } = await execAsync("git status --porcelain", { cwd: worktreePath });
if (status.trim()) {
// There are uncommitted changes - commit them
await execAsync("git add -A", { cwd: worktreePath });
const commitMsg = options.commitMessage || `feat: complete ${featureId}`;
await execAsync(`git commit -m "${commitMsg}"`, { cwd: worktreePath });
}
// Merge the feature branch into the current branch in the main repo
if (options.squash) {
await execAsync(`git merge --squash ${branchName}`, { cwd: projectPath });
const squashMsg = options.squashMessage || `feat: ${featureId} - squashed merge`;
await execAsync(`git commit -m "${squashMsg}"`, { cwd: projectPath });
} else {
await execAsync(`git merge ${branchName} --no-ff -m "Merge ${branchName}"`, { cwd: projectPath });
}
console.log(`[WorktreeManager] Successfully merged ${branchName} into ${baseBranch}`);
// Optionally cleanup worktree after merge
if (options.cleanup) {
await this.removeWorktree(projectPath, featureId, true);
}
return {
success: true,
mergedBranch: branchName,
intoBranch: baseBranch,
};
} catch (error) {
console.error("[WorktreeManager] Failed to merge worktree:", error);
return { success: false, error: error.message };
}
}
/**
* Sync changes from main branch to worktree (rebase or merge)
*/
async syncWorktree(projectPath, featureId, method = "rebase") {
console.log(`[WorktreeManager] Syncing worktree for feature: ${featureId}`);
const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId);
if (!worktreeInfo.success) {
return { success: false, error: "Worktree not found" };
}
const { worktreePath, baseBranch } = worktreeInfo;
try {
if (method === "rebase") {
await execAsync(`git rebase ${baseBranch}`, { cwd: worktreePath });
} else {
await execAsync(`git merge ${baseBranch}`, { cwd: worktreePath });
}
return { success: true, method };
} catch (error) {
console.error("[WorktreeManager] Failed to sync worktree:", error);
return { success: false, error: error.message };
}
}
/**
* Get list of all feature worktrees
*/
async getAllFeatureWorktrees(projectPath) {
const worktrees = await this.listWorktrees(projectPath);
const worktreeBasePath = this.getWorktreeBasePath(projectPath);
return worktrees.filter(w =>
w.path.startsWith(worktreeBasePath) &&
w.branch &&
w.branch.startsWith("feature/")
);
}
/**
* Cleanup orphaned worktrees (worktrees without matching features)
*/
async cleanupOrphanedWorktrees(projectPath, activeFeatureIds) {
console.log("[WorktreeManager] Cleaning up orphaned worktrees...");
const worktrees = await this.getAllFeatureWorktrees(projectPath);
const cleaned = [];
for (const worktree of worktrees) {
// Extract feature ID from branch name
const branchParts = worktree.branch.replace("feature/", "").split("-");
const shortId = branchParts[0];
// Check if any active feature has this short ID
const hasMatchingFeature = activeFeatureIds.some(id => {
const featureShortId = id.replace("feature-", "").substring(0, 12);
return featureShortId === shortId;
});
if (!hasMatchingFeature) {
console.log(`[WorktreeManager] Removing orphaned worktree: ${worktree.path}`);
try {
await execAsync(`git worktree remove "${worktree.path}" --force`, { cwd: projectPath });
await execAsync(`git branch -D ${worktree.branch}`, { cwd: projectPath });
cleaned.push(worktree.path);
} catch (error) {
console.warn(`[WorktreeManager] Failed to cleanup worktree ${worktree.path}:`, error.message);
}
}
}
return { success: true, cleaned };
}
}
module.exports = new WorktreeManager();

13896
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,140 +0,0 @@
{
"name": "automaker",
"version": "0.1.0",
"private": true,
"license": "Unlicense",
"main": "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 .\"",
"build": "next build",
"build:electron": "next build && electron-builder",
"start": "next start",
"lint": "eslint",
"test": "playwright test",
"test:headed": "playwright test --headed"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.61",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.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-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.2.3",
"lucide-react": "^0.556.0",
"next": "16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-markdown": "^10.1.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.9"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.2.1",
"electron": "^39.2.6",
"electron-builder": "^26.0.12",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5",
"wait-on": "^9.0.3"
},
"build": {
"appId": "com.automaker.app",
"productName": "Automaker",
"directories": {
"output": "dist"
},
"files": [
"electron/**/*",
".next/**/*",
"public/**/*",
"!node_modules/**/*",
"node_modules/@anthropic-ai/**/*"
],
"extraResources": [
{
"from": ".env",
"to": ".env",
"filter": [
"**/*"
]
}
],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "public/logo.png"
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"icon": "public/logo.png"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"category": "Development",
"icon": "public/logo.png"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

View File

@@ -1,35 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
const port = process.env.TEST_PORT || 3007;
const reuseServer = process.env.TEST_REUSE_SERVER === "true";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
timeout: 30000,
use: {
baseURL: `http://localhost:${port}`,
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
...(reuseServer
? {}
: {
webServer: {
command: `npx next dev -p ${port}`,
url: `http://localhost:${port}`,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
}),
});

View File

@@ -1,30 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
const port = process.env.TEST_PORT || 3007;
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
timeout: 10000,
use: {
baseURL: `http://localhost:${port}`,
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: `npx next dev -p ${port}`,
url: `http://localhost:${port}`,
reuseExistingServer: true,
timeout: 60000,
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

View File

@@ -1,172 +0,0 @@
import {
query,
Options,
SDKAssistantMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
const systemPrompt = `You are an AI assistant helping users build software. You are part of the Automaker application,
which is designed to help developers plan, design, and implement software projects autonomously.
Your role is to:
- Help users define their project requirements and specifications
- Ask clarifying questions to better understand their needs
- Suggest technical approaches and architectures
- Guide them through the development process
- Be conversational and helpful
- Write, edit, and modify code files as requested
- Execute commands and tests
- Search and analyze the codebase
When discussing projects, help users think through:
- Core functionality and features
- Technical stack choices
- Data models and architecture
- User experience considerations
- Testing strategies
You have full access to the codebase and can:
- Read files to understand existing code
- Write new files
- Edit existing files
- Run bash commands
- Search for code patterns
- Execute tests and builds`;
export async function POST(request: NextRequest) {
try {
const { messages, workingDirectory } = await request.json();
console.log(
"[API] CLAUDE_CODE_OAUTH_TOKEN present:",
!!process.env.CLAUDE_CODE_OAUTH_TOKEN
);
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
return NextResponse.json(
{ error: "CLAUDE_CODE_OAUTH_TOKEN not configured" },
{ status: 500 }
);
}
// Get the last user message
const lastMessage = messages[messages.length - 1];
// Determine working directory - default to parent of app directory
const cwd = workingDirectory || path.resolve(process.cwd(), "..");
console.log("[API] Working directory:", cwd);
// Create query with options that enable code modification
const options: Options = {
// model: "claude-sonnet-4-20250514",
model: "claude-opus-4-5-20251101",
systemPrompt,
maxTurns: 20,
cwd,
// Enable all core tools for code modification
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
],
// Auto-accept file edits within the working directory
permissionMode: "acceptEdits",
// Enable sandbox for safer bash execution
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
};
// Convert message history to SDK format to preserve conversation context
// Include both user and assistant messages for full context
const sessionId = `api-session-${Date.now()}`;
const conversationMessages = messages.map(
(msg: { role: string; content: string }) => {
if (msg.role === "user") {
return {
type: "user" as const,
message: {
role: "user" as const,
content: msg.content,
},
parent_tool_use_id: null,
session_id: sessionId,
};
} else {
// Assistant message
return {
type: "assistant" as const,
message: {
role: "assistant" as const,
content: [
{
type: "text" as const,
text: msg.content,
},
],
},
session_id: sessionId,
};
}
}
);
// Execute query with full conversation context
const queryResult = query({
prompt:
conversationMessages.length > 0
? conversationMessages
: lastMessage.content,
options,
});
let responseText = "";
const toolUses: Array<{ name: string; input: unknown }> = [];
// Collect the response from the async generator
for await (const msg of queryResult) {
if (msg.type === "assistant") {
const assistantMsg = msg as SDKAssistantMessage;
if (assistantMsg.message.content) {
for (const block of assistantMsg.message.content) {
if (block.type === "text") {
responseText += block.text;
} else if (block.type === "tool_use") {
// Track tool usage for transparency
toolUses.push({
name: block.name,
input: block.input,
});
}
}
}
} else if (msg.type === "result") {
if (msg.subtype === "success") {
if (msg.result) {
responseText = msg.result;
}
}
}
}
return NextResponse.json({
content: responseText || "Sorry, I couldn't generate a response.",
toolUses: toolUses.length > 0 ? toolUses : undefined,
});
} catch (error: unknown) {
console.error("Claude API error:", error);
const errorMessage =
error instanceof Error
? error.message
: "Failed to get response from Claude";
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@@ -1,91 +0,0 @@
"use client";
import * as React from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
interface CategoryAutocompleteProps {
value: string;
onChange: (value: string) => void;
suggestions: string[];
placeholder?: string;
className?: string;
disabled?: boolean;
"data-testid"?: string;
}
export function CategoryAutocomplete({
value,
onChange,
suggestions,
placeholder = "Select or type a category...",
className,
disabled = false,
"data-testid": testId,
}: CategoryAutocompleteProps) {
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn("w-full justify-between", className)}
data-testid={testId}
>
{value
? suggestions.find((s) => s === value) ?? value
: placeholder}
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search category..." className="h-9" />
<CommandList>
<CommandEmpty>No category found.</CommandEmpty>
<CommandGroup>
{suggestions.map((suggestion) => (
<CommandItem
key={suggestion}
value={suggestion}
onSelect={(currentValue) => {
onChange(currentValue === value ? "" : currentValue);
setOpen(false);
}}
data-testid={`category-option-${suggestion.toLowerCase().replace(/\s+/g, "-")}`}
>
{suggestion}
<Check
className={cn(
"ml-auto",
value === suggestion ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,30 +0,0 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -1,157 +0,0 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
compact = false,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
compact?: boolean;
}) {
// Check if className contains a custom max-width
const hasCustomMaxWidth =
typeof className === "string" && className.includes("max-w-");
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]",
compact
? "max-w-4xl p-4"
: !hasCustomMaxWidth
? "sm:max-w-2xl p-6"
: "p-6",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className={cn(
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity cursor-pointer hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
compact ? "top-2 right-3" : "top-3 right-5"
)}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -1,21 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-input border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -1,272 +0,0 @@
"use client";
import { useState, useMemo } from "react";
import {
ChevronDown,
ChevronRight,
MessageSquare,
Wrench,
Zap,
AlertCircle,
CheckCircle2,
AlertTriangle,
Bug,
Info,
FileOutput,
Brain,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
parseLogOutput,
getLogTypeColors,
type LogEntry,
type LogEntryType,
} from "@/lib/log-parser";
interface LogViewerProps {
output: string;
className?: string;
}
const getLogIcon = (type: LogEntryType) => {
switch (type) {
case "prompt":
return <MessageSquare className="w-4 h-4" />;
case "tool_call":
return <Wrench className="w-4 h-4" />;
case "tool_result":
return <FileOutput className="w-4 h-4" />;
case "phase":
return <Zap className="w-4 h-4" />;
case "error":
return <AlertCircle className="w-4 h-4" />;
case "success":
return <CheckCircle2 className="w-4 h-4" />;
case "warning":
return <AlertTriangle className="w-4 h-4" />;
case "thinking":
return <Brain className="w-4 h-4" />;
case "debug":
return <Bug className="w-4 h-4" />;
default:
return <Info className="w-4 h-4" />;
}
};
interface LogEntryItemProps {
entry: LogEntry;
isExpanded: boolean;
onToggle: () => void;
}
function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
const colors = getLogTypeColors(entry.type);
const hasContent = entry.content.length > 100;
// Format content - detect and highlight JSON
const formattedContent = useMemo(() => {
const content = entry.content;
// Try to find and format JSON blocks
const jsonRegex = /(\{[\s\S]*?\}|\[[\s\S]*?\])/g;
let lastIndex = 0;
const parts: { type: "text" | "json"; content: string }[] = [];
let match;
while ((match = jsonRegex.exec(content)) !== null) {
// Add text before JSON
if (match.index > lastIndex) {
parts.push({
type: "text",
content: content.slice(lastIndex, match.index),
});
}
// Try to parse and format JSON
try {
const parsed = JSON.parse(match[1]);
parts.push({
type: "json",
content: JSON.stringify(parsed, null, 2),
});
} catch {
// Not valid JSON, treat as text
parts.push({ type: "text", content: match[1] });
}
lastIndex = match.index + match[1].length;
}
// Add remaining text
if (lastIndex < content.length) {
parts.push({ type: "text", content: content.slice(lastIndex) });
}
return parts.length > 0 ? parts : [{ type: "text" as const, content }];
}, [entry.content]);
return (
<div
className={cn(
"rounded-lg border-l-4 transition-all duration-200",
colors.bg,
colors.border,
"hover:brightness-110"
)}
data-testid={`log-entry-${entry.type}`}
>
<button
onClick={onToggle}
className="w-full px-3 py-2 flex items-center gap-2 text-left"
data-testid={`log-entry-toggle-${entry.id}`}
>
{hasContent ? (
isExpanded ? (
<ChevronDown className="w-4 h-4 text-zinc-400 flex-shrink-0" />
) : (
<ChevronRight className="w-4 h-4 text-zinc-400 flex-shrink-0" />
)
) : (
<span className="w-4 flex-shrink-0" />
)}
<span className={cn("flex-shrink-0", colors.icon)}>
{getLogIcon(entry.type)}
</span>
<span
className={cn(
"text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0",
colors.badge
)}
data-testid="log-entry-badge"
>
{entry.title}
</span>
<span className="text-xs text-zinc-400 truncate flex-1 ml-2">
{!isExpanded &&
entry.content.slice(0, 80) +
(entry.content.length > 80 ? "..." : "")}
</span>
</button>
{(isExpanded || !hasContent) && (
<div
className="px-4 pb-3 pt-1"
data-testid={`log-entry-content-${entry.id}`}
>
<div className="font-mono text-xs space-y-1">
{formattedContent.map((part, index) => (
<div key={index}>
{part.type === "json" ? (
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-primary">
{part.content}
</pre>
) : (
<pre
className={cn(
"whitespace-pre-wrap break-words",
colors.text
)}
>
{part.content}
</pre>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
export function LogViewer({ output, className }: LogViewerProps) {
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const entries = useMemo(() => parseLogOutput(output), [output]);
const toggleEntry = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const expandAll = () => {
setExpandedIds(new Set(entries.map((e) => e.id)));
};
const collapseAll = () => {
setExpandedIds(new Set());
};
if (entries.length === 0) {
return null;
}
// Count entries by type
const typeCounts = entries.reduce((acc, entry) => {
acc[entry.type] = (acc[entry.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return (
<div className={cn("flex flex-col gap-2", className)}>
{/* Header with controls */}
<div className="flex items-center justify-between px-1" data-testid="log-viewer-header">
<div className="flex items-center gap-2 flex-wrap">
{Object.entries(typeCounts).map(([type, count]) => {
const colors = getLogTypeColors(type as LogEntryType);
return (
<span
key={type}
className={cn(
"text-xs px-2 py-0.5 rounded-full",
colors.badge
)}
data-testid={`log-type-count-${type}`}
>
{type}: {count}
</span>
);
})}
</div>
<div className="flex items-center gap-1">
<button
onClick={expandAll}
className="text-xs text-zinc-400 hover:text-zinc-200 px-2 py-1 rounded hover:bg-zinc-800/50 transition-colors"
data-testid="log-expand-all"
>
Expand All
</button>
<button
onClick={collapseAll}
className="text-xs text-zinc-400 hover:text-zinc-200 px-2 py-1 rounded hover:bg-zinc-800/50 transition-colors"
data-testid="log-collapse-all"
>
Collapse All
</button>
</div>
</div>
{/* Log entries */}
<div className="space-y-2" data-testid="log-entries-container">
{entries.map((entry) => (
<LogEntryItem
key={entry.id}
entry={entry}
isExpanded={expandedIds.has(entry.id)}
onToggle={() => toggleEntry(entry.id)}
/>
))}
</div>
</div>
);
}

View File

@@ -1,27 +0,0 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ComponentRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View File

@@ -1,71 +0,0 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px] border border-border",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 cursor-pointer",
"text-foreground/70 hover:text-foreground hover:bg-accent",
"data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-md data-[state=active]:border-primary/50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:outline-1",
"disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -1,20 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,32 +0,0 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -1,290 +0,0 @@
"use client";
import { useRef, useCallback, useMemo } from "react";
import { cn } from "@/lib/utils";
interface XmlSyntaxEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
"data-testid"?: string;
}
// Tokenize XML content into parts for highlighting
interface Token {
type:
| "tag-bracket"
| "tag-name"
| "attribute-name"
| "attribute-equals"
| "attribute-value"
| "text"
| "comment"
| "cdata"
| "doctype";
value: string;
}
function tokenizeXml(text: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < text.length) {
// Comment: <!-- ... -->
if (text.slice(i, i + 4) === "<!--") {
const end = text.indexOf("-->", i + 4);
if (end !== -1) {
tokens.push({ type: "comment", value: text.slice(i, end + 3) });
i = end + 3;
continue;
}
}
// CDATA: <![CDATA[ ... ]]>
if (text.slice(i, i + 9) === "<![CDATA[") {
const end = text.indexOf("]]>", i + 9);
if (end !== -1) {
tokens.push({ type: "cdata", value: text.slice(i, end + 3) });
i = end + 3;
continue;
}
}
// DOCTYPE: <!DOCTYPE ... >
if (text.slice(i, i + 9).toUpperCase() === "<!DOCTYPE") {
const end = text.indexOf(">", i + 9);
if (end !== -1) {
tokens.push({ type: "doctype", value: text.slice(i, end + 1) });
i = end + 1;
continue;
}
}
// Tag: < ... >
if (text[i] === "<") {
// Find the end of the tag
let tagEnd = i + 1;
let inString: string | null = null;
while (tagEnd < text.length) {
const char = text[tagEnd];
if (inString) {
if (char === inString && text[tagEnd - 1] !== "\\") {
inString = null;
}
} else {
if (char === '"' || char === "'") {
inString = char;
} else if (char === ">") {
tagEnd++;
break;
}
}
tagEnd++;
}
const tagContent = text.slice(i, tagEnd);
const tagTokens = tokenizeTag(tagContent);
tokens.push(...tagTokens);
i = tagEnd;
continue;
}
// Text content between tags
const nextTag = text.indexOf("<", i);
if (nextTag === -1) {
tokens.push({ type: "text", value: text.slice(i) });
break;
} else if (nextTag > i) {
tokens.push({ type: "text", value: text.slice(i, nextTag) });
i = nextTag;
}
}
return tokens;
}
function tokenizeTag(tag: string): Token[] {
const tokens: Token[] = [];
let i = 0;
// Opening bracket (< or </ or <?)
if (tag.startsWith("</")) {
tokens.push({ type: "tag-bracket", value: "</" });
i = 2;
} else if (tag.startsWith("<?")) {
tokens.push({ type: "tag-bracket", value: "<?" });
i = 2;
} else {
tokens.push({ type: "tag-bracket", value: "<" });
i = 1;
}
// Skip whitespace
while (i < tag.length && /\s/.test(tag[i])) {
tokens.push({ type: "text", value: tag[i] });
i++;
}
// Tag name
let tagName = "";
while (i < tag.length && /[a-zA-Z0-9_:-]/.test(tag[i])) {
tagName += tag[i];
i++;
}
if (tagName) {
tokens.push({ type: "tag-name", value: tagName });
}
// Attributes and closing
while (i < tag.length) {
// Skip whitespace
if (/\s/.test(tag[i])) {
let ws = "";
while (i < tag.length && /\s/.test(tag[i])) {
ws += tag[i];
i++;
}
tokens.push({ type: "text", value: ws });
continue;
}
// Closing bracket
if (tag[i] === ">" || tag.slice(i, i + 2) === "/>" || tag.slice(i, i + 2) === "?>") {
tokens.push({ type: "tag-bracket", value: tag.slice(i) });
break;
}
// Attribute name
let attrName = "";
while (i < tag.length && /[a-zA-Z0-9_:-]/.test(tag[i])) {
attrName += tag[i];
i++;
}
if (attrName) {
tokens.push({ type: "attribute-name", value: attrName });
}
// Skip whitespace around =
while (i < tag.length && /\s/.test(tag[i])) {
tokens.push({ type: "text", value: tag[i] });
i++;
}
// Equals sign
if (tag[i] === "=") {
tokens.push({ type: "attribute-equals", value: "=" });
i++;
}
// Skip whitespace after =
while (i < tag.length && /\s/.test(tag[i])) {
tokens.push({ type: "text", value: tag[i] });
i++;
}
// Attribute value
if (tag[i] === '"' || tag[i] === "'") {
const quote = tag[i];
let value = quote;
i++;
while (i < tag.length && tag[i] !== quote) {
value += tag[i];
i++;
}
if (i < tag.length) {
value += tag[i];
i++;
}
tokens.push({ type: "attribute-value", value });
}
}
return tokens;
}
export function XmlSyntaxEditor({
value,
onChange,
placeholder,
className,
"data-testid": testId,
}: XmlSyntaxEditorProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const highlightRef = useRef<HTMLDivElement>(null);
// Sync scroll between textarea and highlight layer
const handleScroll = useCallback(() => {
if (textareaRef.current && highlightRef.current) {
highlightRef.current.scrollTop = textareaRef.current.scrollTop;
highlightRef.current.scrollLeft = textareaRef.current.scrollLeft;
}
}, []);
// Handle tab key for indentation
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Tab") {
e.preventDefault();
const textarea = e.currentTarget;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newValue =
value.substring(0, start) + " " + value.substring(end);
onChange(newValue);
// Reset cursor position after state update
requestAnimationFrame(() => {
textarea.selectionStart = textarea.selectionEnd = start + 2;
});
}
},
[value, onChange]
);
// Memoize the highlighted content
const highlightedContent = useMemo(() => {
const tokens = tokenizeXml(value);
return tokens.map((token, index) => {
const className = `xml-${token.type}`;
// React handles escaping automatically, just render the raw value
return (
<span key={index} className={className}>
{token.value}
</span>
);
});
}, [value]);
return (
<div className={cn("relative w-full h-full xml-editor", className)}>
{/* Syntax highlighted layer (read-only, behind textarea) */}
<div
ref={highlightRef}
className="absolute inset-0 overflow-auto pointer-events-none font-mono text-sm p-4 whitespace-pre-wrap break-words"
aria-hidden="true"
>
{value ? (
<code className="xml-highlight">{highlightedContent}</code>
) : (
<span className="text-muted-foreground opacity-50">{placeholder}</span>
)}
</div>
{/* Actual textarea (transparent text, handles input) */}
<textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
onScroll={handleScroll}
onKeyDown={handleKeyDown}
placeholder=""
spellCheck={false}
className="absolute inset-0 w-full h-full font-mono text-sm p-4 bg-transparent resize-none focus:outline-none text-transparent caret-foreground selection:bg-primary/30"
data-testid={testId}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,917 +0,0 @@
"use client";
import { useState, useEffect, memo } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Feature, useAppStore } from "@/store/app-store";
import {
GripVertical,
Edit,
CheckCircle2,
Circle,
Loader2,
Trash2,
Eye,
PlayCircle,
RotateCcw,
StopCircle,
Hand,
MessageSquare,
GitCommit,
Cpu,
Wrench,
ListTodo,
Sparkles,
Expand,
FileText,
MoreVertical,
AlertCircle,
GitBranch,
Undo2,
GitMerge,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer";
import { getElectronAPI } from "@/lib/electron";
import {
parseAgentContext,
AgentTaskInfo,
formatModelName,
DEFAULT_MODEL,
} from "@/lib/agent-context-parser";
import { Markdown } from "@/components/ui/markdown";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface KanbanCardProps {
feature: Feature;
onEdit: () => void;
onDelete: () => void;
onViewOutput?: () => void;
onVerify?: () => void;
onResume?: () => void;
onForceStop?: () => void;
onManualVerify?: () => void;
onMoveBackToInProgress?: () => void;
onFollowUp?: () => void;
onCommit?: () => void;
onRevert?: () => void;
onMerge?: () => void;
hasContext?: boolean;
isCurrentAutoTask?: boolean;
shortcutKey?: string;
/** Context content for extracting progress info */
contextContent?: string;
/** Feature summary from agent completion */
summary?: string;
}
export const KanbanCard = memo(function KanbanCard({
feature,
onEdit,
onDelete,
onViewOutput,
onVerify,
onResume,
onForceStop,
onManualVerify,
onMoveBackToInProgress,
onFollowUp,
onCommit,
onRevert,
onMerge,
hasContext,
isCurrentAutoTask,
shortcutKey,
contextContent,
summary,
}: KanbanCardProps) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
const [isRevertDialogOpen, setIsRevertDialogOpen] = useState(false);
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
const { kanbanCardDetailLevel } = useAppStore();
// Check if feature has worktree
const hasWorktree = !!feature.branchName;
// Helper functions to check what should be shown based on detail level
const showSteps =
kanbanCardDetailLevel === "standard" ||
kanbanCardDetailLevel === "detailed";
const showAgentInfo = kanbanCardDetailLevel === "detailed";
// Load context file for in_progress, waiting_approval, and verified features
useEffect(() => {
const loadContext = async () => {
// Use provided context or load from file
if (contextContent) {
const info = parseAgentContext(contextContent);
setAgentInfo(info);
return;
}
// Only load for non-backlog features
if (feature.status === "backlog") {
setAgentInfo(null);
return;
}
try {
const api = getElectronAPI();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentProject = (window as any).__currentProject;
if (!currentProject?.path) return;
// Use features API to get agent output
if (api.features) {
const result = await api.features.getAgentOutput(
currentProject.path,
feature.id
);
if (result.success && result.content) {
const info = parseAgentContext(result.content);
setAgentInfo(info);
}
} else {
// Fallback to direct file read for backward compatibility
const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`;
const result = await api.readFile(contextPath);
if (result.success && result.content) {
const info = parseAgentContext(result.content);
setAgentInfo(info);
}
}
} catch {
// Context file might not exist
console.debug("[KanbanCard] No context file for feature:", feature.id);
}
};
loadContext();
// Reload context periodically while feature is running
if (isCurrentAutoTask) {
const interval = setInterval(loadContext, 3000);
return () => clearInterval(interval);
}
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsDeleteDialogOpen(true);
};
const handleConfirmDelete = () => {
setIsDeleteDialogOpen(false);
onDelete();
};
const handleCancelDelete = () => {
setIsDeleteDialogOpen(false);
};
// Dragging logic:
// - Backlog items can always be dragged
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
// - waiting_approval items can always be dragged (to allow manual verification via drag)
// - Non-skipTests (TDD) items in progress or verified cannot be dragged
const isDraggable =
feature.status === "backlog" ||
feature.status === "waiting_approval" ||
(feature.skipTests && !isCurrentAutoTask);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: feature.id,
disabled: !isDraggable,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<Card
ref={setNodeRef}
style={style}
className={cn(
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-border relative kanban-card-content select-none",
isDragging && "opacity-50 scale-105 shadow-lg",
isCurrentAutoTask &&
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse",
feature.error &&
!isCurrentAutoTask &&
"border-red-500 border-2 shadow-red-500/30 shadow-lg",
!isDraggable && "cursor-default"
)}
data-testid={`kanban-card-${feature.id}`}
onDoubleClick={onEdit}
{...attributes}
{...(isDraggable ? listeners : {})}
>
{/* Skip Tests indicator badge */}
{feature.skipTests && !feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
"top-2 left-2",
"bg-orange-500/20 border border-orange-500/50 text-orange-400"
)}
data-testid={`skip-tests-badge-${feature.id}`}
title="Manual verification required"
>
<Hand className="w-3 h-3" />
<span>Manual</span>
</div>
)}
{/* Error indicator badge */}
{feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
"top-2 left-2",
"bg-red-500/20 border border-red-500/50 text-red-400"
)}
data-testid={`error-badge-${feature.id}`}
title={feature.error}
>
<AlertCircle className="w-3 h-3" />
<span>Errored</span>
</div>
)}
{/* Branch badge - show when feature has a worktree */}
{hasWorktree && !isCurrentAutoTask && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default",
"bg-purple-500/20 border border-purple-500/50 text-purple-400",
// Position below error badge if present, otherwise use normal position
feature.error || feature.skipTests
? "top-8 left-2"
: "top-2 left-2"
)}
data-testid={`branch-badge-${feature.id}`}
>
<GitBranch className="w-3 h-3 shrink-0" />
<span className="truncate max-w-[80px]">{feature.branchName?.replace("feature/", "")}</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[300px]">
<p className="font-mono text-xs break-all">{feature.branchName}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<CardHeader
className={cn(
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
// Add extra top padding when badges are present to prevent text overlap
(feature.skipTests || feature.error) && "pt-10",
// Add even more top padding when both badges and branch are shown
hasWorktree && (feature.skipTests || feature.error) && "pt-14"
)}
>
{isCurrentAutoTask && (
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
{feature.startedAt && (
<CountUpTimer
startedAt={feature.startedAt}
className="text-running-indicator"
/>
)}
</div>
)}
{!isCurrentAutoTask && (
<div className="absolute top-2 right-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-white/10"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`menu-${feature.id}`}
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-2" />
Edit
</DropdownMenuItem>
{onViewOutput && feature.status !== "backlog" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
data-testid={`view-logs-${feature.id}`}
>
<FileText className="w-3 h-3 mr-2" />
Logs
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(e as unknown as React.MouseEvent);
}}
data-testid={`delete-feature-${feature.id}`}
>
<Trash2 className="w-3 h-3 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<div className="flex items-start gap-2">
{isDraggable && (
<div
className="-ml-2 -mt-1 p-2 touch-none"
data-testid={`drag-handle-${feature.id}`}
>
<GripVertical className="w-4 h-4 text-muted-foreground" />
</div>
)}
<div className="flex-1 min-w-0 overflow-hidden">
<CardTitle
className={cn(
"text-sm leading-tight break-words hyphens-auto overflow-hidden",
!isDescriptionExpanded && "line-clamp-3"
)}
>
{feature.description}
</CardTitle>
{/* Show More/Less toggle - only show when description is likely truncated */}
{feature.description.length > 100 && (
<button
onClick={(e) => {
e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded);
}}
onPointerDown={(e) => e.stopPropagation()}
className="flex items-center gap-0.5 text-[10px] text-muted-foreground hover:text-foreground mt-1 transition-colors"
data-testid={`toggle-description-${feature.id}`}
>
{isDescriptionExpanded ? (
<>
<ChevronUp className="w-3 h-3" />
<span>Show Less</span>
</>
) : (
<>
<ChevronDown className="w-3 h-3" />
<span>Show More</span>
</>
)}
</button>
)}
<CardDescription className="text-xs mt-1 truncate">
{feature.category}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-3 pt-0">
{/* Steps Preview - Show in Standard and Detailed modes */}
{showSteps && feature.steps.length > 0 && (
<div className="mb-3 space-y-1">
{feature.steps.slice(0, 3).map((step, index) => (
<div
key={index}
className="flex items-start gap-2 text-xs text-muted-foreground"
>
{feature.status === "verified" ? (
<CheckCircle2 className="w-3 h-3 mt-0.5 text-green-500 shrink-0" />
) : (
<Circle className="w-3 h-3 mt-0.5 shrink-0" />
)}
<span className="break-words hyphens-auto line-clamp-2 leading-relaxed">{step}</span>
</div>
))}
{feature.steps.length > 3 && (
<p className="text-xs text-muted-foreground pl-5">
+{feature.steps.length - 3} more steps
</p>
)}
</div>
)}
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
{/* Detailed mode: Show all agent info */}
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
<div className="mb-3 space-y-2 overflow-hidden">
{/* Model & Phase */}
<div className="flex items-center gap-2 text-xs flex-wrap">
<div className="flex items-center gap-1 text-cyan-400">
<Cpu className="w-3 h-3" />
<span className="font-medium">
{formatModelName(feature.model ?? DEFAULT_MODEL)}
</span>
</div>
{agentInfo.currentPhase && (
<div
className={cn(
"px-1.5 py-0.5 rounded text-[10px] font-medium",
agentInfo.currentPhase === "planning" &&
"bg-blue-500/20 text-blue-400",
agentInfo.currentPhase === "action" &&
"bg-amber-500/20 text-amber-400",
agentInfo.currentPhase === "verification" &&
"bg-green-500/20 text-green-400"
)}
>
{agentInfo.currentPhase}
</div>
)}
</div>
{/* Task List Progress (if todos found) */}
{agentInfo.todos.length > 0 && (
<div className="space-y-1">
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
<ListTodo className="w-3 h-3" />
<span>
{
agentInfo.todos.filter((t) => t.status === "completed")
.length
}
/{agentInfo.todos.length} tasks
</span>
</div>
<div className="space-y-0.5 max-h-16 overflow-y-auto">
{agentInfo.todos.slice(0, 3).map((todo, idx) => (
<div
key={idx}
className="flex items-center gap-1.5 text-[10px]"
>
{todo.status === "completed" ? (
<CheckCircle2 className="w-2.5 h-2.5 text-green-500 shrink-0" />
) : todo.status === "in_progress" ? (
<Loader2 className="w-2.5 h-2.5 text-amber-400 animate-spin shrink-0" />
) : (
<Circle className="w-2.5 h-2.5 text-muted-foreground shrink-0" />
)}
<span
className={cn(
"break-words hyphens-auto line-clamp-2 leading-relaxed",
todo.status === "completed" &&
"text-muted-foreground line-through",
todo.status === "in_progress" && "text-amber-400",
todo.status === "pending" && "text-foreground-secondary"
)}
>
{todo.content}
</span>
</div>
))}
{agentInfo.todos.length > 3 && (
<p className="text-[10px] text-muted-foreground pl-4">
+{agentInfo.todos.length - 3} more
</p>
)}
</div>
</div>
)}
{/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */}
{(feature.status === "waiting_approval" ||
feature.status === "verified") && (
<>
{(feature.summary || summary || agentInfo.summary) && (
<div className="space-y-1 pt-1 border-t border-border-glass overflow-hidden">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1 text-[10px] text-green-400 min-w-0">
<Sparkles className="w-3 h-3 shrink-0" />
<span className="truncate">Summary</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setIsSummaryDialogOpen(true);
}}
onPointerDown={(e) => e.stopPropagation()}
className="p-0.5 rounded hover:bg-accent transition-colors text-muted-foreground hover:text-foreground shrink-0"
title="View full summary"
data-testid={`expand-summary-${feature.id}`}
>
<Expand className="w-3 h-3" />
</button>
</div>
<p className="text-[10px] text-foreground-secondary line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden">
{feature.summary || summary || agentInfo.summary}
</p>
</div>
)}
{/* Show tool count even without summary */}
{!feature.summary &&
!summary &&
!agentInfo.summary &&
agentInfo.toolCallCount > 0 && (
<div className="flex items-center gap-2 text-[10px] text-muted-foreground pt-1 border-t border-border-glass">
<span className="flex items-center gap-1">
<Wrench className="w-2.5 h-2.5" />
{agentInfo.toolCallCount} tool calls
</span>
{agentInfo.todos.length > 0 && (
<span className="flex items-center gap-1">
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
{
agentInfo.todos.filter(
(t) => t.status === "completed"
).length
}{" "}
tasks done
</span>
)}
</div>
)}
</>
)}
</div>
)}
{/* Actions */}
<div className="flex gap-2">
{isCurrentAutoTask && (
<>
{onViewOutput && (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-action-view hover:bg-action-view-hover"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`view-output-${feature.id}`}
>
<FileText className="w-3 h-3 mr-1" />
Logs
{shortcutKey && (
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
</span>
)}
</Button>
)}
{onForceStop && (
<Button
variant="destructive"
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onForceStop();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`force-stop-${feature.id}`}
>
<StopCircle className="w-3 h-3 mr-1" />
Stop
</Button>
)}
</>
)}
{!isCurrentAutoTask && feature.status === "in_progress" && (
<>
{/* skipTests features show manual verify button */}
{feature.skipTests && onManualVerify ? (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-primary hover:bg-primary/90"
onClick={(e) => {
e.stopPropagation();
onManualVerify();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`manual-verify-${feature.id}`}
>
<CheckCircle2 className="w-3 h-3 mr-1" />
Verify
</Button>
) : hasContext && onResume ? (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover"
onClick={(e) => {
e.stopPropagation();
onResume();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`resume-feature-${feature.id}`}
>
<RotateCcw className="w-3 h-3 mr-1" />
Resume
</Button>
) : onVerify ? (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover"
onClick={(e) => {
e.stopPropagation();
onVerify();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`verify-feature-${feature.id}`}
>
<PlayCircle className="w-3 h-3 mr-1" />
Resume
</Button>
) : null}
{onViewOutput && !feature.skipTests && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`view-output-inprogress-${feature.id}`}
>
<FileText className="w-3 h-3 mr-1" />
Logs
</Button>
)}
</>
)}
{!isCurrentAutoTask && feature.status === "verified" && (
<>
{/* Logs button if context exists */}
{hasContext && onViewOutput && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`view-output-verified-${feature.id}`}
>
<FileText className="w-3 h-3 mr-1" />
Logs
</Button>
)}
</>
)}
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
<>
{/* Revert button - only show when worktree exists (icon only to save space) */}
{hasWorktree && onRevert && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/20 shrink-0"
onClick={(e) => {
e.stopPropagation();
setIsRevertDialogOpen(true);
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`revert-${feature.id}`}
>
<Undo2 className="w-3.5 h-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Revert changes</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Follow-up prompt button */}
{onFollowUp && (
<Button
variant="secondary"
size="sm"
className="flex-1 h-7 text-xs min-w-0"
onClick={(e) => {
e.stopPropagation();
onFollowUp();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`follow-up-${feature.id}`}
>
<MessageSquare className="w-3 h-3 mr-1 shrink-0" />
<span className="truncate">Follow-up</span>
</Button>
)}
{/* Merge button - only show when worktree exists */}
{hasWorktree && onMerge && (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-purple-600 hover:bg-purple-700 min-w-0"
onClick={(e) => {
e.stopPropagation();
onMerge();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`merge-${feature.id}`}
title="Merge changes into main branch"
>
<GitMerge className="w-3 h-3 mr-1 shrink-0" />
<span className="truncate">Merge</span>
</Button>
)}
{/* Commit and verify button - show when no worktree */}
{!hasWorktree && onCommit && (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onCommit();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`commit-${feature.id}`}
>
<GitCommit className="w-3 h-3 mr-1" />
Commit
</Button>
)}
</>
)}
</div>
</CardContent>
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent data-testid="delete-confirmation-dialog">
<DialogHeader>
<DialogTitle>Delete Feature</DialogTitle>
<DialogDescription>
Are you sure you want to delete this feature? This action cannot
be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-6">
<Button
variant="ghost"
onClick={handleCancelDelete}
data-testid="cancel-delete-button"
>
Cancel
</Button>
<HotkeyButton
variant="destructive"
onClick={handleConfirmDelete}
data-testid="confirm-delete-button"
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={isDeleteDialogOpen}
>
Delete
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Summary Modal */}
<Dialog open={isSummaryDialogOpen} onOpenChange={setIsSummaryDialogOpen}>
<DialogContent
className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
data-testid={`summary-dialog-${feature.id}`}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-green-400" />
Implementation Summary
</DialogTitle>
<DialogDescription className="text-sm" title={feature.description}>
{feature.description.length > 100
? `${feature.description.slice(0, 100)}...`
: feature.description}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border">
<Markdown>
{feature.summary ||
summary ||
agentInfo?.summary ||
"No summary available"}
</Markdown>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setIsSummaryDialogOpen(false)}
data-testid="close-summary-button"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Revert Confirmation Dialog */}
<Dialog open={isRevertDialogOpen} onOpenChange={setIsRevertDialogOpen}>
<DialogContent data-testid="revert-confirmation-dialog">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-400">
<Undo2 className="w-5 h-5" />
Revert Changes
</DialogTitle>
<DialogDescription>
This will discard all changes made by the agent and move the feature back to the backlog.
{feature.branchName && (
<span className="block mt-2 font-medium">
Branch <code className="bg-muted px-1 py-0.5 rounded">{feature.branchName}</code> will be deleted.
</span>
)}
<span className="block mt-2 text-red-400 font-medium">
This action cannot be undone.
</span>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setIsRevertDialogOpen(false)}
data-testid="cancel-revert-button"
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
setIsRevertDialogOpen(false);
onRevert?.();
}}
data-testid="confirm-revert-button"
>
<Undo2 className="w-4 h-4 mr-2" />
Revert Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
});

View File

@@ -1,52 +0,0 @@
"use client";
import { memo } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
interface KanbanColumnProps {
id: string;
title: string;
color: string;
count: number;
children: ReactNode;
headerAction?: ReactNode;
}
export const KanbanColumn = memo(function KanbanColumn({
id,
title,
color,
count,
children,
headerAction,
}: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({ id });
return (
<div
ref={setNodeRef}
className={cn(
"flex flex-col h-full rounded-lg bg-card backdrop-blur-sm border border-border transition-colors w-72",
isOver && "bg-accent"
)}
data-testid={`kanban-column-${id}`}
>
{/* Column Header */}
<div className="flex items-center gap-2 p-3 border-b border-border">
<div className={cn("w-3 h-3 rounded-full", color)} />
<h3 className="font-medium text-sm flex-1">{title}</h3>
{headerAction}
<span className="text-xs text-muted-foreground bg-background px-2 py-0.5 rounded-full">
{count}
</span>
</div>
{/* Column Content */}
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{children}
</div>
</div>
);
});

View File

@@ -1,718 +0,0 @@
"use client";
import { useState, useMemo, useCallback, useEffect } from "react";
import {
useAppStore,
AIProfile,
AgentModel,
ThinkingLevel,
ModelProvider,
} from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { cn, modelSupportsThinking } from "@/lib/utils";
import {
useKeyboardShortcuts,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
UserCircle,
Plus,
Pencil,
Trash2,
Brain,
Zap,
Scale,
Cpu,
Rocket,
Sparkles,
GripVertical,
Lock,
Check,
} from "lucide-react";
import { toast } from "sonner";
import {
DndContext,
DragEndEvent,
PointerSensor,
useSensor,
useSensors,
closestCenter,
} from "@dnd-kit/core";
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
// Icon mapping for profiles
const PROFILE_ICONS: Record<
string,
React.ComponentType<{ className?: string }>
> = {
Brain,
Zap,
Scale,
Cpu,
Rocket,
Sparkles,
};
// Available icons for selection
const ICON_OPTIONS = [
{ name: "Brain", icon: Brain },
{ name: "Zap", icon: Zap },
{ name: "Scale", icon: Scale },
{ name: "Cpu", icon: Cpu },
{ name: "Rocket", icon: Rocket },
{ name: "Sparkles", icon: Sparkles },
];
// Model options for the form
const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
{ id: "haiku", label: "Claude Haiku" },
{ id: "sonnet", label: "Claude Sonnet" },
{ id: "opus", label: "Claude Opus" },
];
const CODEX_MODELS: { id: AgentModel; label: string }[] = [
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
{ id: "gpt-5.1", label: "GPT-5.1" },
];
const THINKING_LEVELS: { id: ThinkingLevel; label: string }[] = [
{ id: "none", label: "None" },
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium" },
{ id: "high", label: "High" },
{ id: "ultrathink", label: "Ultrathink" },
];
// Helper to determine provider from model
function getProviderFromModel(model: AgentModel): ModelProvider {
if (model.startsWith("gpt")) {
return "codex";
}
return "claude";
}
// Sortable Profile Card Component
function SortableProfileCard({
profile,
onEdit,
onDelete,
}: {
profile: AIProfile;
onEdit: () => void;
onDelete: () => void;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: profile.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
const isCodex = profile.provider === "codex";
return (
<div
ref={setNodeRef}
style={style}
className={cn(
"group relative flex items-start gap-4 p-4 rounded-xl border bg-card transition-all",
isDragging && "shadow-lg",
profile.isBuiltIn
? "border-border/50"
: "border-border hover:border-primary/50 hover:shadow-sm"
)}
data-testid={`profile-card-${profile.id}`}
>
{/* Drag Handle */}
<button
{...attributes}
{...listeners}
className="p-1 rounded hover:bg-accent cursor-grab active:cursor-grabbing flex-shrink-0 mt-1"
data-testid={`profile-drag-handle-${profile.id}`}
onClick={(e) => e.stopPropagation()}
>
<GripVertical className="h-4 w-4 text-muted-foreground" />
</button>
{/* Icon */}
<div
className={cn(
"flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center",
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
)}
>
{IconComponent && (
<IconComponent
className={cn(
"w-5 h-5",
isCodex ? "text-emerald-500" : "text-primary"
)}
/>
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-foreground">{profile.name}</h3>
{profile.isBuiltIn && (
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
<Lock className="w-2.5 h-2.5" />
Built-in
</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">
{profile.description}
</p>
<div className="flex items-center gap-2 mt-2 flex-wrap">
<span
className={cn(
"text-xs px-2 py-0.5 rounded-full border",
isCodex
? "border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10"
: "border-primary/30 text-primary bg-primary/10"
)}
>
{profile.model}
</span>
{profile.thinkingLevel !== "none" && (
<span className="text-xs px-2 py-0.5 rounded-full border border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/10">
{profile.thinkingLevel}
</span>
)}
</div>
</div>
{/* Actions */}
{!profile.isBuiltIn && (
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
onClick={onEdit}
className="h-8 w-8 p-0"
data-testid={`edit-profile-${profile.id}`}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={onDelete}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
data-testid={`delete-profile-${profile.id}`}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
)}
</div>
);
}
// Profile Form Component
function ProfileForm({
profile,
onSave,
onCancel,
isEditing,
hotkeyActive,
}: {
profile: Partial<AIProfile>;
onSave: (profile: Omit<AIProfile, "id">) => void;
onCancel: () => void;
isEditing: boolean;
hotkeyActive: boolean;
}) {
const [formData, setFormData] = useState({
name: profile.name || "",
description: profile.description || "",
model: profile.model || ("opus" as AgentModel),
thinkingLevel: profile.thinkingLevel || ("none" as ThinkingLevel),
icon: profile.icon || "Brain",
});
const provider = getProviderFromModel(formData.model);
const supportsThinking = modelSupportsThinking(formData.model);
const handleModelChange = (model: AgentModel) => {
const newProvider = getProviderFromModel(model);
setFormData({
...formData,
model,
// Reset thinking level when switching to Codex (doesn't support thinking)
thinkingLevel: newProvider === "codex" ? "none" : formData.thinkingLevel,
});
};
const handleSubmit = () => {
if (!formData.name.trim()) {
toast.error("Please enter a profile name");
return;
}
onSave({
name: formData.name.trim(),
description: formData.description.trim(),
model: formData.model,
thinkingLevel: supportsThinking ? formData.thinkingLevel : "none",
provider,
isBuiltIn: false,
icon: formData.icon,
});
};
return (
<div className="space-y-4">
{/* Name */}
<div className="space-y-2">
<Label htmlFor="profile-name">Profile Name</Label>
<Input
id="profile-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g., Heavy Task, Quick Fix"
data-testid="profile-name-input"
/>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="profile-description">Description</Label>
<Textarea
id="profile-description"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="Describe when to use this profile..."
rows={2}
data-testid="profile-description-input"
/>
</div>
{/* Icon Selection */}
<div className="space-y-2">
<Label>Icon</Label>
<div className="flex gap-2 flex-wrap">
{ICON_OPTIONS.map(({ name, icon: Icon }) => (
<button
key={name}
type="button"
onClick={() => setFormData({ ...formData, icon: name })}
className={cn(
"w-10 h-10 rounded-lg flex items-center justify-center border transition-colors",
formData.icon === name
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`icon-select-${name}`}
>
<Icon className="w-5 h-5" />
</button>
))}
</div>
</div>
{/* Model Selection - Claude */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" />
Claude Models
</Label>
<div className="flex gap-2 flex-wrap">
{CLAUDE_MODELS.map(({ id, label }) => (
<button
key={id}
type="button"
onClick={() => handleModelChange(id)}
className={cn(
"flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
formData.model === id
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`model-select-${id}`}
>
{label.replace("Claude ", "")}
</button>
))}
</div>
</div>
{/* Model Selection - Codex */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-500" />
Codex Models
</Label>
<div className="flex gap-2 flex-wrap">
{CODEX_MODELS.map(({ id, label }) => (
<button
key={id}
type="button"
onClick={() => handleModelChange(id)}
className={cn(
"flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
formData.model === id
? "bg-emerald-600 text-white border-emerald-500"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`model-select-${id}`}
>
{label.replace("GPT-5.1 ", "").replace("Codex ", "")}
</button>
))}
</div>
</div>
{/* Thinking Level - Only for Claude models */}
{supportsThinking && (
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-amber-500" />
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{THINKING_LEVELS.map(({ id, label }) => (
<button
key={id}
type="button"
onClick={() => {
setFormData({ ...formData, thinkingLevel: id });
if (id === "ultrathink") {
toast.warning("Ultrathink uses extensive reasoning", {
description:
"Best for complex architecture, migrations, or deep debugging (~$0.48/task).",
duration: 4000,
});
}
}}
className={cn(
"flex-1 min-w-[70px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
formData.thinkingLevel === id
? "bg-amber-500 text-white border-amber-400"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`thinking-select-${id}`}
>
{label}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex problems.
</p>
</div>
)}
{/* Actions */}
<DialogFooter className="pt-4">
<Button variant="ghost" onClick={onCancel}>
Cancel
</Button>
<HotkeyButton
onClick={handleSubmit}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={hotkeyActive}
data-testid="save-profile-button"
>
{isEditing ? "Save Changes" : "Create Profile"}
</HotkeyButton>
</DialogFooter>
</div>
);
}
export function ProfilesView() {
const {
aiProfiles,
addAIProfile,
updateAIProfile,
removeAIProfile,
reorderAIProfiles,
} = useAppStore();
const shortcuts = useKeyboardShortcutsConfig();
const [showAddDialog, setShowAddDialog] = useState(false);
const [editingProfile, setEditingProfile] = useState<AIProfile | null>(null);
// Sensors for drag-and-drop
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
})
);
// Separate built-in and custom profiles
const builtInProfiles = useMemo(
() => aiProfiles.filter((p) => p.isBuiltIn),
[aiProfiles]
);
const customProfiles = useMemo(
() => aiProfiles.filter((p) => !p.isBuiltIn),
[aiProfiles]
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = aiProfiles.findIndex((p) => p.id === active.id);
const newIndex = aiProfiles.findIndex((p) => p.id === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
reorderAIProfiles(oldIndex, newIndex);
}
}
},
[aiProfiles, reorderAIProfiles]
);
const handleAddProfile = (profile: Omit<AIProfile, "id">) => {
addAIProfile(profile);
setShowAddDialog(false);
toast.success("Profile created", {
description: `Created "${profile.name}" profile`,
});
};
const handleUpdateProfile = (profile: Omit<AIProfile, "id">) => {
if (editingProfile) {
updateAIProfile(editingProfile.id, profile);
setEditingProfile(null);
toast.success("Profile updated", {
description: `Updated "${profile.name}" profile`,
});
}
};
const handleDeleteProfile = (profile: AIProfile) => {
if (profile.isBuiltIn) return;
removeAIProfile(profile.id);
toast.success("Profile deleted", {
description: `Deleted "${profile.name}" profile`,
});
};
// Build keyboard shortcuts for profiles view
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcutsList: KeyboardShortcut[] = [];
// Add profile shortcut - when in profiles view
shortcutsList.push({
key: shortcuts.addProfile,
action: () => setShowAddDialog(true),
description: "Create new profile",
});
return shortcutsList;
}, [shortcuts]);
// Register keyboard shortcuts for profiles view
useKeyboardShortcuts(profilesShortcuts);
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="profiles-view"
>
{/* Header Section */}
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<UserCircle className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">
AI Profiles
</h1>
<p className="text-sm text-muted-foreground">
Create and manage model configuration presets
</p>
</div>
</div>
<HotkeyButton
onClick={() => setShowAddDialog(true)}
hotkey={shortcuts.addProfile}
hotkeyActive={false}
data-testid="add-profile-button"
>
<Plus className="w-4 h-4 mr-2" />
New Profile
</HotkeyButton>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-8">
{/* Custom Profiles Section */}
<div>
<div className="flex items-center gap-2 mb-4">
<h2 className="text-lg font-semibold text-foreground">
Custom Profiles
</h2>
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary">
{customProfiles.length}
</span>
</div>
{customProfiles.length === 0 ? (
<div className="rounded-xl border border-dashed border-border p-8 text-center">
<Sparkles className="w-10 h-10 text-muted-foreground mx-auto mb-3 opacity-50" />
<p className="text-muted-foreground">
No custom profiles yet. Create one to get started!
</p>
<Button
variant="outline"
className="mt-4"
onClick={() => setShowAddDialog(true)}
>
<Plus className="w-4 h-4 mr-2" />
Create Profile
</Button>
</div>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={customProfiles.map((p) => p.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-3">
{customProfiles.map((profile) => (
<SortableProfileCard
key={profile.id}
profile={profile}
onEdit={() => setEditingProfile(profile)}
onDelete={() => handleDeleteProfile(profile)}
/>
))}
</div>
</SortableContext>
</DndContext>
)}
</div>
{/* Built-in Profiles Section */}
<div>
<div className="flex items-center gap-2 mb-4">
<h2 className="text-lg font-semibold text-foreground">
Built-in Profiles
</h2>
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
{builtInProfiles.length}
</span>
</div>
<p className="text-sm text-muted-foreground mb-4">
Pre-configured profiles for common use cases. These cannot be
edited or deleted.
</p>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={builtInProfiles.map((p) => p.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-3">
{builtInProfiles.map((profile) => (
<SortableProfileCard
key={profile.id}
profile={profile}
onEdit={() => {}}
onDelete={() => {}}
/>
))}
</div>
</SortableContext>
</DndContext>
</div>
</div>
</div>
{/* Add Profile Dialog */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent data-testid="add-profile-dialog">
<DialogHeader>
<DialogTitle>Create New Profile</DialogTitle>
<DialogDescription>
Define a reusable model configuration preset.
</DialogDescription>
</DialogHeader>
<ProfileForm
profile={{}}
onSave={handleAddProfile}
onCancel={() => setShowAddDialog(false)}
isEditing={false}
hotkeyActive={showAddDialog}
/>
</DialogContent>
</Dialog>
{/* Edit Profile Dialog */}
<Dialog
open={!!editingProfile}
onOpenChange={() => setEditingProfile(null)}
>
<DialogContent data-testid="edit-profile-dialog">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Modify your profile settings.</DialogDescription>
</DialogHeader>
{editingProfile && (
<ProfileForm
profile={editingProfile}
onSave={handleUpdateProfile}
onCancel={() => setEditingProfile(null)}
isEditing={true}
hotkeyActive={!!editingProfile}
/>
)}
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,263 +0,0 @@
"use client";
import { useState } from "react";
import { useAppStore } from "@/store/app-store";
import { Label } from "@/components/ui/label";
import {
Key,
Palette,
Terminal,
Atom,
LayoutGrid,
FlaskConical,
Trash2,
Settings2,
Volume2,
VolumeX,
} from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { useCliStatus } from "./settings-view/hooks/use-cli-status";
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
import { SettingsHeader } from "./settings-view/components/settings-header";
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog";
import { SettingsNavigation } from "./settings-view/components/settings-navigation";
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status";
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
import { KanbanDisplaySection } from "./settings-view/kanban-display/kanban-display-section";
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section";
import type {
Project as SettingsProject,
Theme,
} from "./settings-view/shared/types";
import type { Project as ElectronProject } from "@/lib/electron";
// Navigation items for the side panel
const NAV_ITEMS = [
{ id: "api-keys", label: "API Keys", icon: Key },
{ id: "claude", label: "Claude", icon: Terminal },
{ id: "codex", label: "Codex", icon: Atom },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
{ id: "audio", label: "Audio", icon: Volume2 },
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
{ id: "danger", label: "Danger Zone", icon: Trash2 },
];
export function SettingsView() {
const {
theme,
setTheme,
setProjectTheme,
kanbanCardDetailLevel,
setKanbanCardDetailLevel,
defaultSkipTests,
setDefaultSkipTests,
useWorktrees,
setUseWorktrees,
showProfilesOnly,
setShowProfilesOnly,
muteDoneSound,
setMuteDoneSound,
currentProject,
moveProjectToTrash,
} = useAppStore();
// Convert electron Project to settings-view Project type
const convertProject = (
project: ElectronProject | null
): SettingsProject | null => {
if (!project) return null;
return {
id: project.id,
name: project.name,
path: project.path,
theme: project.theme as Theme | undefined,
};
};
const settingsProject = convertProject(currentProject);
// Compute the effective theme for the current project
const effectiveTheme = (settingsProject?.theme || theme) as Theme;
// Handler to set theme - saves to project if one is selected, otherwise to global
const handleSetTheme = (newTheme: typeof theme) => {
if (currentProject) {
setProjectTheme(currentProject.id, newTheme);
} else {
setTheme(newTheme);
}
};
// Use CLI status hook
const {
claudeCliStatus,
codexCliStatus,
isCheckingClaudeCli,
isCheckingCodexCli,
handleRefreshClaudeCli,
handleRefreshCodexCli,
} = useCliStatus();
// Use scroll tracking hook
const { activeSection, scrollToSection, scrollContainerRef } =
useScrollTracking({
items: NAV_ITEMS,
filterFn: (item) => item.id !== "danger" || !!currentProject,
initialSection: "api-keys",
});
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="settings-view"
>
{/* Header Section */}
<SettingsHeader />
{/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Sticky Side Navigation */}
<SettingsNavigation
navItems={NAV_ITEMS}
activeSection={activeSection}
currentProject={currentProject}
onNavigate={scrollToSection}
/>
{/* Scrollable Content */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6 pb-96">
{/* API Keys Section */}
<ApiKeysSection />
{/* Claude CLI Status Section */}
{claudeCliStatus && (
<ClaudeCliStatus
status={claudeCliStatus}
isChecking={isCheckingClaudeCli}
onRefresh={handleRefreshClaudeCli}
/>
)}
{/* Codex CLI Status Section */}
{codexCliStatus && (
<CodexCliStatus
status={codexCliStatus}
isChecking={isCheckingCodexCli}
onRefresh={handleRefreshCodexCli}
/>
)}
{/* Appearance Section */}
<AppearanceSection
effectiveTheme={effectiveTheme}
currentProject={settingsProject}
onThemeChange={handleSetTheme}
/>
{/* Kanban Card Display Section */}
<KanbanDisplaySection
detailLevel={kanbanCardDetailLevel}
onChange={setKanbanCardDetailLevel}
/>
{/* Keyboard Shortcuts Section */}
<KeyboardShortcutsSection
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
/>
{/* Audio Section */}
<div
id="audio"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Volume2 className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Audio
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure audio and notification settings.
</p>
</div>
<div className="p-6 space-y-4">
{/* Mute Done Sound Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="mute-done-sound"
checked={muteDoneSound}
onCheckedChange={(checked) =>
setMuteDoneSound(checked === true)
}
className="mt-0.5"
data-testid="mute-done-sound-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="mute-done-sound"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<VolumeX className="w-4 h-4 text-brand-500" />
Mute notification sound when agents complete
</Label>
<p className="text-xs text-muted-foreground">
When enabled, disables the &quot;ding&quot; sound that
plays when an agent completes a feature. The feature
will still move to the completed column, but without
audio notification.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Feature Defaults Section */}
<FeatureDefaultsSection
showProfilesOnly={showProfilesOnly}
defaultSkipTests={defaultSkipTests}
useWorktrees={useWorktrees}
onShowProfilesOnlyChange={setShowProfilesOnly}
onDefaultSkipTestsChange={setDefaultSkipTests}
onUseWorktreesChange={setUseWorktrees}
/>
{/* Danger Zone Section - Only show when a project is selected */}
<DangerZoneSection
project={settingsProject}
onDeleteClick={() => setShowDeleteDialog(true)}
/>
</div>
</div>
</div>
{/* Keyboard Map Dialog */}
<KeyboardMapDialog
open={showKeyboardMapDialog}
onOpenChange={setShowKeyboardMapDialog}
/>
{/* Delete Project Confirmation Dialog */}
<DeleteProjectDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
project={currentProject}
onConfirm={moveProjectToTrash}
/>
</div>
);
}

View File

@@ -1,72 +0,0 @@
import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store";
import { Button } from "@/components/ui/button";
import { Key, CheckCircle2 } from "lucide-react";
import { ApiKeyField } from "./api-key-field";
import { buildProviderConfigs } from "@/config/api-providers";
import { AuthenticationStatusDisplay } from "./authentication-status-display";
import { SecurityNotice } from "./security-notice";
import { useApiKeyManagement } from "./hooks/use-api-key-management";
export function ApiKeysSection() {
const { apiKeys } = useAppStore();
const { claudeAuthStatus, codexAuthStatus } = useSetupStore();
const { providerConfigParams, apiKeyStatus, handleSave, saved } =
useApiKeyManagement();
const providerConfigs = buildProviderConfigs(providerConfigParams);
return (
<div
id="api-keys"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">API Keys</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally in your
browser.
</p>
</div>
<div className="p-6 space-y-6">
{/* API Key Fields */}
{providerConfigs.map((provider) => (
<ApiKeyField key={provider.key} config={provider} />
))}
{/* Authentication Status Display */}
<AuthenticationStatusDisplay
claudeAuthStatus={claudeAuthStatus}
codexAuthStatus={codexAuthStatus}
apiKeyStatus={apiKeyStatus}
apiKeys={apiKeys}
/>
{/* Security Notice */}
<SecurityNotice />
{/* Save Button */}
<div className="flex items-center gap-4 pt-2">
<Button
onClick={handleSave}
data-testid="save-settings"
className="min-w-[120px] bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
>
{saved ? (
<>
<CheckCircle2 className="w-4 h-4 mr-2" />
Saved!
</>
) : (
"Save API Keys"
)}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,212 +0,0 @@
import { Label } from "@/components/ui/label";
import {
CheckCircle2,
AlertCircle,
Info,
Terminal,
Atom,
Sparkles,
} from "lucide-react";
import type { ClaudeAuthStatus, CodexAuthStatus } from "@/store/setup-store";
interface AuthenticationStatusDisplayProps {
claudeAuthStatus: ClaudeAuthStatus | null;
codexAuthStatus: CodexAuthStatus | null;
apiKeyStatus: {
hasAnthropicKey: boolean;
hasOpenAIKey: boolean;
hasGoogleKey: boolean;
} | null;
apiKeys: {
anthropic: string;
google: string;
openai: string;
};
}
export function AuthenticationStatusDisplay({
claudeAuthStatus,
codexAuthStatus,
apiKeyStatus,
apiKeys,
}: AuthenticationStatusDisplayProps) {
return (
<div className="space-y-4 pt-4 border-t border-border">
<div className="flex items-center gap-2 mb-3">
<Info className="w-4 h-4 text-brand-500" />
<Label className="text-foreground font-semibold">
Current Authentication Configuration
</Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Claude Authentication Status */}
<div className="p-3 rounded-lg bg-card border border-border">
<div className="flex items-center gap-2 mb-1.5">
<Terminal className="w-4 h-4 text-brand-500" />
<span className="text-sm font-medium text-foreground">
Claude (Anthropic)
</span>
</div>
<div className="space-y-1.5 text-xs min-h-12">
{claudeAuthStatus?.authenticated ? (
<>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
<span className="text-muted-foreground">
Method:{" "}
<span className="font-mono text-foreground">
{claudeAuthStatus.method === "oauth"
? "OAuth Token"
: claudeAuthStatus.method === "api_key"
? "API Key"
: "Unknown"}
</span>
</span>
</div>
{claudeAuthStatus.oauthTokenValid && (
<div className="flex items-center gap-2 text-green-400">
<CheckCircle2 className="w-3 h-3 shrink-0" />
<span>OAuth token configured</span>
</div>
)}
{claudeAuthStatus.apiKeyValid && (
<div className="flex items-center gap-2 text-green-400">
<CheckCircle2 className="w-3 h-3 shrink-0" />
<span>API key configured</span>
</div>
)}
{apiKeyStatus?.hasAnthropicKey && (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Environment variable detected</span>
</div>
)}
{apiKeys.anthropic && (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Manual API key in settings</span>
</div>
)}
</>
) : apiKeyStatus?.hasAnthropicKey ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using environment variable (ANTHROPIC_API_KEY)</span>
</div>
) : apiKeys.anthropic ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using manual API key from settings</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-muted-foreground py-0.5">
<AlertCircle className="w-2.5 h-2.5 shrink-0" />
<span className="text-xs">Not Setup</span>
</div>
)}
</div>
</div>
{/* Codex/OpenAI Authentication Status */}
<div className="p-3 rounded-lg bg-card border border-border">
<div className="flex items-center gap-2 mb-1.5">
<Atom className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium text-foreground">
Codex (OpenAI)
</span>
</div>
<div className="space-y-1.5 text-xs min-h-12">
{codexAuthStatus?.authenticated ? (
<>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
<span className="text-muted-foreground">
Method:{" "}
<span className="font-mono text-foreground">
{codexAuthStatus.method === "cli_verified" ||
codexAuthStatus.method === "cli_tokens"
? "CLI Login (OpenAI Account)"
: codexAuthStatus.method === "api_key"
? "API Key (Auth File)"
: codexAuthStatus.method === "env"
? "API Key (Environment)"
: "Unknown"}
</span>
</span>
</div>
{codexAuthStatus.method === "cli_verified" ||
codexAuthStatus.method === "cli_tokens" ? (
<div className="flex items-center gap-2 text-green-400">
<CheckCircle2 className="w-3 h-3 shrink-0" />
<span>Account authenticated</span>
</div>
) : codexAuthStatus.apiKeyValid ? (
<div className="flex items-center gap-2 text-green-400">
<CheckCircle2 className="w-3 h-3 shrink-0" />
<span>API key configured</span>
</div>
) : null}
{apiKeyStatus?.hasOpenAIKey && (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Environment variable detected</span>
</div>
)}
{apiKeys.openai && (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Manual API key in settings</span>
</div>
)}
</>
) : apiKeyStatus?.hasOpenAIKey ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using environment variable (OPENAI_API_KEY)</span>
</div>
) : apiKeys.openai ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using manual API key from settings</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-muted-foreground py-0.5">
<AlertCircle className="w-2.5 h-2.5 shrink-0" />
<span className="text-xs">Not Setup</span>
</div>
)}
</div>
</div>
{/* Google/Gemini Authentication Status */}
<div className="p-3 rounded-lg bg-card border border-border">
<div className="flex items-center gap-2 mb-1.5">
<Sparkles className="w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-foreground">
Gemini (Google)
</span>
</div>
<div className="space-y-1.5 text-xs min-h-12">
{apiKeyStatus?.hasGoogleKey ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using environment variable (GOOGLE_API_KEY)</span>
</div>
) : apiKeys.google ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using manual API key from settings</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-muted-foreground py-0.5">
<AlertCircle className="w-2.5 h-2.5 shrink-0" />
<span className="text-xs">Not Setup</span>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,61 +0,0 @@
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Palette } from "lucide-react";
import { themeOptions } from "@/config/theme-options";
import type { Theme, Project } from "../shared/types";
interface AppearanceSectionProps {
effectiveTheme: Theme;
currentProject: Project | null;
onThemeChange: (theme: Theme) => void;
}
export function AppearanceSection({
effectiveTheme,
currentProject,
onThemeChange,
}: AppearanceSectionProps) {
return (
<div
id="appearance"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Palette className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">Appearance</h2>
</div>
<p className="text-sm text-muted-foreground">
Customize the look and feel of your application.
</p>
</div>
<div className="p-6 space-y-4">
<div className="space-y-3">
<Label className="text-foreground">
Theme{" "}
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
</Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{themeOptions.map(({ value, label, Icon, testId }) => {
const isActive = effectiveTheme === value;
return (
<Button
key={value}
variant={isActive ? "secondary" : "outline"}
onClick={() => onThemeChange(value)}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
isActive ? "border-brand-500 ring-1 ring-brand-500/50" : ""
}`}
data-testid={testId}
>
<Icon className="w-4 h-4" />
<span className="font-medium text-sm">{label}</span>
</Button>
);
})}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,148 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Terminal,
CheckCircle2,
AlertCircle,
RefreshCw,
} from "lucide-react";
import type { CliStatus } from "../shared/types";
interface CliStatusProps {
status: CliStatus | null;
isChecking: boolean;
onRefresh: () => void;
}
export function ClaudeCliStatus({
status,
isChecking,
onRefresh,
}: CliStatusProps) {
if (!status) return null;
return (
<div
id="claude"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Claude Code CLI
</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={onRefresh}
disabled={isChecking}
data-testid="refresh-claude-cli"
title="Refresh Claude CLI detection"
>
<RefreshCw
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
/>
</Button>
</div>
<p className="text-sm text-muted-foreground">
Claude Code CLI provides better performance for long-running tasks,
especially with ultrathink.
</p>
</div>
<div className="p-6 space-y-4">
{status.success && status.status === "installed" ? (
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-green-400">
Claude Code CLI Installed
</p>
<div className="text-xs text-green-400/80 mt-1 space-y-1">
{status.method && (
<p>
Method: <span className="font-mono">{status.method}</span>
</p>
)}
{status.version && (
<p>
Version:{" "}
<span className="font-mono">{status.version}</span>
</p>
)}
{status.path && (
<p className="truncate" title={status.path}>
Path:{" "}
<span className="font-mono text-[10px]">
{status.path}
</span>
</p>
)}
</div>
</div>
</div>
{status.recommendation && (
<p className="text-xs text-muted-foreground">
{status.recommendation}
</p>
)}
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-400">
Claude Code CLI Not Detected
</p>
<p className="text-xs text-yellow-400/80 mt-1">
{status.recommendation ||
"Consider installing Claude Code CLI for optimal performance with ultrathink."}
</p>
</div>
</div>
{status.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-foreground-secondary">
Installation Commands:
</p>
<div className="space-y-1">
{status.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">npm:</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.npm}
</code>
</div>
)}
{status.installCommands.macos && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
macOS/Linux:
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.macos}
</code>
</div>
)}
{status.installCommands.windows && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
Windows (PowerShell):
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.windows}
</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,169 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Terminal,
CheckCircle2,
AlertCircle,
RefreshCw,
} from "lucide-react";
import type { CliStatus } from "../shared/types";
interface CliStatusProps {
status: CliStatus | null;
isChecking: boolean;
onRefresh: () => void;
}
export function CodexCliStatus({
status,
isChecking,
onRefresh,
}: CliStatusProps) {
if (!status) return null;
return (
<div
id="codex"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-foreground">
OpenAI Codex CLI
</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={onRefresh}
disabled={isChecking}
data-testid="refresh-codex-cli"
title="Refresh Codex CLI detection"
>
<RefreshCw
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
/>
</Button>
</div>
<p className="text-sm text-muted-foreground">
Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks.
</p>
</div>
<div className="p-6 space-y-4">
{status.success && status.status === "installed" ? (
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-green-400">
Codex CLI Installed
</p>
<div className="text-xs text-green-400/80 mt-1 space-y-1">
{status.method && (
<p>
Method: <span className="font-mono">{status.method}</span>
</p>
)}
{status.version && (
<p>
Version:{" "}
<span className="font-mono">{status.version}</span>
</p>
)}
{status.path && (
<p className="truncate" title={status.path}>
Path:{" "}
<span className="font-mono text-[10px]">
{status.path}
</span>
</p>
)}
</div>
</div>
</div>
{status.recommendation && (
<p className="text-xs text-muted-foreground">
{status.recommendation}
</p>
)}
</div>
) : status.status === "api_key_only" ? (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-blue-400">
API Key Detected - CLI Not Installed
</p>
<p className="text-xs text-blue-400/80 mt-1">
{status.recommendation ||
"OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."}
</p>
</div>
</div>
{status.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-foreground-secondary">
Installation Commands:
</p>
<div className="space-y-1">
{status.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">npm:</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.npm}
</code>
</div>
)}
</div>
</div>
)}
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-400">
Codex CLI Not Detected
</p>
<p className="text-xs text-yellow-400/80 mt-1">
{status.recommendation ||
"Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."}
</p>
</div>
</div>
{status.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-foreground-secondary">
Installation Commands:
</p>
<div className="space-y-1">
{status.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">npm:</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.npm}
</code>
</div>
)}
{status.installCommands.macos && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
macOS (Homebrew):
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{status.installCommands.macos}
</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,78 +0,0 @@
import { Trash2, Folder } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import type { Project } from "@/store/app-store";
interface DeleteProjectDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
project: Project | null;
onConfirm: (projectId: string) => void;
}
export function DeleteProjectDialog({
open,
onOpenChange,
project,
onConfirm,
}: DeleteProjectDialogProps) {
const handleConfirm = () => {
if (project) {
onConfirm(project.id);
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-popover border-border max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="w-5 h-5 text-destructive" />
Delete Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Are you sure you want to move this project to Trash?
</DialogDescription>
</DialogHeader>
{project && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">{project.name}</p>
<p className="text-xs text-muted-foreground truncate">{project.path}</p>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from Trash.
</p>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
data-testid="confirm-delete-project"
>
<Trash2 className="w-4 h-4 mr-2" />
Move to Trash
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,27 +0,0 @@
import { Settings } from "lucide-react";
interface SettingsHeaderProps {
title?: string;
description?: string;
}
export function SettingsHeader({
title = "Settings",
description = "Configure your API keys and preferences",
}: SettingsHeaderProps) {
return (
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<Settings className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,50 +0,0 @@
import { cn } from "@/lib/utils";
import type { Project } from "@/store/app-store";
import type { NavigationItem } from "../config/navigation";
interface SettingsNavigationProps {
navItems: NavigationItem[];
activeSection: string;
currentProject: Project | null;
onNavigate: (sectionId: string) => void;
}
export function SettingsNavigation({
navItems,
activeSection,
currentProject,
onNavigate,
}: SettingsNavigationProps) {
return (
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm">
<div className="sticky top-0 p-4 space-y-1">
{navItems
.filter((item) => item.id !== "danger" || currentProject)
.map((item) => {
const Icon = item.icon;
const isActive = activeSection === item.id;
return (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
className={cn(
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left",
isActive
? "bg-brand-500/10 text-brand-500 border border-brand-500/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<Icon
className={cn(
"w-4 h-4 shrink-0",
isActive ? "text-brand-500" : ""
)}
/>
<span className="truncate">{item.label}</span>
</button>
);
})}
</div>
</nav>
);
}

View File

@@ -1,57 +0,0 @@
import { Button } from "@/components/ui/button";
import { Trash2, Folder } from "lucide-react";
import type { Project } from "../shared/types";
interface DangerZoneSectionProps {
project: Project | null;
onDeleteClick: () => void;
}
export function DangerZoneSection({
project,
onDeleteClick,
}: DangerZoneSectionProps) {
if (!project) return null;
return (
<div
id="danger"
className="rounded-xl border border-destructive/30 bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-destructive/30">
<div className="flex items-center gap-2 mb-2">
<Trash2 className="w-5 h-5 text-destructive" />
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2>
</div>
<p className="text-sm text-muted-foreground">
Permanently remove this project from Automaker.
</p>
</div>
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{project.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{project.path}
</p>
</div>
</div>
<Button
variant="destructive"
onClick={onDeleteClick}
data-testid="delete-project-button"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,134 +0,0 @@
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react";
interface FeatureDefaultsSectionProps {
showProfilesOnly: boolean;
defaultSkipTests: boolean;
useWorktrees: boolean;
onShowProfilesOnlyChange: (value: boolean) => void;
onDefaultSkipTestsChange: (value: boolean) => void;
onUseWorktreesChange: (value: boolean) => void;
}
export function FeatureDefaultsSection({
showProfilesOnly,
defaultSkipTests,
useWorktrees,
onShowProfilesOnlyChange,
onDefaultSkipTestsChange,
onUseWorktreesChange,
}: FeatureDefaultsSectionProps) {
return (
<div
id="defaults"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<FlaskConical className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Feature Defaults
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure default settings for new features.
</p>
</div>
<div className="p-6 space-y-4">
{/* Profiles Only Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="show-profiles-only"
checked={showProfilesOnly}
onCheckedChange={(checked) =>
onShowProfilesOnlyChange(checked === true)
}
className="mt-0.5"
data-testid="show-profiles-only-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="show-profiles-only"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<Settings2 className="w-4 h-4 text-brand-500" />
Show profiles only by default
</Label>
<p className="text-xs text-muted-foreground">
When enabled, the Add Feature dialog will show only AI profiles
and hide advanced model tweaking options (Claude SDK, thinking
levels, and OpenAI Codex CLI). This creates a cleaner, less
overwhelming UI. You can always disable this to access advanced
settings.
</p>
</div>
</div>
</div>
{/* Separator */}
<div className="border-t border-border" />
{/* Skip Tests Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="default-skip-tests"
checked={defaultSkipTests}
onCheckedChange={(checked) =>
onDefaultSkipTestsChange(checked === true)
}
className="mt-0.5"
data-testid="default-skip-tests-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="default-skip-tests"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<TestTube className="w-4 h-4 text-brand-500" />
Skip automated testing by default
</Label>
<p className="text-xs text-muted-foreground">
When enabled, new features will default to manual verification
instead of TDD (test-driven development). You can still override
this for individual features.
</p>
</div>
</div>
</div>
{/* Worktree Isolation Setting */}
<div className="space-y-3 pt-2 border-t border-border">
<div className="flex items-start space-x-3">
<Checkbox
id="use-worktrees"
checked={useWorktrees}
onCheckedChange={(checked) =>
onUseWorktreesChange(checked === true)
}
className="mt-0.5"
data-testid="use-worktrees-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="use-worktrees"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<GitBranch className="w-4 h-4 text-brand-500" />
Enable Git Worktree Isolation (experimental)
</Label>
<p className="text-xs text-muted-foreground">
Creates isolated git branches for each feature. When disabled,
agents work directly in the main project directory. This feature
is experimental and may require additional setup like branch
selection and merge configuration.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,169 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI } from "@/lib/electron";
interface CliStatusResult {
success: boolean;
status?: string;
method?: string;
version?: string;
path?: string;
recommendation?: string;
installCommands?: {
macos?: string;
windows?: string;
linux?: string;
npm?: string;
};
error?: string;
}
interface CodexCliStatusResult extends CliStatusResult {
hasApiKey?: boolean;
}
/**
* Custom hook for managing Claude and Codex CLI status
* Handles checking CLI installation, authentication, and refresh functionality
*/
export function useCliStatus() {
const { setClaudeAuthStatus, setCodexAuthStatus } = useSetupStore();
const [claudeCliStatus, setClaudeCliStatus] =
useState<CliStatusResult | null>(null);
const [codexCliStatus, setCodexCliStatus] =
useState<CodexCliStatusResult | null>(null);
const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);
const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false);
// Check CLI status on mount
useEffect(() => {
const checkCliStatus = async () => {
const api = getElectronAPI();
// Check Claude CLI
if (api?.checkClaudeCli) {
try {
const status = await api.checkClaudeCli();
setClaudeCliStatus(status);
} catch (error) {
console.error("Failed to check Claude CLI status:", error);
}
}
// Check Codex CLI
if (api?.checkCodexCli) {
try {
const status = await api.checkCodexCli();
setCodexCliStatus(status);
} catch (error) {
console.error("Failed to check Codex CLI status:", error);
}
}
// Check Claude auth status (re-fetch on mount to ensure persistence)
if (api?.setup?.getClaudeStatus) {
try {
const result = await api.setup.getClaudeStatus();
if (result.success && result.auth) {
const auth = result.auth;
const authStatus = {
authenticated: auth.authenticated,
method:
auth.method === "oauth_token"
? ("oauth" as const)
: auth.method?.includes("api_key")
? ("api_key" as const)
: ("none" as const),
hasCredentialsFile: auth.hasCredentialsFile ?? false,
oauthTokenValid: auth.hasStoredOAuthToken,
apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey,
};
setClaudeAuthStatus(authStatus);
}
} catch (error) {
console.error("Failed to check Claude auth status:", error);
}
}
// Check Codex auth status (re-fetch on mount to ensure persistence)
if (api?.setup?.getCodexStatus) {
try {
const result = await api.setup.getCodexStatus();
if (result.success && result.auth) {
const auth = result.auth;
// Determine method - prioritize cli_verified and cli_tokens over auth_file
const method =
auth.method === "cli_verified" || auth.method === "cli_tokens"
? auth.method === "cli_verified"
? ("cli_verified" as const)
: ("cli_tokens" as const)
: auth.method === "auth_file"
? ("api_key" as const)
: auth.method === "env_var"
? ("env" as const)
: ("none" as const);
const authStatus = {
authenticated: auth.authenticated,
method,
// Only set apiKeyValid for actual API key methods, not CLI login
apiKeyValid:
method === "cli_verified" || method === "cli_tokens"
? undefined
: auth.hasAuthFile || auth.hasEnvKey,
};
setCodexAuthStatus(authStatus);
}
} catch (error) {
console.error("Failed to check Codex auth status:", error);
}
}
};
checkCliStatus();
}, [setClaudeAuthStatus, setCodexAuthStatus]);
// Refresh Claude CLI status
const handleRefreshClaudeCli = useCallback(async () => {
setIsCheckingClaudeCli(true);
try {
const api = getElectronAPI();
if (api?.checkClaudeCli) {
const status = await api.checkClaudeCli();
setClaudeCliStatus(status);
}
} catch (error) {
console.error("Failed to refresh Claude CLI status:", error);
} finally {
setIsCheckingClaudeCli(false);
}
}, []);
// Refresh Codex CLI status
const handleRefreshCodexCli = useCallback(async () => {
setIsCheckingCodexCli(true);
try {
const api = getElectronAPI();
if (api?.checkCodexCli) {
const status = await api.checkCodexCli();
setCodexCliStatus(status);
}
} catch (error) {
console.error("Failed to refresh Codex CLI status:", error);
} finally {
setIsCheckingCodexCli(false);
}
}, []);
return {
claudeCliStatus,
codexCliStatus,
isCheckingClaudeCli,
isCheckingCodexCli,
handleRefreshClaudeCli,
handleRefreshCodexCli,
};
}

View File

@@ -1,96 +0,0 @@
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { LayoutGrid, Minimize2, Square, Maximize2 } from "lucide-react";
import type { KanbanDetailLevel } from "../shared/types";
interface KanbanDisplaySectionProps {
detailLevel: KanbanDetailLevel;
onChange: (level: KanbanDetailLevel) => void;
}
export function KanbanDisplaySection({
detailLevel,
onChange,
}: KanbanDisplaySectionProps) {
return (
<div
id="kanban"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<LayoutGrid className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Kanban Card Display
</h2>
</div>
<p className="text-sm text-muted-foreground">
Control how much information is displayed on Kanban cards.
</p>
</div>
<div className="p-6 space-y-4">
<div className="space-y-3">
<Label className="text-foreground">Detail Level</Label>
<div className="grid grid-cols-3 gap-3">
<Button
variant={detailLevel === "minimal" ? "secondary" : "outline"}
onClick={() => onChange("minimal")}
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
detailLevel === "minimal"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="kanban-detail-minimal"
>
<Minimize2 className="w-5 h-5" />
<span className="font-medium text-sm">Minimal</span>
<span className="text-xs text-muted-foreground text-center">
Title & category only
</span>
</Button>
<Button
variant={detailLevel === "standard" ? "secondary" : "outline"}
onClick={() => onChange("standard")}
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
detailLevel === "standard"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="kanban-detail-standard"
>
<Square className="w-5 h-5" />
<span className="font-medium text-sm">Standard</span>
<span className="text-xs text-muted-foreground text-center">
Steps & progress
</span>
</Button>
<Button
variant={detailLevel === "detailed" ? "secondary" : "outline"}
onClick={() => onChange("detailed")}
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
detailLevel === "detailed"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="kanban-detail-detailed"
>
<Maximize2 className="w-5 h-5" />
<span className="font-medium text-sm">Detailed</span>
<span className="text-xs text-muted-foreground text-center">
Model, tools & tasks
</span>
</Button>
</div>
<p className="text-xs text-muted-foreground">
<strong>Minimal:</strong> Shows only title and category
<br />
<strong>Standard:</strong> Adds steps preview and progress bar
<br />
<strong>Detailed:</strong> Shows all info including model, tool
calls, task list, and summaries
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,59 +0,0 @@
import { Button } from "@/components/ui/button";
import { Settings2, Keyboard } from "lucide-react";
interface KeyboardShortcutsSectionProps {
onOpenKeyboardMap: () => void;
}
export function KeyboardShortcutsSection({
onOpenKeyboardMap,
}: KeyboardShortcutsSectionProps) {
return (
<div
id="keyboard"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Settings2 className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Keyboard Shortcuts
</h2>
</div>
<p className="text-sm text-muted-foreground">
Customize keyboard shortcuts for navigation and actions using the
visual keyboard map.
</p>
</div>
<div className="p-6">
{/* Centered message directing to keyboard map */}
<div className="flex flex-col items-center justify-center py-16 text-center space-y-4">
<div className="relative">
<Keyboard className="w-16 h-16 text-brand-500/30" />
<div className="absolute inset-0 bg-brand-500/10 blur-xl rounded-full" />
</div>
<div className="space-y-2 max-w-md">
<h3 className="text-lg font-semibold text-foreground">
Use the Visual Keyboard Map
</h3>
<p className="text-sm text-muted-foreground">
Click the &quot;View Keyboard Map&quot; button above to customize
your keyboard shortcuts. The visual interface shows all available
keys and lets you easily edit shortcuts with single-modifier
restrictions.
</p>
</div>
<Button
variant="default"
size="lg"
onClick={onOpenKeyboardMap}
className="gap-2 mt-4"
>
<Keyboard className="w-5 h-5" />
Open Keyboard Map
</Button>
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,437 +0,0 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
import type { SpecRegenerationEvent } from "@/types/electron";
export function SpecView() {
const { currentProject, appSpec, setAppSpec } = useAppStore();
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [specExists, setSpecExists] = useState(true);
// Regeneration state
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
const [projectDefinition, setProjectDefinition] = useState("");
const [isRegenerating, setIsRegenerating] = useState(false);
// Create spec state
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [projectOverview, setProjectOverview] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [generateFeatures, setGenerateFeatures] = useState(true);
// Load spec from file
const loadSpec = useCallback(async () => {
if (!currentProject) return;
setIsLoading(true);
try {
const api = getElectronAPI();
const result = await api.readFile(
`${currentProject.path}/.automaker/app_spec.txt`
);
if (result.success && result.content) {
setAppSpec(result.content);
setSpecExists(true);
setHasChanges(false);
} else {
// File doesn't exist
setAppSpec("");
setSpecExists(false);
}
} catch (error) {
console.error("Failed to load spec:", error);
setSpecExists(false);
} finally {
setIsLoading(false);
}
}, [currentProject, setAppSpec]);
useEffect(() => {
loadSpec();
}, [loadSpec]);
// Subscribe to spec regeneration events
useEffect(() => {
const api = getElectronAPI();
if (!api.specRegeneration) return;
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
console.log("[SpecView] Regeneration event:", event.type);
if (event.type === "spec_regeneration_complete") {
setIsRegenerating(false);
setIsCreating(false);
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition("");
setProjectOverview("");
// Reload the spec to show the new content
loadSpec();
} else if (event.type === "spec_regeneration_error") {
setIsRegenerating(false);
setIsCreating(false);
console.error("[SpecView] Regeneration error:", event.error);
}
});
return () => {
unsubscribe();
};
}, [loadSpec]);
// Save spec to file
const saveSpec = async () => {
if (!currentProject) return;
setIsSaving(true);
try {
const api = getElectronAPI();
await api.writeFile(
`${currentProject.path}/.automaker/app_spec.txt`,
appSpec
);
setHasChanges(false);
} catch (error) {
console.error("Failed to save spec:", error);
} finally {
setIsSaving(false);
}
};
const handleChange = (value: string) => {
setAppSpec(value);
setHasChanges(true);
};
const handleRegenerate = async () => {
if (!currentProject || !projectDefinition.trim()) return;
setIsRegenerating(true);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
console.error("[SpecView] Spec regeneration not available");
setIsRegenerating(false);
return;
}
const result = await api.specRegeneration.generate(
currentProject.path,
projectDefinition.trim()
);
if (!result.success) {
console.error("[SpecView] Failed to start regeneration:", result.error);
setIsRegenerating(false);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
console.error("[SpecView] Failed to regenerate spec:", error);
setIsRegenerating(false);
}
};
const handleCreateSpec = async () => {
if (!currentProject || !projectOverview.trim()) return;
setIsCreating(true);
setShowCreateDialog(false);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
console.error("[SpecView] Spec regeneration not available");
setIsCreating(false);
return;
}
const result = await api.specRegeneration.create(
currentProject.path,
projectOverview.trim(),
generateFeatures
);
if (!result.success) {
console.error("[SpecView] Failed to start spec creation:", result.error);
setIsCreating(false);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
console.error("[SpecView] Failed to create spec:", error);
setIsCreating(false);
}
};
if (!currentProject) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="spec-view-no-project"
>
<p className="text-muted-foreground">No project selected</p>
</div>
);
}
if (isLoading) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="spec-view-loading"
>
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
// Show empty state when no spec exists (isCreating is handled by bottom-right indicator in sidebar)
if (!specExists) {
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="spec-view-empty"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">App Specification</h1>
<p className="text-sm text-muted-foreground">
{currentProject.path}/.automaker/app_spec.txt
</p>
</div>
</div>
</div>
{/* Empty State */}
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center max-w-md">
<div className="mb-6 flex justify-center">
<div className="p-4 rounded-full bg-primary/10">
<FilePlus2 className="w-12 h-12 text-primary" />
</div>
</div>
<h2 className="text-2xl font-semibold mb-3">No App Specification Found</h2>
<p className="text-muted-foreground mb-6">
Create an app specification to help our system understand your project.
We&apos;ll analyze your codebase and generate a comprehensive spec based on your description.
</p>
<Button
size="lg"
onClick={() => setShowCreateDialog(true)}
>
<FilePlus2 className="w-5 h-5 mr-2" />
Create app_spec
</Button>
</div>
</div>
{/* Create Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create App Specification</DialogTitle>
<DialogDescription className="text-muted-foreground">
We didn&apos;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&apos;ll analyze your project&apos;s
tech stack and create a comprehensive specification.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Project Overview
</label>
<p className="text-xs text-muted-foreground">
Describe what your project does and what features you want to build.
Be as detailed as you want - this will help us create a better specification.
</p>
<textarea
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
value={projectOverview}
onChange={(e) => setProjectOverview(e.target.value)}
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
autoFocus
/>
</div>
<div className="flex items-start space-x-3 pt-2">
<Checkbox
id="generate-features"
checked={generateFeatures}
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
/>
<div className="space-y-1">
<label
htmlFor="generate-features"
className="text-sm font-medium cursor-pointer"
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder from the
implementation roadmap after the spec is generated.
</p>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowCreateDialog(false)}
>
Cancel
</Button>
<HotkeyButton
onClick={handleCreateSpec}
disabled={!projectOverview.trim()}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showCreateDialog}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="spec-view"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">App Specification</h1>
<p className="text-sm text-muted-foreground">
{currentProject.path}/.automaker/app_spec.txt
</p>
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setShowRegenerateDialog(true)}
disabled={isRegenerating}
data-testid="regenerate-spec"
>
{isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>
<Button
size="sm"
onClick={saveSpec}
disabled={!hasChanges || isSaving}
data-testid="save-spec"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
</Button>
</div>
</div>
{/* Editor */}
<div className="flex-1 p-4 overflow-hidden">
<Card className="h-full overflow-hidden">
<XmlSyntaxEditor
value={appSpec}
onChange={handleChange}
placeholder="Write your app specification here..."
data-testid="spec-editor"
/>
</Card>
</div>
{/* Regenerate Dialog */}
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Regenerate App Specification</DialogTitle>
<DialogDescription className="text-muted-foreground">
We will regenerate your app spec based on a short project definition and the
current tech stack found in your project. The agent will analyze your codebase
to understand your existing technologies and create a comprehensive specification.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Describe your project
</label>
<p className="text-xs text-muted-foreground">
Provide a clear description of what your app should do. Be as detailed as you
want - the more context you provide, the more comprehensive the spec will be.
</p>
<textarea
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
value={projectDefinition}
onChange={(e) => setProjectDefinition(e.target.value)}
placeholder="e.g., A task management app where users can create projects, add tasks with due dates, assign tasks to team members, track progress with a kanban board, and receive notifications for upcoming deadlines..."
disabled={isRegenerating}
/>
</div>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowRegenerateDialog(false)}
disabled={isRegenerating}
>
Cancel
</Button>
<HotkeyButton
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showRegenerateDialog}
>
{isRegenerating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Regenerate Spec
</>
)}
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,620 +0,0 @@
"use client";
import { useState, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { initializeProject } from "@/lib/project-init";
import {
FolderOpen,
Plus,
Folder,
Clock,
Sparkles,
MessageSquare,
ChevronDown,
Loader2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { toast } from "sonner";
export function WelcomeView() {
const { projects, addProject, setCurrentProject, setCurrentView } =
useAppStore();
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false);
const [newProjectName, setNewProjectName] = useState("");
const [newProjectPath, setNewProjectPath] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [isOpening, setIsOpening] = useState(false);
const [showInitDialog, setShowInitDialog] = useState(false);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [initStatus, setInitStatus] = useState<{
isNewProject: boolean;
createdFiles: string[];
projectName: string;
projectPath: string;
} | null>(null);
/**
* Kick off project analysis agent to analyze the codebase
*/
const analyzeProject = useCallback(async (projectPath: string) => {
const api = getElectronAPI();
if (!api.autoMode?.analyzeProject) {
console.log("[Welcome] Auto mode API not available, skipping analysis");
return;
}
setIsAnalyzing(true);
try {
console.log("[Welcome] Starting project analysis for:", projectPath);
const result = await api.autoMode.analyzeProject(projectPath);
if (result.success) {
toast.success("Project analyzed", {
description: "AI agent has analyzed your project structure",
});
} else {
console.error("[Welcome] Project analysis failed:", result.error);
}
} catch (error) {
console.error("[Welcome] Failed to analyze project:", error);
} finally {
setIsAnalyzing(false);
}
}, []);
/**
* Initialize project and optionally kick off project analysis agent
*/
const initializeAndOpenProject = useCallback(
async (path: string, name: string) => {
setIsOpening(true);
try {
// Initialize the .automaker directory structure
const initResult = await initializeProject(path);
if (!initResult.success) {
toast.error("Failed to initialize project", {
description: initResult.error || "Unknown error occurred",
});
return;
}
const project = {
id: `project-${Date.now()}`,
name,
path,
lastOpened: new Date().toISOString(),
};
addProject(project);
setCurrentProject(project);
// Show initialization dialog if files were created
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
setInitStatus({
isNewProject: initResult.isNewProject,
createdFiles: initResult.createdFiles,
projectName: name,
projectPath: path,
});
setShowInitDialog(true);
// Kick off agent to analyze the project and update app_spec.txt
console.log(
"[Welcome] Project initialized, created files:",
initResult.createdFiles
);
console.log("[Welcome] Kicking off project analysis agent...");
// Start analysis in background (don't await, let it run async)
analyzeProject(path);
} else {
toast.success("Project opened", {
description: `Opened ${name}`,
});
}
} catch (error) {
console.error("[Welcome] Failed to open project:", error);
toast.error("Failed to open project", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsOpening(false);
}
},
[addProject, setCurrentProject, analyzeProject]
);
const handleOpenProject = useCallback(async () => {
const api = getElectronAPI();
const result = await api.openDirectory();
if (!result.canceled && result.filePaths[0]) {
const path = result.filePaths[0];
// Extract folder name from path (works on both Windows and Mac/Linux)
const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
await initializeAndOpenProject(path, name);
}
}, [initializeAndOpenProject]);
/**
* Handle clicking on a recent project
*/
const handleRecentProjectClick = useCallback(
async (project: { id: string; name: string; path: string }) => {
await initializeAndOpenProject(project.path, project.name);
},
[initializeAndOpenProject]
);
const handleNewProject = () => {
setNewProjectName("");
setNewProjectPath("");
setShowNewProjectDialog(true);
};
const handleInteractiveMode = () => {
setCurrentView("interview");
};
const handleSelectDirectory = async () => {
const api = getElectronAPI();
const result = await api.openDirectory();
if (!result.canceled && result.filePaths[0]) {
setNewProjectPath(result.filePaths[0]);
}
};
const handleCreateProject = async () => {
if (!newProjectName || !newProjectPath) return;
setIsCreating(true);
try {
const api = getElectronAPI();
const projectPath = `${newProjectPath}/${newProjectName}`;
// Create project directory
await api.mkdir(projectPath);
// Initialize .automaker directory with all necessary files
const initResult = await initializeProject(projectPath);
if (!initResult.success) {
toast.error("Failed to initialize project", {
description: initResult.error || "Unknown error occurred",
});
return;
}
// Update the app_spec.txt with the project name
await api.writeFile(
`${projectPath}/.automaker/app_spec.txt`,
`<project_specification>
<project_name>${newProjectName}</project_name>
<overview>
Describe your project here. This file will be analyzed by an AI agent
to understand your project structure and tech stack.
</overview>
<technology_stack>
<!-- The AI agent will fill this in after analyzing your project -->
</technology_stack>
<core_capabilities>
<!-- List core features and capabilities -->
</core_capabilities>
<implemented_features>
<!-- The AI agent will populate this based on code analysis -->
</implemented_features>
</project_specification>`
);
const project = {
id: `project-${Date.now()}`,
name: newProjectName,
path: projectPath,
lastOpened: new Date().toISOString(),
};
addProject(project);
setCurrentProject(project);
setShowNewProjectDialog(false);
toast.success("Project created", {
description: `Created ${newProjectName} with .automaker directory`,
});
// Set init status to show the dialog
setInitStatus({
isNewProject: true,
createdFiles: initResult.createdFiles || [],
projectName: newProjectName,
projectPath: projectPath,
});
setShowInitDialog(true);
} catch (error) {
console.error("Failed to create project:", error);
toast.error("Failed to create project", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsCreating(false);
}
};
const recentProjects = [...projects]
.sort((a, b) => {
const dateA = a.lastOpened ? new Date(a.lastOpened).getTime() : 0;
const dateB = b.lastOpened ? new Date(b.lastOpened).getTime() : 0;
return dateB - dateA;
})
.slice(0, 5);
return (
<div className="flex-1 flex flex-col content-bg" data-testid="welcome-view">
{/* Header Section */}
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl flex items-center justify-center">
<img src="/logo.png" alt="Automaker Logo" className="w-10 h-10" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">
Welcome to Automaker
</h1>
<p className="text-sm text-muted-foreground">
Your autonomous AI development studio
</p>
</div>
</div>
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-6xl mx-auto">
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
<div
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200"
data-testid="new-project-card"
>
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/5 to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="relative p-6 h-full flex flex-col">
<div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-lg bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center group-hover:scale-110 transition-transform shrink-0">
<Plus className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground mb-1">
New Project
</h3>
<p className="text-sm text-muted-foreground">
Create a new project from scratch with AI-powered
development
</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="w-full mt-4 bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
data-testid="create-new-project"
>
<Plus className="w-4 h-4 mr-2" />
Create Project
<ChevronDown className="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={handleNewProject}
data-testid="quick-setup-option"
>
<Plus className="w-4 h-4 mr-2" />
Quick Setup
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleInteractiveMode}
data-testid="interactive-mode-option"
>
<MessageSquare className="w-4 h-4 mr-2" />
Interactive Mode
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200 cursor-pointer"
onClick={handleOpenProject}
data-testid="open-project-card"
>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="relative p-6 h-full flex flex-col">
<div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:scale-110 transition-transform shrink-0">
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground mb-1">
Open Project
</h3>
<p className="text-sm text-muted-foreground">
Open an existing project folder to continue working
</p>
</div>
</div>
<Button
variant="secondary"
className="w-full mt-4 bg-secondary hover:bg-secondary/80 text-foreground border border-border hover:border-border-glass"
data-testid="open-existing-project"
>
<FolderOpen className="w-4 h-4 mr-2" />
Browse Folder
</Button>
</div>
</div>
</div>
{/* Recent Projects */}
{recentProjects.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-4">
<Clock className="w-5 h-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">
Recent Projects
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{recentProjects.map((project) => (
<div
key={project.id}
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer"
onClick={() => handleRecentProjectClick(project)}
data-testid={`recent-project-${project.id}`}
>
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all"></div>
<div className="relative p-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:border-brand-500/50 transition-colors">
<Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
{project.name}
</p>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">
{project.path}
</p>
{project.lastOpened && (
<p className="text-xs text-muted-foreground mt-1">
{new Date(
project.lastOpened
).toLocaleDateString()}
</p>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Empty State for No Projects */}
{recentProjects.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-16 h-16 rounded-2xl bg-muted border border-border flex items-center justify-center mb-4">
<Sparkles className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">
No projects yet
</h3>
<p className="text-sm text-zinc-400 max-w-md">
Get started by creating a new project or opening an existing one
</p>
</div>
)}
</div>
</div>
{/* New Project Dialog */}
<Dialog
open={showNewProjectDialog}
onOpenChange={setShowNewProjectDialog}
>
<DialogContent
className="bg-card border-border"
data-testid="new-project-dialog"
>
<DialogHeader>
<DialogTitle className="text-foreground">
Create New Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Set up a new project directory with initial configuration files.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="project-name" className="text-foreground">
Project Name
</Label>
<Input
id="project-name"
placeholder="my-awesome-project"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
className="bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid="project-name-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="project-path" className="text-foreground">
Parent Directory
</Label>
<div className="flex gap-2">
<Input
id="project-path"
placeholder="/path/to/projects"
value={newProjectPath}
onChange={(e) => setNewProjectPath(e.target.value)}
className="flex-1 bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid="project-path-input"
/>
<Button
variant="secondary"
onClick={handleSelectDirectory}
className="bg-secondary hover:bg-secondary/80 text-foreground border border-border"
data-testid="browse-directory"
>
Browse
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowNewProjectDialog(false)}
className="text-muted-foreground hover:text-foreground hover:bg-accent"
>
Cancel
</Button>
<HotkeyButton
onClick={handleCreateProject}
disabled={!newProjectName || !newProjectPath || isCreating}
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showNewProjectDialog}
data-testid="confirm-create-project"
>
{isCreating ? "Creating..." : "Create Project"}
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Project Initialization Dialog */}
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
<DialogContent
className="bg-card border-border"
data-testid="project-init-dialog"
>
<DialogHeader>
<DialogTitle className="text-foreground flex items-center gap-2">
<Sparkles className="w-5 h-5 text-brand-500" />
{initStatus?.isNewProject
? "Project Initialized"
: "Project Updated"}
</DialogTitle>
<DialogDescription className="text-muted-foreground">
{initStatus?.isNewProject
? `Created .automaker directory structure for ${initStatus?.projectName}`
: `Updated missing files in .automaker for ${initStatus?.projectName}`}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p className="text-sm text-foreground font-medium">
Created files:
</p>
<ul className="space-y-1.5">
{initStatus?.createdFiles.map((file) => (
<li
key={file}
className="flex items-center gap-2 text-sm text-muted-foreground"
>
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
<code className="text-xs bg-muted px-2 py-0.5 rounded">
{file}
</code>
</li>
))}
</ul>
</div>
{initStatus?.isNewProject && (
<div className="mt-4 p-3 rounded-lg bg-muted/50 border border-border-glass">
{isAnalyzing ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 text-brand-500 animate-spin" />
<p className="text-sm text-brand-400">
AI agent is analyzing your project structure...
</p>
</div>
) : (
<p className="text-sm text-muted-foreground">
<span className="text-brand-400">Tip:</span> Edit the{" "}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
app_spec.txt
</code>{" "}
file to describe your project. The AI agent will use this to
understand your project structure.
</p>
)}
</div>
)}
</div>
<DialogFooter>
<Button
onClick={() => setShowInitDialog(false)}
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
data-testid="close-init-dialog"
>
Get Started
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Loading overlay when opening project */}
{isOpening && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
data-testid="project-opening-overlay"
>
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-card border border-border">
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" />
<p className="text-foreground font-medium">
Initializing project...
</p>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,403 +0,0 @@
/**
* Log Parser Utility
* Parses agent output into structured sections for display
*/
export type LogEntryType =
| "prompt"
| "tool_call"
| "tool_result"
| "phase"
| "error"
| "success"
| "info"
| "debug"
| "warning"
| "thinking";
export interface LogEntry {
id: string;
type: LogEntryType;
title: string;
content: string;
timestamp?: string;
collapsed?: boolean;
metadata?: {
toolName?: string;
phase?: string;
[key: string]: string | undefined;
};
}
/**
* Generates a deterministic ID based on content and position
* This ensures the same log entry always gets the same ID,
* preserving expanded/collapsed state when new logs stream in
*
* Uses only the first 200 characters of content to ensure stability
* even when entries are merged (which appends content at the end)
*/
const generateDeterministicId = (content: string, lineIndex: number): string => {
// Use first 200 chars to ensure stability when entries are merged
const stableContent = content.slice(0, 200);
// Simple hash function for the content
let hash = 0;
const str = stableContent + '|' + lineIndex.toString();
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return 'log_' + Math.abs(hash).toString(36);
};
/**
* Detects the type of log entry based on content patterns
*/
function detectEntryType(content: string): LogEntryType {
const trimmed = content.trim();
// Tool calls
if (trimmed.startsWith("🔧 Tool:") || trimmed.match(/^Tool:\s*/)) {
return "tool_call";
}
// Tool results / Input
if (trimmed.startsWith("Input:") || trimmed.startsWith("Result:") || trimmed.startsWith("Output:")) {
return "tool_result";
}
// Phase changes
if (
trimmed.startsWith("📋") ||
trimmed.startsWith("⚡") ||
trimmed.startsWith("✅") ||
trimmed.match(/^(Planning|Action|Verification)/i)
) {
return "phase";
}
// Errors
if (trimmed.startsWith("❌") || trimmed.toLowerCase().includes("error:")) {
return "error";
}
// Success messages
if (
trimmed.startsWith("✅") ||
trimmed.toLowerCase().includes("success") ||
trimmed.toLowerCase().includes("completed")
) {
return "success";
}
// Warnings
if (trimmed.startsWith("⚠️") || trimmed.toLowerCase().includes("warning:")) {
return "warning";
}
// Thinking/Preparation info
if (
trimmed.toLowerCase().includes("ultrathink") ||
trimmed.toLowerCase().includes("thinking level") ||
trimmed.toLowerCase().includes("estimated cost") ||
trimmed.toLowerCase().includes("estimated time") ||
trimmed.toLowerCase().includes("budget tokens") ||
trimmed.match(/thinking.*preparation/i)
) {
return "thinking";
}
// Debug info (JSON, stack traces, etc.)
if (
trimmed.startsWith("{") ||
trimmed.startsWith("[") ||
trimmed.includes("at ") ||
trimmed.match(/^\s*\d+\s*\|/)
) {
return "debug";
}
// Default to info
return "info";
}
/**
* Extracts tool name from a tool call entry
*/
function extractToolName(content: string): string | undefined {
const match = content.match(/🔧\s*Tool:\s*(\S+)/);
return match?.[1];
}
/**
* Extracts phase name from a phase entry
*/
function extractPhase(content: string): string | undefined {
if (content.includes("📋")) return "planning";
if (content.includes("⚡")) return "action";
if (content.includes("✅")) return "verification";
const match = content.match(/^(Planning|Action|Verification)/i);
return match?.[1]?.toLowerCase();
}
/**
* Generates a title for a log entry
*/
function generateTitle(type: LogEntryType, content: string): string {
switch (type) {
case "tool_call": {
const toolName = extractToolName(content);
return toolName ? `Tool Call: ${toolName}` : "Tool Call";
}
case "tool_result":
return "Tool Input/Result";
case "phase": {
const phase = extractPhase(content);
return phase ? `Phase: ${phase.charAt(0).toUpperCase() + phase.slice(1)}` : "Phase Change";
}
case "error":
return "Error";
case "success":
return "Success";
case "warning":
return "Warning";
case "thinking":
return "Thinking Level";
case "debug":
return "Debug Info";
case "prompt":
return "Prompt";
default:
return "Info";
}
}
/**
* Parses raw log output into structured entries
*/
export function parseLogOutput(rawOutput: string): LogEntry[] {
if (!rawOutput || !rawOutput.trim()) {
return [];
}
const entries: LogEntry[] = [];
const lines = rawOutput.split("\n");
let currentEntry: Omit<LogEntry, 'id'> & { id?: string } | null = null;
let currentContent: string[] = [];
let entryStartLine = 0; // Track the starting line for deterministic ID generation
const finalizeEntry = () => {
if (currentEntry && currentContent.length > 0) {
currentEntry.content = currentContent.join("\n").trim();
if (currentEntry.content) {
// Generate deterministic ID based on content and position
const entryWithId: LogEntry = {
...currentEntry as Omit<LogEntry, 'id'>,
id: generateDeterministicId(currentEntry.content, entryStartLine),
};
entries.push(entryWithId);
}
}
currentContent = [];
};
let lineIndex = 0;
for (const line of lines) {
const trimmedLine = line.trim();
// Skip empty lines at the beginning
if (!trimmedLine && !currentEntry) {
lineIndex++;
continue;
}
// Detect if this line starts a new entry
const lineType = detectEntryType(trimmedLine);
const isNewEntry =
trimmedLine.startsWith("🔧") ||
trimmedLine.startsWith("📋") ||
trimmedLine.startsWith("⚡") ||
trimmedLine.startsWith("✅") ||
trimmedLine.startsWith("❌") ||
trimmedLine.startsWith("⚠️") ||
trimmedLine.startsWith("🧠") ||
trimmedLine.toLowerCase().includes("ultrathink preparation") ||
trimmedLine.toLowerCase().includes("thinking level") ||
(trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call");
if (isNewEntry) {
// Finalize previous entry
finalizeEntry();
// Track starting line for deterministic ID
entryStartLine = lineIndex;
// Start new entry (ID will be generated when finalizing)
currentEntry = {
type: lineType,
title: generateTitle(lineType, trimmedLine),
content: "",
metadata: {
toolName: extractToolName(trimmedLine),
phase: extractPhase(trimmedLine),
},
};
currentContent.push(trimmedLine);
} else if (currentEntry) {
// Continue current entry
currentContent.push(line);
} else {
// Track starting line for deterministic ID
entryStartLine = lineIndex;
// No current entry, create a default info entry
currentEntry = {
type: "info",
title: "Info",
content: "",
};
currentContent.push(line);
}
lineIndex++;
}
// Finalize last entry
finalizeEntry();
// Merge consecutive entries of the same type if they're both debug or info
const mergedEntries = mergeConsecutiveEntries(entries);
return mergedEntries;
}
/**
* Merges consecutive entries of the same type for cleaner display
*/
function mergeConsecutiveEntries(entries: LogEntry[]): LogEntry[] {
if (entries.length <= 1) return entries;
const merged: LogEntry[] = [];
let current: LogEntry | null = null;
let mergeIndex = 0;
for (const entry of entries) {
if (
current &&
(current.type === "debug" || current.type === "info") &&
current.type === entry.type
) {
// Merge into current - regenerate ID based on merged content
current.content += "\n\n" + entry.content;
current.id = generateDeterministicId(current.content, mergeIndex);
} else {
if (current) {
merged.push(current);
}
current = { ...entry };
mergeIndex = merged.length;
}
}
if (current) {
merged.push(current);
}
return merged;
}
/**
* Gets the color classes for a log entry type
*/
export function getLogTypeColors(type: LogEntryType): {
bg: string;
border: string;
text: string;
icon: string;
badge: string;
} {
switch (type) {
case "prompt":
return {
bg: "bg-blue-500/10",
border: "border-l-blue-500",
text: "text-blue-300",
icon: "text-blue-400",
badge: "bg-blue-500/20 text-blue-300",
};
case "tool_call":
return {
bg: "bg-amber-500/10",
border: "border-l-amber-500",
text: "text-amber-300",
icon: "text-amber-400",
badge: "bg-amber-500/20 text-amber-300",
};
case "tool_result":
return {
bg: "bg-slate-500/10",
border: "border-l-slate-400",
text: "text-slate-300",
icon: "text-slate-400",
badge: "bg-slate-500/20 text-slate-300",
};
case "phase":
return {
bg: "bg-cyan-500/10",
border: "border-l-cyan-500",
text: "text-cyan-300",
icon: "text-cyan-400",
badge: "bg-cyan-500/20 text-cyan-300",
};
case "error":
return {
bg: "bg-red-500/10",
border: "border-l-red-500",
text: "text-red-300",
icon: "text-red-400",
badge: "bg-red-500/20 text-red-300",
};
case "success":
return {
bg: "bg-emerald-500/10",
border: "border-l-emerald-500",
text: "text-emerald-300",
icon: "text-emerald-400",
badge: "bg-emerald-500/20 text-emerald-300",
};
case "warning":
return {
bg: "bg-orange-500/10",
border: "border-l-orange-500",
text: "text-orange-300",
icon: "text-orange-400",
badge: "bg-orange-500/20 text-orange-300",
};
case "thinking":
return {
bg: "bg-indigo-500/10",
border: "border-l-indigo-500",
text: "text-indigo-300",
icon: "text-indigo-400",
badge: "bg-indigo-500/20 text-indigo-300",
};
case "debug":
return {
bg: "bg-primary/10",
border: "border-l-primary",
text: "text-primary",
icon: "text-primary",
badge: "bg-primary/20 text-primary",
};
default:
return {
bg: "bg-zinc-500/10",
border: "border-l-zinc-500",
text: "text-zinc-300",
icon: "text-zinc-400",
badge: "bg-zinc-500/20 text-zinc-300",
};
}
}

View File

@@ -1,45 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import type { AgentModel } from "@/store/app-store"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Check if a model is a Codex/OpenAI model (doesn't support thinking)
*/
export function isCodexModel(model?: AgentModel | string): boolean {
if (!model) return false;
const codexModels: string[] = [
"gpt-5.1-codex-max",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1",
];
return codexModels.includes(model);
}
/**
* Determine if the current model supports extended thinking controls
*/
export function modelSupportsThinking(model?: AgentModel | string): boolean {
if (!model) return true;
return !isCodexModel(model);
}
/**
* Get display name for a model
*/
export function getModelDisplayName(model: AgentModel | string): string {
const displayNames: Record<string, string> = {
haiku: "Claude Haiku",
sonnet: "Claude Sonnet",
opus: "Claude Opus",
"gpt-5.1-codex-max": "GPT-5.1 Codex Max",
"gpt-5.1-codex": "GPT-5.1 Codex",
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
"gpt-5.1": "GPT-5.1",
};
return displayNames[model] || model;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -48,3 +48,4 @@ next-env.d.ts
# Electron
/dist/
/server-bundle/

435
apps/app/electron/main.js Normal file
View File

@@ -0,0 +1,435 @@
/**
* Simplified Electron main process
*
* This version spawns the backend server and uses HTTP API for most operations.
* Only native features (dialogs, shell) use IPC.
*/
const path = require("path");
const { spawn } = require("child_process");
const fs = require("fs");
const http = require("http");
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
// Load environment variables from .env file (development only)
if (!app.isPackaged) {
try {
require("dotenv").config({ path: path.join(__dirname, "../.env") });
} catch (error) {
console.warn("[Electron] dotenv not available:", error.message);
}
}
let mainWindow = null;
let serverProcess = null;
let staticServer = null;
const SERVER_PORT = 3008;
const STATIC_PORT = 3007;
// Get icon path - works in both dev and production, cross-platform
function getIconPath() {
// Different icon formats for different platforms
let iconFile;
if (process.platform === "win32") {
iconFile = "icon.ico";
} else if (process.platform === "darwin") {
iconFile = "logo_larger.png";
} else {
// Linux
iconFile = "logo_larger.png";
}
const iconPath = path.join(__dirname, "../public", iconFile);
// Verify the icon exists
if (!fs.existsSync(iconPath)) {
console.warn(`[Electron] Icon not found at: ${iconPath}`);
return null;
}
return iconPath;
}
/**
* Start static file server for production builds
*/
async function startStaticServer() {
const staticPath = path.join(__dirname, "../out");
staticServer = http.createServer((request, response) => {
// Parse the URL and remove query string
let filePath = path.join(staticPath, request.url.split("?")[0]);
// Default to index.html for directory requests
if (filePath.endsWith("/")) {
filePath = path.join(filePath, "index.html");
} else if (!path.extname(filePath)) {
filePath += ".html";
}
// Check if file exists
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
// Try index.html for SPA fallback
filePath = path.join(staticPath, "index.html");
}
// Read and serve the file
fs.readFile(filePath, (error, content) => {
if (error) {
response.writeHead(500);
response.end("Server Error");
return;
}
// Set content type based on file extension
const ext = path.extname(filePath);
const contentTypes = {
".html": "text/html",
".js": "application/javascript",
".css": "text/css",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".eot": "application/vnd.ms-fontobject",
};
response.writeHead(200, { "Content-Type": contentTypes[ext] || "application/octet-stream" });
response.end(content);
});
});
});
return new Promise((resolve, reject) => {
staticServer.listen(STATIC_PORT, (err) => {
if (err) {
reject(err);
} else {
console.log(`[Electron] Static server running at http://localhost:${STATIC_PORT}`);
resolve();
}
});
});
}
/**
* Start the backend server
*/
async function startServer() {
const isDev = !app.isPackaged;
// Server entry point - use tsx in dev, compiled version in production
let command, args, serverPath;
if (isDev) {
// In development, use tsx to run TypeScript directly
// Use node from PATH (process.execPath in Electron points to Electron, not Node.js)
// spawn() resolves "node" from PATH on all platforms (Windows, Linux, macOS)
command = "node";
serverPath = path.join(__dirname, "../../server/src/index.ts");
// Find tsx CLI - check server node_modules first, then root
const serverNodeModules = path.join(
__dirname,
"../../server/node_modules/tsx"
);
const rootNodeModules = path.join(__dirname, "../../../node_modules/tsx");
let tsxCliPath;
if (fs.existsSync(path.join(serverNodeModules, "dist/cli.mjs"))) {
tsxCliPath = path.join(serverNodeModules, "dist/cli.mjs");
} else if (fs.existsSync(path.join(rootNodeModules, "dist/cli.mjs"))) {
tsxCliPath = path.join(rootNodeModules, "dist/cli.mjs");
} else {
// Last resort: try require.resolve
try {
tsxCliPath = require.resolve("tsx/cli.mjs", {
paths: [path.join(__dirname, "../../server")],
});
} catch {
throw new Error(
"Could not find tsx. Please run 'npm install' in the server directory."
);
}
}
args = [tsxCliPath, "watch", serverPath];
} else {
// In production, use compiled JavaScript
command = "node";
serverPath = path.join(process.resourcesPath, "server", "index.js");
args = [serverPath];
// Verify server files exist
if (!fs.existsSync(serverPath)) {
throw new Error(`Server not found at: ${serverPath}`);
}
}
// Set environment variables for server
const serverNodeModules = app.isPackaged
? path.join(process.resourcesPath, "server", "node_modules")
: path.join(__dirname, "../../server/node_modules");
// Set default workspace directory to user's Documents/Automaker
const defaultWorkspaceDir = path.join(app.getPath("documents"), "Automaker");
// Ensure workspace directory exists
if (!fs.existsSync(defaultWorkspaceDir)) {
try {
fs.mkdirSync(defaultWorkspaceDir, { recursive: true });
console.log("[Electron] Created workspace directory:", defaultWorkspaceDir);
} catch (error) {
console.error("[Electron] Failed to create workspace directory:", error);
}
}
const env = {
...process.env,
PORT: SERVER_PORT.toString(),
DATA_DIR: app.getPath("userData"),
NODE_PATH: serverNodeModules,
WORKSPACE_DIR: process.env.WORKSPACE_DIR || defaultWorkspaceDir,
};
console.log("[Electron] Starting backend server...");
console.log("[Electron] Server path:", serverPath);
console.log("[Electron] NODE_PATH:", serverNodeModules);
serverProcess = spawn(command, args, {
cwd: path.dirname(serverPath),
env,
stdio: ["ignore", "pipe", "pipe"],
});
serverProcess.stdout.on("data", (data) => {
console.log(`[Server] ${data.toString().trim()}`);
});
serverProcess.stderr.on("data", (data) => {
console.error(`[Server Error] ${data.toString().trim()}`);
});
serverProcess.on("close", (code) => {
console.log(`[Server] Process exited with code ${code}`);
serverProcess = null;
});
serverProcess.on("error", (err) => {
console.error(`[Server] Failed to start server process:`, err);
serverProcess = null;
});
// Wait for server to be ready
await waitForServer();
}
/**
* Wait for server to be available
*/
async function waitForServer(maxAttempts = 30) {
const http = require("http");
for (let i = 0; i < maxAttempts; i++) {
try {
await new Promise((resolve, reject) => {
const req = http.get(
`http://localhost:${SERVER_PORT}/api/health`,
(res) => {
if (res.statusCode === 200) {
resolve();
} else {
reject(new Error(`Status: ${res.statusCode}`));
}
}
);
req.on("error", reject);
req.setTimeout(1000, () => {
req.destroy();
reject(new Error("Timeout"));
});
});
console.log("[Electron] Server is ready");
return;
} catch {
await new Promise((r) => setTimeout(r, 500));
}
}
throw new Error("Server failed to start");
}
/**
* Create the main window
*/
function createWindow() {
const iconPath = getIconPath();
const windowOptions = {
width: 1400,
height: 900,
minWidth: 1024,
minHeight: 700,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
titleBarStyle: "hiddenInset",
backgroundColor: "#0a0a0a",
};
// Only set icon if it exists
if (iconPath) {
windowOptions.icon = iconPath;
}
mainWindow = new BrowserWindow(windowOptions);
// Load Next.js dev server in development or static server in production
const isDev = !app.isPackaged;
mainWindow.loadURL(`http://localhost:${STATIC_PORT}`);
if (isDev && process.env.OPEN_DEVTOOLS === "true") {
mainWindow.webContents.openDevTools();
}
mainWindow.on("closed", () => {
mainWindow = null;
});
// Handle external links - open in default browser
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: "deny" };
});
}
// App lifecycle
app.whenReady().then(async () => {
// Set app icon (dock icon on macOS)
if (process.platform === "darwin" && app.dock) {
const iconPath = getIconPath();
if (iconPath) {
try {
app.dock.setIcon(iconPath);
} catch (error) {
console.warn("[Electron] Failed to set dock icon:", error.message);
}
}
}
try {
// Start static file server in production
if (app.isPackaged) {
await startStaticServer();
}
// Start backend server
await startServer();
// Create window
createWindow();
} catch (error) {
console.error("[Electron] Failed to start:", error);
app.quit();
}
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("before-quit", () => {
// Kill server process
if (serverProcess) {
console.log("[Electron] Stopping server...");
serverProcess.kill();
serverProcess = null;
}
// Close static server
if (staticServer) {
console.log("[Electron] Stopping static server...");
staticServer.close();
staticServer = null;
}
});
// ============================================
// IPC Handlers - Only native features
// ============================================
// Native file dialogs
ipcMain.handle("dialog:openDirectory", async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory", "createDirectory"],
});
return result;
});
ipcMain.handle("dialog:openFile", async (_, options = {}) => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openFile"],
...options,
});
return result;
});
ipcMain.handle("dialog:saveFile", async (_, options = {}) => {
const result = await dialog.showSaveDialog(mainWindow, options);
return result;
});
// Shell operations
ipcMain.handle("shell:openExternal", async (_, url) => {
try {
await shell.openExternal(url);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle("shell:openPath", async (_, filePath) => {
try {
await shell.openPath(filePath);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// App info
ipcMain.handle("app:getPath", async (_, name) => {
return app.getPath(name);
});
ipcMain.handle("app:getVersion", async () => {
return app.getVersion();
});
ipcMain.handle("app:isPackaged", async () => {
return app.isPackaged;
});
// Ping - for connection check
ipcMain.handle("ping", async () => {
return "pong";
});
// Get server URL for HTTP client
ipcMain.handle("server:getUrl", async () => {
return `http://localhost:${SERVER_PORT}`;
});

View File

@@ -0,0 +1,37 @@
/**
* 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,9 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
env: {
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || "",
},
output: "export",
};
export default nextConfig;

194
apps/app/package.json Normal file
View File

@@ -0,0 +1,194 @@
{
"name": "@automaker/app",
"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",
"repository": {
"type": "git",
"url": "https://github.com/AutoMaker-Org/automaker.git"
},
"author": {
"name": "Cody Seibert",
"email": "webdevcody@gmail.com"
},
"private": true,
"license": "Unlicense",
"main": "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",
"postinstall": "electron-builder install-app-deps",
"start": "next start",
"lint": "eslint",
"pretest": "node scripts/setup-e2e-fixtures.js",
"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\""
},
"dependencies": {
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lezer/highlight": "^1.2.3",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.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-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.12",
"@uiw/react-codemirror": "^4.25.4",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"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",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.9"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "^1.29.2",
"lightningcss-darwin-x64": "^1.29.2",
"lightningcss-linux-arm-gnueabihf": "^1.29.2",
"lightningcss-linux-arm64-gnu": "^1.29.2",
"lightningcss-linux-arm64-musl": "^1.29.2",
"lightningcss-linux-x64-gnu": "^1.29.2",
"lightningcss-linux-x64-musl": "^1.29.2",
"lightningcss-win32-arm64-msvc": "^1.29.2",
"lightningcss-win32-x64-msvc": "^1.29.2"
},
"devDependencies": {
"@electron/rebuild": "^4.0.2",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.2.1",
"electron": "39.2.7",
"electron-builder": "^26.0.12",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "5.9.3",
"wait-on": "^9.0.3"
},
"build": {
"appId": "com.automaker.app",
"productName": "Automaker",
"artifactName": "${productName}-${version}-${arch}.${ext}",
"afterPack": "./scripts/rebuild-server-natives.js",
"directories": {
"output": "dist"
},
"files": [
"electron/**/*",
"out/**/*",
"public/**/*",
"!node_modules/**/*"
],
"extraResources": [
{
"from": "server-bundle/dist",
"to": "server"
},
{
"from": "server-bundle/node_modules",
"to": "server/node_modules"
},
{
"from": "server-bundle/package.json",
"to": "server/package.json"
},
{
"from": "../../.env",
"to": ".env",
"filter": [
"**/*"
]
}
],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "public/logo_larger.png"
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"icon": "public/icon.ico"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"category": "Development",
"icon": "public/logo_larger.png",
"maintainer": "webdevcody@gmail.com",
"executableName": "automaker"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

View File

@@ -0,0 +1,59 @@
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";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
timeout: 30000,
use: {
baseURL: `http://localhost:${port}`,
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
...(reuseServer
? {}
: {
webServer: [
// Backend server - runs with mock agent enabled in CI
{
command: `cd ../server && npm run dev`,
url: `http://localhost:${serverPort}/api/health`,
reuseExistingServer: true,
timeout: 60000,
env: {
...process.env,
PORT: String(serverPort),
// Enable mock agent in CI to avoid real API calls
AUTOMAKER_MOCK_AGENT: mockAgent ? "true" : "false",
// Allow access to test directories and common project paths
ALLOWED_PROJECT_DIRS: "/Users,/home,/tmp,/var/folders",
},
},
// Frontend Next.js server
{
command: `npx next dev -p ${port}`,
url: `http://localhost:${port}`,
reuseExistingServer: true,
timeout: 120000,
env: {
...process.env,
NEXT_PUBLIC_SKIP_SETUP: "true",
},
},
],
}),
});

View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-label="Code icon">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#6B5BFF"></stop>
<stop offset="100%" stop-color="#2EC7FF"></stop>
</linearGradient>
<filter id="iconShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#000000" flood-opacity="0.25"></feDropShadow>
</filter>
</defs>
<!-- Rounded square background -->
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#bg)"></rect>
<!-- </> icon (slightly reduced overall size) -->
<g fill="none" stroke="#FFFFFF" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" filter="url(#iconShadow)">
<!-- Left bracket < -->
<path d="M92 92 L52 128 L92 164"></path>
<!-- Slash / -->
<path d="M144 72 L116 184"></path>
<!-- Right bracket > -->
<path d="M164 92 L204 128 L164 164"></path>
</g>
</svg>

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

BIN
apps/app/public/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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