diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml new file mode 100644 index 00000000..a58020ec --- /dev/null +++ b/.github/actions/setup-project/action.yml @@ -0,0 +1,66 @@ +name: "Setup Project" +description: "Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules" + +inputs: + node-version: + description: "Node.js version to use" + required: false + default: "22" + check-lockfile: + description: "Run lockfile lint check for SSH URLs" + required: false + default: "false" + rebuild-node-pty-path: + description: "Working directory for node-pty rebuild (empty = root)" + required: false + default: "" + +runs: + using: "composite" + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: "npm" + cache-dependency-path: package-lock.json + + - name: Check for SSH URLs in lockfile + if: inputs.check-lockfile == 'true' + shell: bash + run: npm run lint:lockfile + + - name: Configure Git for HTTPS + shell: bash + # Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp) + # This is needed because SSH authentication isn't available in CI + run: git config --global url."https://github.com/".insteadOf "git@github.com:" + + - name: Install dependencies + shell: bash + # Use npm install instead of npm ci to correctly resolve platform-specific + # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) + # Skip scripts to avoid electron-builder install-app-deps which uses too much memory + run: npm install --ignore-scripts + + - name: Install Linux native bindings + shell: bash + # Workaround for npm optional dependencies bug (npm/cli#4828) + # Explicitly install Linux bindings needed for build tools + run: | + npm install --no-save --force --ignore-scripts \ + @rollup/rollup-linux-x64-gnu@4.53.3 \ + @tailwindcss/oxide-linux-x64-gnu@4.1.17 + + - name: Rebuild native modules (root) + if: inputs.rebuild-node-pty-path == '' + shell: bash + # Rebuild node-pty and other native modules for Electron + run: npm rebuild node-pty + + - name: Rebuild native modules (workspace) + if: inputs.rebuild-node-pty-path != '' + shell: bash + # Rebuild node-pty and other native modules needed for server + run: npm rebuild node-pty + working-directory: ${{ inputs.rebuild-node-pty-path }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 24065347..9f8e49a8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -18,34 +18,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup project + uses: ./.github/actions/setup-project with: - node-version: "22" - cache: "npm" - cache-dependency-path: package-lock.json - - - name: Configure Git for HTTPS - # Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp) - # This is needed because SSH authentication isn't available in CI - run: git config --global url."https://github.com/".insteadOf "git@github.com:" - - - name: Install dependencies - # Use npm install instead of npm ci to correctly resolve platform-specific - # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) - run: npm install - - - name: Install Linux native bindings - # Workaround for npm optional dependencies bug (npm/cli#4828) - # Explicitly install Linux bindings needed for build tools - run: | - npm install --no-save --force \ - @rollup/rollup-linux-x64-gnu@4.53.3 \ - @tailwindcss/oxide-linux-x64-gnu@4.1.17 + check-lockfile: "true" + rebuild-node-pty-path: "apps/server" - name: Install Playwright browsers run: npx playwright install --with-deps chromium - working-directory: apps/app + working-directory: apps/ui - name: Build server run: npm run build --workspace=apps/server @@ -71,20 +52,20 @@ jobs: exit 1 - name: Run E2E tests - # Playwright automatically starts the Next.js frontend via webServer config - # (see apps/app/playwright.config.ts) - no need to start it manually - run: npm run test --workspace=apps/app + # Playwright automatically starts the Vite frontend via webServer config + # (see apps/ui/playwright.config.ts) - no need to start it manually + run: npm run test --workspace=apps/ui env: CI: true - NEXT_PUBLIC_SERVER_URL: http://localhost:3008 - NEXT_PUBLIC_SKIP_SETUP: "true" + VITE_SERVER_URL: http://localhost:3008 + VITE_SKIP_SETUP: "true" - name: Upload Playwright report uses: actions/upload-artifact@v4 if: always() with: name: playwright-report - path: apps/app/playwright-report/ + path: apps/ui/playwright-report/ retention-days: 7 - name: Upload test results @@ -92,5 +73,5 @@ jobs: if: failure() with: name: test-results - path: apps/app/test-results/ + path: apps/ui/test-results/ retention-days: 7 diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ea452a15..38e0c978 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -17,33 +17,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup project + uses: ./.github/actions/setup-project with: - node-version: "22" - cache: "npm" - cache-dependency-path: package-lock.json + check-lockfile: "true" - - name: Check for SSH URLs in lockfile - run: npm run lint:lockfile - - - name: Configure Git for HTTPS - # Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp) - # This is needed because SSH authentication isn't available in CI - run: git config --global url."https://github.com/".insteadOf "git@github.com:" - - - name: Install dependencies - # Use npm install instead of npm ci to correctly resolve platform-specific - # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) - run: npm install - - - name: Install Linux native bindings - # Workaround for npm optional dependencies bug (npm/cli#4828) - # Explicitly install Linux bindings needed for build tools - run: | - npm install --no-save --force \ - @rollup/rollup-linux-x64-gnu@4.53.3 \ - @tailwindcss/oxide-linux-x64-gnu@4.1.17 - - - name: Run build:electron - run: npm run build:electron + - name: Run build:electron (dir only - faster CI) + run: npm run build:electron:dir diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index aa6ec548..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,180 +0,0 @@ -name: Build and Release Electron App - -on: - push: - tags: - - "v*.*.*" # Triggers on version tags like v1.0.0 - workflow_dispatch: # Allows manual triggering - inputs: - version: - description: "Version to release (e.g., v1.0.0)" - required: true - default: "v0.1.0" - -jobs: - build-and-release: - strategy: - fail-fast: false - matrix: - include: - - os: macos-latest - name: macOS - artifact-name: macos-builds - - os: windows-latest - name: Windows - artifact-name: windows-builds - - os: ubuntu-latest - name: Linux - artifact-name: linux-builds - - runs-on: ${{ matrix.os }} - - permissions: - contents: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - cache-dependency-path: package-lock.json - - - name: Configure Git for HTTPS - # Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp) - # This is needed because SSH authentication isn't available in CI - run: git config --global url."https://github.com/".insteadOf "git@github.com:" - - - name: Install dependencies - # Use npm install instead of npm ci to correctly resolve platform-specific - # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) - run: npm install - - - name: Install Linux native bindings - # Workaround for npm optional dependencies bug (npm/cli#4828) - # Only needed on Linux - macOS and Windows get their bindings automatically - if: matrix.os == 'ubuntu-latest' - run: | - npm install --no-save --force \ - @rollup/rollup-linux-x64-gnu@4.53.3 \ - @tailwindcss/oxide-linux-x64-gnu@4.1.17 - - - name: Extract and set version - id: version - shell: bash - run: | - VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}" - # Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0) - VERSION="${VERSION_TAG#v}" - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION from tag: $VERSION_TAG" - # Update the app's package.json version - cd apps/app - npm version $VERSION --no-git-tag-version - cd ../.. - echo "Updated apps/app/package.json to version $VERSION" - - - name: Build Electron App (macOS) - if: matrix.os == 'macos-latest' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm run build:electron -- --mac --x64 --arm64 - - - name: Build Electron App (Windows) - if: matrix.os == 'windows-latest' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm run build:electron -- --win --x64 - - - name: Build Electron App (Linux) - if: matrix.os == 'ubuntu-latest' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm run build:electron -- --linux --x64 - - - name: Upload Release Assets - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ github.event.inputs.version || github.ref_name }} - files: | - apps/app/dist/*.exe - apps/app/dist/*.dmg - apps/app/dist/*.AppImage - apps/app/dist/*.zip - apps/app/dist/*.deb - apps/app/dist/*.rpm - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload macOS artifacts for R2 - if: matrix.os == 'macos-latest' - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact-name }} - path: apps/app/dist/*.dmg - retention-days: 1 - - - name: Upload Windows artifacts for R2 - if: matrix.os == 'windows-latest' - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact-name }} - path: apps/app/dist/*.exe - retention-days: 1 - - - name: Upload Linux artifacts for R2 - if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact-name }} - path: apps/app/dist/*.AppImage - retention-days: 1 - - upload-to-r2: - needs: build-and-release - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Install AWS SDK - run: npm install @aws-sdk/client-s3 - - - name: Extract version - id: version - shell: bash - run: | - VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}" - # Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0) - VERSION="${VERSION_TAG#v}" - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION from tag: $VERSION_TAG" - - - name: Upload to R2 and update releases.json - env: - R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} - R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} - R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} - R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} - R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }} - RELEASE_VERSION: ${{ steps.version.outputs.version }} - RELEASE_TAG: ${{ steps.version.outputs.version_tag }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: node .github/scripts/upload-to-r2.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cadeb2f3..1d15b425 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,30 +17,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup project + uses: ./.github/actions/setup-project with: - node-version: "22" - cache: "npm" - cache-dependency-path: package-lock.json - - - name: Configure Git for HTTPS - # Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp) - # This is needed because SSH authentication isn't available in CI - run: git config --global url."https://github.com/".insteadOf "git@github.com:" - - - name: Install dependencies - # Use npm install instead of npm ci to correctly resolve platform-specific - # optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries) - run: npm install - - - name: Install Linux native bindings - # Workaround for npm optional dependencies bug (npm/cli#4828) - # Explicitly install Linux bindings needed for build tools - run: | - npm install --no-save --force \ - @rollup/rollup-linux-x64-gnu@4.53.3 \ - @tailwindcss/oxide-linux-x64-gnu@4.1.17 + check-lockfile: "true" + rebuild-node-pty-path: "apps/server" - name: Run server tests with coverage run: npm run test:server:coverage diff --git a/README.md b/README.md index 8c863c53..39c31d4b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Automaker Logo + Automaker Logo

> **[!TIP]** @@ -88,6 +88,7 @@ The future of software development is **agentic coding**β€”where developers beco Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows. In the Discord, you can: + - πŸ’¬ Discuss agentic coding patterns and best practices - 🧠 Share ideas for AI-driven development workflows - πŸ› οΈ Get help setting up or extending Automaker @@ -252,19 +253,16 @@ This project is licensed under the **Automaker License Agreement**. See [LICENSE **Summary of Terms:** - **Allowed:** - - **Build Anything:** You can clone and use Automaker locally or in your organization to build ANY product (commercial or free). - **Internal Use:** You can use it internally within your company (commercial or non-profit) without restriction. - **Modify:** You can modify the code for internal use within your organization (commercial or non-profit). - **Restricted (The "No Monetization of the Tool" Rule):** - - **No Resale:** You cannot resell Automaker itself. - **No SaaS:** You cannot host Automaker as a service for others. - **No Monetizing Mods:** You cannot distribute modified versions of Automaker for money. - **Liability:** - - **Use at Own Risk:** This tool uses AI. We are **NOT** responsible if it breaks your computer, deletes your files, or generates bad code. You assume all risk. - **Contributing:** diff --git a/REFACTORING_CANDIDATES.md b/REFACTORING_CANDIDATES.md deleted file mode 100644 index fcff3070..00000000 --- a/REFACTORING_CANDIDATES.md +++ /dev/null @@ -1,310 +0,0 @@ -# Large Files - Refactoring Candidates - -This document tracks files in the AutoMaker codebase that exceed 3000 lines or are significantly large (1000+ lines) and should be considered for refactoring into smaller, more maintainable components. - -**Last Updated:** 2025-12-15 -**Total Large Files:** 8 -**Combined Size:** 15,027 lines - ---- - -## πŸ”΄ CRITICAL - Over 3000 Lines - -### 1. board-view.tsx - 3,325 lines -**Path:** `apps/app/src/components/views/board-view.tsx` -**Type:** React Component (TSX) -**Priority:** VERY HIGH - -**Description:** -Main Kanban board view component that serves as the centerpiece of the application. - -**Current Responsibilities:** -- Feature/task card management and drag-and-drop operations using @dnd-kit -- Adding, editing, and deleting features -- Running autonomous agents to implement features -- Displaying feature status across multiple columns (Backlog, In Progress, Waiting Approval, Verified) -- Model/AI profile selection for feature implementation -- Advanced options configuration (thinking level, model selection, skip tests) -- Search/filtering functionality for cards -- Output modal for viewing agent results -- Feature suggestions dialog -- Board background customization -- Integration with Electron APIs for IPC communication -- Keyboard shortcuts support -- 40+ state variables for managing UI state - -**Refactoring Recommendations:** -Extract into smaller components: -- `AddFeatureDialog.tsx` - Feature creation dialog with image upload -- `EditFeatureDialog.tsx` - Feature editing dialog -- `AgentOutputModal.tsx` - Already exists, verify separation -- `FeatureSuggestionsDialog.tsx` - Already exists, verify separation -- `BoardHeader.tsx` - Header with controls and search -- `BoardSearchBar.tsx` - Search and filter functionality -- `ConcurrencyControl.tsx` - Concurrency slider component -- `BoardActions.tsx` - Action buttons (add feature, auto mode, etc.) -- `DragDropContext.tsx` - Wrap drag-and-drop logic -- Custom hooks: - - `useBoardFeatures.ts` - Feature loading and management - - `useBoardDragDrop.ts` - Drag and drop handlers - - `useBoardActions.ts` - Feature action handlers (run, verify, delete, etc.) - - `useBoardKeyboardShortcuts.ts` - Keyboard shortcut logic - ---- - -## 🟑 HIGH PRIORITY - 2000+ Lines - -### 2. sidebar.tsx - 2,396 lines -**Path:** `apps/app/src/components/layout/sidebar.tsx` -**Type:** React Component (TSX) -**Priority:** HIGH - -**Description:** -Main navigation sidebar with comprehensive project management. - -**Current Responsibilities:** -- Project folder navigation and selection -- View mode switching (Board, Agent, Settings, etc.) -- Project operations (create, delete, rename) -- Theme and appearance controls -- Terminal, Wiki, and other view launchers -- Drag-and-drop project reordering -- Settings and configuration access - -**Refactoring Recommendations:** -Split into focused components: -- `ProjectSelector.tsx` - Project list and selection -- `NavigationTabs.tsx` - View mode tabs -- `ProjectActions.tsx` - Create, delete, rename operations -- `SettingsMenu.tsx` - Settings dropdown -- `ThemeSelector.tsx` - Theme controls -- `ViewLaunchers.tsx` - Terminal, Wiki launchers -- Custom hooks: - - `useProjectManagement.ts` - Project CRUD operations - - `useSidebarState.ts` - Sidebar state management - ---- - -### 3. electron.ts - 2,356 lines -**Path:** `apps/app/src/lib/electron.ts` -**Type:** TypeScript Utility/API Bridge -**Priority:** HIGH - -**Description:** -Electron IPC bridge and type definitions for frontend-backend communication. - -**Current Responsibilities:** -- File system operations (read, write, directory listing) -- Project management APIs -- Feature management APIs -- Terminal/shell execution -- Auto mode and agent execution APIs -- Worktree management -- Provider status APIs -- Event handling and subscriptions - -**Refactoring Recommendations:** -Modularize into domain-specific API modules: -- `api/file-system-api.ts` - File operations -- `api/project-api.ts` - Project CRUD -- `api/feature-api.ts` - Feature management -- `api/execution-api.ts` - Auto mode and agent execution -- `api/provider-api.ts` - Provider status and management -- `api/worktree-api.ts` - Git worktree operations -- `api/terminal-api.ts` - Terminal/shell APIs -- `types/electron-types.ts` - Shared type definitions -- `electron.ts` - Main export aggregator - ---- - -### 4. app-store.ts - 2,174 lines -**Path:** `apps/app/src/store/app-store.ts` -**Type:** TypeScript State Management (Zustand Store) -**Priority:** HIGH - -**Description:** -Centralized application state store using Zustand. - -**Current Responsibilities:** -- Global app state types and interfaces -- Project and feature management state -- Theme and appearance settings -- API keys configuration -- Keyboard shortcuts configuration -- Terminal themes configuration -- Auto mode settings -- All store mutations and selectors - -**Refactoring Recommendations:** -Split into domain-specific stores: -- `stores/projects-store.ts` - Project state and actions -- `stores/features-store.ts` - Feature state and actions -- `stores/ui-store.ts` - UI state (theme, sidebar, modals) -- `stores/settings-store.ts` - User settings and preferences -- `stores/execution-store.ts` - Auto mode and running tasks -- `stores/provider-store.ts` - Provider configuration -- `types/store-types.ts` - Shared type definitions -- `app-store.ts` - Main store aggregator with combined selectors - ---- - -## 🟒 MEDIUM PRIORITY - 1000-2000 Lines - -### 5. auto-mode-service.ts - 1,232 lines -**Path:** `apps/server/src/services/auto-mode-service.ts` -**Type:** TypeScript Service (Backend) -**Priority:** MEDIUM-HIGH - -**Description:** -Core autonomous feature implementation service. - -**Current Responsibilities:** -- Worktree creation and management -- Feature execution with Claude Agent SDK -- Concurrent execution with concurrency limits -- Progress streaming via events -- Verification and merge workflows -- Provider management -- Error handling and classification - -**Refactoring Recommendations:** -Extract into service modules: -- `services/worktree-manager.ts` - Worktree operations -- `services/feature-executor.ts` - Feature execution logic -- `services/concurrency-manager.ts` - Concurrency control -- `services/verification-service.ts` - Verification workflows -- `utils/error-classifier.ts` - Error handling utilities - ---- - -### 6. spec-view.tsx - 1,230 lines -**Path:** `apps/app/src/components/views/spec-view.tsx` -**Type:** React Component (TSX) -**Priority:** MEDIUM - -**Description:** -Specification editor view component for feature specification management. - -**Refactoring Recommendations:** -Extract editor components and hooks: -- `SpecEditor.tsx` - Main editor component -- `SpecToolbar.tsx` - Editor toolbar -- `SpecSidebar.tsx` - Spec navigation sidebar -- `useSpecEditor.ts` - Editor state management - ---- - -### 7. kanban-card.tsx - 1,180 lines -**Path:** `apps/app/src/components/views/kanban-card.tsx` -**Type:** React Component (TSX) -**Priority:** MEDIUM - -**Description:** -Individual Kanban card component with rich feature display and interaction. - -**Refactoring Recommendations:** -Split into smaller card components: -- `KanbanCardHeader.tsx` - Card title and metadata -- `KanbanCardBody.tsx` - Card content -- `KanbanCardActions.tsx` - Action buttons -- `KanbanCardStatus.tsx` - Status indicators -- `useKanbanCard.ts` - Card interaction logic - ---- - -### 8. analysis-view.tsx - 1,134 lines -**Path:** `apps/app/src/components/views/analysis-view.tsx` -**Type:** React Component (TSX) -**Priority:** MEDIUM - -**Description:** -Analysis view component for displaying and managing feature analysis data. - -**Refactoring Recommendations:** -Extract visualization and data components: -- `AnalysisChart.tsx` - Chart/graph components -- `AnalysisTable.tsx` - Data table -- `AnalysisFilters.tsx` - Filter controls -- `useAnalysisData.ts` - Data fetching and processing - ---- - -## Refactoring Strategy - -### Phase 1: Critical (Immediate) -1. **board-view.tsx** - Break into dialogs, header, and custom hooks - - Extract all dialogs first (AddFeature, EditFeature) - - Move to custom hooks for business logic - - Split remaining UI into smaller components - -### Phase 2: High Priority (Next Sprint) -2. **sidebar.tsx** - Componentize navigation and project management -3. **electron.ts** - Modularize into API domains -4. **app-store.ts** - Split into domain stores - -### Phase 3: Medium Priority (Future) -5. **auto-mode-service.ts** - Extract service modules -6. **spec-view.tsx** - Break into editor components -7. **kanban-card.tsx** - Split card into sub-components -8. **analysis-view.tsx** - Extract visualization components - ---- - -## General Refactoring Guidelines - -### When Refactoring Large Components: - -1. **Extract Dialogs/Modals First** - - Move dialog components to separate files - - Keep dialog state management in parent initially - - Later extract to custom hooks if complex - -2. **Create Custom Hooks for Business Logic** - - Move data fetching to `useFetch*` hooks - - Move complex state logic to `use*State` hooks - - Move side effects to `use*Effect` hooks - -3. **Split UI into Presentational Components** - - Header/toolbar components - - Content area components - - Footer/action components - -4. **Move Utils and Helpers** - - Extract pure functions to utility files - - Move constants to separate constant files - - Create type files for shared interfaces - -### When Refactoring Large Files: - -1. **Identify Domains/Concerns** - - Group related functionality - - Find natural boundaries - -2. **Extract Gradually** - - Start with least coupled code - - Work towards core functionality - - Test after each extraction - -3. **Maintain Type Safety** - - Export types from extracted modules - - Use shared type files for common interfaces - - Ensure no type errors after refactoring - ---- - -## Progress Tracking - -- [ ] board-view.tsx (3,325 lines) -- [ ] sidebar.tsx (2,396 lines) -- [ ] electron.ts (2,356 lines) -- [ ] app-store.ts (2,174 lines) -- [ ] auto-mode-service.ts (1,232 lines) -- [ ] spec-view.tsx (1,230 lines) -- [ ] kanban-card.tsx (1,180 lines) -- [ ] analysis-view.tsx (1,134 lines) - -**Target:** All files under 500 lines, most under 300 lines - ---- - -*Generated: 2025-12-15* diff --git a/apps/app/electron/.eslintrc.js b/apps/app/electron/.eslintrc.js deleted file mode 100644 index 5c4bdfee..00000000 --- a/apps/app/electron/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - rules: { - "@typescript-eslint/no-require-imports": "off", - }, -}; diff --git a/apps/app/electron/preload.js b/apps/app/electron/preload.js deleted file mode 100644 index 289d2cd7..00000000 --- a/apps/app/electron/preload.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Simplified Electron preload script - * - * Only exposes native features (dialogs, shell) and server URL. - * All other operations go through HTTP API. - */ - -const { contextBridge, ipcRenderer } = require("electron"); - -// Expose minimal API for native features -contextBridge.exposeInMainWorld("electronAPI", { - // Platform info - platform: process.platform, - isElectron: true, - - // Connection check - ping: () => ipcRenderer.invoke("ping"), - - // Get server URL for HTTP client - getServerUrl: () => ipcRenderer.invoke("server:getUrl"), - - // Native dialogs - better UX than prompt() - openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"), - openFile: (options) => ipcRenderer.invoke("dialog:openFile", options), - saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options), - - // Shell operations - openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url), - openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath), - - // App info - getPath: (name) => ipcRenderer.invoke("app:getPath", name), - getVersion: () => ipcRenderer.invoke("app:getVersion"), - isPackaged: () => ipcRenderer.invoke("app:isPackaged"), -}); - -console.log("[Preload] Electron API exposed (simplified mode)"); diff --git a/apps/app/eslint.config.mjs b/apps/app/eslint.config.mjs deleted file mode 100644 index 6c419a68..00000000 --- a/apps/app/eslint.config.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - // Electron files use CommonJS - "electron/**", - ]), -]); - -export default eslintConfig; diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts deleted file mode 100644 index 65c102b9..00000000 --- a/apps/app/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - output: "export", -}; - -export default nextConfig; diff --git a/apps/app/postcss.config.mjs b/apps/app/postcss.config.mjs deleted file mode 100644 index 61e36849..00000000 --- a/apps/app/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/apps/app/src/app/api/claude/test/route.ts b/apps/app/src/app/api/claude/test/route.ts deleted file mode 100644 index 95dab4ba..00000000 --- a/apps/app/src/app/api/claude/test/route.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -interface AnthropicResponse { - content?: Array<{ type: string; text?: string }>; - model?: string; - error?: { message?: string }; -} - -export async function POST(request: NextRequest) { - try { - const { apiKey } = await request.json(); - - // Use provided API key or fall back to environment variable - const effectiveApiKey = apiKey || process.env.ANTHROPIC_API_KEY; - - if (!effectiveApiKey) { - return NextResponse.json( - { success: false, error: "No API key provided or configured in environment" }, - { status: 400 } - ); - } - - // Send a simple test prompt to the Anthropic API - const response = await fetch("https://api.anthropic.com/v1/messages", { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": effectiveApiKey, - "anthropic-version": "2023-06-01", - }, - body: JSON.stringify({ - model: "claude-sonnet-4-20250514", - max_tokens: 100, - messages: [ - { - role: "user", - content: "Respond with exactly: 'Claude API connection successful!' and nothing else.", - }, - ], - }), - }); - - if (!response.ok) { - const errorData = (await response.json()) as AnthropicResponse; - const errorMessage = errorData.error?.message || `HTTP ${response.status}`; - - if (response.status === 401) { - return NextResponse.json( - { success: false, error: "Invalid API key. Please check your Anthropic API key." }, - { status: 401 } - ); - } - - if (response.status === 429) { - return NextResponse.json( - { success: false, error: "Rate limit exceeded. Please try again later." }, - { status: 429 } - ); - } - - return NextResponse.json( - { success: false, error: `API error: ${errorMessage}` }, - { status: response.status } - ); - } - - const data = (await response.json()) as AnthropicResponse; - - // Check if we got a valid response - if (data.content && data.content.length > 0) { - const textContent = data.content.find((block) => block.type === "text"); - if (textContent && textContent.type === "text" && textContent.text) { - return NextResponse.json({ - success: true, - message: `Connection successful! Response: "${textContent.text}"`, - model: data.model, - }); - } - } - - return NextResponse.json({ - success: true, - message: "Connection successful! Claude responded.", - model: data.model, - }); - } catch (error: unknown) { - console.error("Claude API test error:", error); - - const errorMessage = - error instanceof Error ? error.message : "Failed to connect to Claude API"; - - return NextResponse.json( - { success: false, error: errorMessage }, - { status: 500 } - ); - } -} diff --git a/apps/app/src/app/api/gemini/test/route.ts b/apps/app/src/app/api/gemini/test/route.ts deleted file mode 100644 index a4830c84..00000000 --- a/apps/app/src/app/api/gemini/test/route.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -interface GeminiContent { - parts: Array<{ - text?: string; - inlineData?: { - mimeType: string; - data: string; - }; - }>; - role?: string; -} - -interface GeminiRequest { - contents: GeminiContent[]; - generationConfig?: { - maxOutputTokens?: number; - temperature?: number; - }; -} - -interface GeminiResponse { - candidates?: Array<{ - content: { - parts: Array<{ - text: string; - }>; - role: string; - }; - finishReason: string; - safetyRatings?: Array<{ - category: string; - probability: string; - }>; - }>; - promptFeedback?: { - safetyRatings?: Array<{ - category: string; - probability: string; - }>; - }; - error?: { - code: number; - message: string; - status: string; - }; -} - -export async function POST(request: NextRequest) { - try { - const { apiKey, imageData, mimeType, prompt } = await request.json(); - - // Use provided API key or fall back to environment variable - const effectiveApiKey = apiKey || process.env.GOOGLE_API_KEY; - - if (!effectiveApiKey) { - return NextResponse.json( - { success: false, error: "No API key provided or configured in environment" }, - { status: 400 } - ); - } - - // Build the request body - const requestBody: GeminiRequest = { - contents: [ - { - parts: [], - }, - ], - generationConfig: { - maxOutputTokens: 150, - temperature: 0.4, - }, - }; - - // Add image if provided - if (imageData && mimeType) { - requestBody.contents[0].parts.push({ - inlineData: { - mimeType: mimeType, - data: imageData, - }, - }); - } - - // Add text prompt - const textPrompt = prompt || (imageData - ? "Describe what you see in this image briefly." - : "Respond with exactly: 'Gemini SDK connection successful!' and nothing else."); - - requestBody.contents[0].parts.push({ - text: textPrompt, - }); - - // Call Gemini API - using gemini-1.5-flash as it supports both text and vision - const model = imageData ? "gemini-1.5-flash" : "gemini-1.5-flash"; - const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${effectiveApiKey}`; - - const response = await fetch(geminiUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(requestBody), - }); - - const data: GeminiResponse = await response.json(); - - // Check for API errors - if (data.error) { - const errorMessage = data.error.message || "Unknown Gemini API error"; - const statusCode = data.error.code || 500; - - if (statusCode === 400 && errorMessage.includes("API key")) { - return NextResponse.json( - { success: false, error: "Invalid API key. Please check your Google API key." }, - { status: 401 } - ); - } - - if (statusCode === 429) { - return NextResponse.json( - { success: false, error: "Rate limit exceeded. Please try again later." }, - { status: 429 } - ); - } - - return NextResponse.json( - { success: false, error: `API error: ${errorMessage}` }, - { status: statusCode } - ); - } - - // Check for valid response - if (!response.ok) { - return NextResponse.json( - { success: false, error: `HTTP error: ${response.status} ${response.statusText}` }, - { status: response.status } - ); - } - - // Extract response text - if (data.candidates && data.candidates.length > 0 && data.candidates[0].content?.parts?.length > 0) { - const responseText = data.candidates[0].content.parts - .filter((part) => part.text) - .map((part) => part.text) - .join(""); - - return NextResponse.json({ - success: true, - message: `Connection successful! Response: "${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}"`, - model: model, - hasImage: !!imageData, - }); - } - - // Handle blocked responses - if (data.promptFeedback?.safetyRatings) { - return NextResponse.json({ - success: true, - message: "Connection successful! Gemini responded (response may have been filtered).", - model: model, - hasImage: !!imageData, - }); - } - - return NextResponse.json({ - success: true, - message: "Connection successful! Gemini responded.", - model: model, - hasImage: !!imageData, - }); - } catch (error: unknown) { - console.error("Gemini API test error:", error); - - if (error instanceof TypeError && error.message.includes("fetch")) { - return NextResponse.json( - { success: false, error: "Network error. Unable to reach Gemini API." }, - { status: 503 } - ); - } - - const errorMessage = - error instanceof Error ? error.message : "Failed to connect to Gemini API"; - - return NextResponse.json( - { success: false, error: errorMessage }, - { status: 500 } - ); - } -} diff --git a/apps/app/src/app/favicon.ico b/apps/app/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/apps/app/src/app/favicon.ico and /dev/null differ diff --git a/apps/app/src/app/layout.tsx b/apps/app/src/app/layout.tsx deleted file mode 100644 index 2d7df503..00000000 --- a/apps/app/src/app/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Metadata } from "next"; -import { GeistSans } from "geist/font/sans"; -import { GeistMono } from "geist/font/mono"; -import { Toaster } from "sonner"; -import "./globals.css"; -export const metadata: Metadata = { - title: "Automaker - Autonomous AI Development Studio", - description: "Build software autonomously with intelligent orchestration", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - - ); -} diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx deleted file mode 100644 index 0ac464f2..00000000 --- a/apps/app/src/app/page.tsx +++ /dev/null @@ -1,234 +0,0 @@ -"use client"; - -import { useEffect, useState, useCallback } from "react"; -import { Sidebar } from "@/components/layout/sidebar"; -import { WelcomeView } from "@/components/views/welcome-view"; -import { BoardView } from "@/components/views/board-view"; -import { SpecView } from "@/components/views/spec-view"; -import { AgentView } from "@/components/views/agent-view"; -import { SettingsView } from "@/components/views/settings-view"; -import { InterviewView } from "@/components/views/interview-view"; -import { ContextView } from "@/components/views/context-view"; -import { ProfilesView } from "@/components/views/profiles-view"; -import { SetupView } from "@/components/views/setup-view"; -import { RunningAgentsView } from "@/components/views/running-agents-view"; -import { TerminalView } from "@/components/views/terminal-view"; -import { WikiView } from "@/components/views/wiki-view"; -import { useAppStore } from "@/store/app-store"; -import { useSetupStore } from "@/store/setup-store"; -import { getElectronAPI, isElectron } from "@/lib/electron"; -import { - FileBrowserProvider, - useFileBrowser, - setGlobalFileBrowser, -} from "@/contexts/file-browser-context"; - -function HomeContent() { - const { - currentView, - setCurrentView, - setIpcConnected, - theme, - currentProject, - previewTheme, - getEffectiveTheme, - } = useAppStore(); - const { isFirstRun, setupComplete } = useSetupStore(); - const [isMounted, setIsMounted] = useState(false); - const [streamerPanelOpen, setStreamerPanelOpen] = useState(false); - const { openFileBrowser } = useFileBrowser(); - - // Hidden streamer panel - opens with "\" key - const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => { - // Don't trigger when typing in inputs - const activeElement = document.activeElement; - if (activeElement) { - const tagName = activeElement.tagName.toLowerCase(); - if ( - tagName === "input" || - tagName === "textarea" || - tagName === "select" - ) { - return; - } - if (activeElement.getAttribute("contenteditable") === "true") { - return; - } - const role = activeElement.getAttribute("role"); - if (role === "textbox" || role === "searchbox" || role === "combobox") { - return; - } - } - - // Don't trigger with modifier keys - if (event.ctrlKey || event.altKey || event.metaKey) { - return; - } - - // Check for "\" key (backslash) - if (event.key === "\\") { - event.preventDefault(); - setStreamerPanelOpen((prev) => !prev); - } - }, []); - - // Register the "\" shortcut for streamer panel - useEffect(() => { - window.addEventListener("keydown", handleStreamerPanelShortcut); - return () => { - window.removeEventListener("keydown", handleStreamerPanelShortcut); - }; - }, [handleStreamerPanelShortcut]); - - // Compute the effective theme: previewTheme takes priority, then project theme, then global theme - // This is reactive because it depends on previewTheme, currentProject, and theme from the store - const effectiveTheme = getEffectiveTheme(); - - // Prevent hydration issues - useEffect(() => { - setIsMounted(true); - }, []); - - // Initialize global file browser for HttpApiClient - useEffect(() => { - setGlobalFileBrowser(openFileBrowser); - }, [openFileBrowser]); - - // Check if this is first run and redirect to setup if needed - useEffect(() => { - console.log("[Setup Flow] Checking setup state:", { - isMounted, - isFirstRun, - setupComplete, - currentView, - shouldShowSetup: isMounted && isFirstRun && !setupComplete, - }); - - if (isMounted && isFirstRun && !setupComplete) { - console.log( - "[Setup Flow] Redirecting to setup wizard (first run, not complete)" - ); - setCurrentView("setup"); - } else if (isMounted && setupComplete) { - console.log("[Setup Flow] Setup already complete, showing normal view"); - } - }, [isMounted, isFirstRun, setupComplete, setCurrentView, currentView]); - - // Test IPC connection on mount - useEffect(() => { - const testConnection = async () => { - try { - const api = getElectronAPI(); - const result = await api.ping(); - setIpcConnected(result === "pong"); - } catch (error) { - console.error("IPC connection failed:", error); - setIpcConnected(false); - } - }; - - testConnection(); - }, [setIpcConnected]); - - // Apply theme class to document (uses effective theme - preview, project-specific, or global) - useEffect(() => { - const root = document.documentElement; - const themeClasses = [ - "dark", - "light", - "retro", - "dracula", - "nord", - "monokai", - "tokyonight", - "solarized", - "gruvbox", - "catppuccin", - "onedark", - "synthwave", - "red", - "cream", - "sunset", - "gray", - ]; - - // Remove all theme classes - root.classList.remove(...themeClasses); - - // Apply the effective theme - if (themeClasses.includes(effectiveTheme)) { - root.classList.add(effectiveTheme); - } else if (effectiveTheme === "system") { - // System theme - detect OS preference - const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - root.classList.add(isDark ? "dark" : "light"); - } - }, [effectiveTheme, previewTheme, currentProject, theme]); - - const renderView = () => { - switch (currentView) { - case "welcome": - return ; - case "setup": - return ; - case "board": - return ; - case "spec": - return ; - case "agent": - return ; - case "settings": - return ; - case "interview": - return ; - case "context": - return ; - case "profiles": - return ; - case "running-agents": - return ; - case "terminal": - return ; - case "wiki": - return ; - default: - return ; - } - }; - - // Setup view is full-screen without sidebar - if (currentView === "setup") { - return ( -
- -
- ); - } - - return ( -
- -
- {renderView()} -
- - {/* Hidden streamer panel - opens with "\" key, pushes content */} -
-
- ); -} - -export default function Home() { - return ( - - - - ); -} diff --git a/apps/app/src/components/ui/course-promo-badge.tsx b/apps/app/src/components/ui/course-promo-badge.tsx deleted file mode 100644 index cf9fbdbb..00000000 --- a/apps/app/src/components/ui/course-promo-badge.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Sparkles, X } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; - -interface CoursePromoBadgeProps { - sidebarOpen?: boolean; -} - -export function CoursePromoBadge({ sidebarOpen = true }: CoursePromoBadgeProps) { - const [dismissed, setDismissed] = React.useState(false); - - if (dismissed) { - return null; - } - - // Collapsed state - show only icon with tooltip - if (!sidebarOpen) { - return ( -
- - - - - - - - - Become a 10x Dev - { - e.preventDefault(); - e.stopPropagation(); - setDismissed(true); - }} - className="p-0.5 rounded-full hover:bg-primary/30 transition-colors cursor-pointer" - aria-label="Dismiss" - > - - - - - -
- ); - } - - // Expanded state - show full badge - return ( - - ); -} diff --git a/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx deleted file mode 100644 index 4f130db7..00000000 --- a/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { RefreshCw, Globe, Loader2 } from "lucide-react"; -import { cn } from "@/lib/utils"; -import type { WorktreeInfo, BranchInfo, DevServerInfo } from "../types"; -import { BranchSwitchDropdown } from "./branch-switch-dropdown"; -import { WorktreeActionsDropdown } from "./worktree-actions-dropdown"; - -interface WorktreeTabProps { - worktree: WorktreeInfo; - cardCount?: number; // Number of unarchived cards for this branch - isSelected: boolean; - isRunning: boolean; - isActivating: boolean; - isDevServerRunning: boolean; - devServerInfo?: DevServerInfo; - defaultEditorName: string; - branches: BranchInfo[]; - filteredBranches: BranchInfo[]; - branchFilter: string; - isLoadingBranches: boolean; - isSwitching: boolean; - isPulling: boolean; - isPushing: boolean; - isStartingDevServer: boolean; - aheadCount: number; - behindCount: number; - onSelectWorktree: (worktree: WorktreeInfo) => void; - onBranchDropdownOpenChange: (open: boolean) => void; - onActionsDropdownOpenChange: (open: boolean) => void; - onBranchFilterChange: (value: string) => void; - onSwitchBranch: (worktree: WorktreeInfo, branchName: string) => void; - onCreateBranch: (worktree: WorktreeInfo) => void; - onPull: (worktree: WorktreeInfo) => void; - onPush: (worktree: WorktreeInfo) => void; - onOpenInEditor: (worktree: WorktreeInfo) => void; - onCommit: (worktree: WorktreeInfo) => void; - onCreatePR: (worktree: WorktreeInfo) => void; - onDeleteWorktree: (worktree: WorktreeInfo) => void; - onStartDevServer: (worktree: WorktreeInfo) => void; - onStopDevServer: (worktree: WorktreeInfo) => void; - onOpenDevServerUrl: (worktree: WorktreeInfo) => void; -} - -export function WorktreeTab({ - worktree, - cardCount, - isSelected, - isRunning, - isActivating, - isDevServerRunning, - devServerInfo, - defaultEditorName, - branches, - filteredBranches, - branchFilter, - isLoadingBranches, - isSwitching, - isPulling, - isPushing, - isStartingDevServer, - aheadCount, - behindCount, - onSelectWorktree, - onBranchDropdownOpenChange, - onActionsDropdownOpenChange, - onBranchFilterChange, - onSwitchBranch, - onCreateBranch, - onPull, - onPush, - onOpenInEditor, - onCommit, - onCreatePR, - onDeleteWorktree, - onStartDevServer, - onStopDevServer, - onOpenDevServerUrl, -}: WorktreeTabProps) { - return ( -
- {worktree.isMain ? ( - <> - - - - ) : ( - - )} - - {isDevServerRunning && ( - - )} - - -
- ); -} diff --git a/apps/app/src/components/views/spec-view.tsx b/apps/app/src/components/views/spec-view.tsx deleted file mode 100644 index 935d2c72..00000000 --- a/apps/app/src/components/views/spec-view.tsx +++ /dev/null @@ -1,1230 +0,0 @@ -"use client"; - -import { useEffect, useState, useCallback, useRef } 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, - AlertCircle, - CheckCircle2, -} from "lucide-react"; -import { toast } from "sonner"; -import { Checkbox } from "@/components/ui/checkbox"; -import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor"; -import type { SpecRegenerationEvent } from "@/types/electron"; - -// Delay before reloading spec file to ensure it's written to disk -const SPEC_FILE_WRITE_DELAY = 500; - -// Interval for polling backend status during generation -const STATUS_CHECK_INTERVAL_MS = 2000; - -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); - const [generateFeaturesOnRegenerate, setGenerateFeaturesOnRegenerate] = - useState(true); - const [analyzeProjectOnRegenerate, setAnalyzeProjectOnRegenerate] = - useState(true); - - // Create spec state - const [showCreateDialog, setShowCreateDialog] = useState(false); - const [projectOverview, setProjectOverview] = useState(""); - const [isCreating, setIsCreating] = useState(false); - const [generateFeatures, setGenerateFeatures] = useState(true); - const [analyzeProjectOnCreate, setAnalyzeProjectOnCreate] = useState(true); - - // Generate features only state - const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false); - - // Logs state (kept for internal tracking, but UI removed) - const [logs, setLogs] = useState(""); - const logsRef = useRef(""); - - // Phase tracking and status - const [currentPhase, setCurrentPhase] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); - const statusCheckRef = useRef(false); - const stateRestoredRef = useRef(false); - const pendingStatusTimeoutRef = useRef(null); - - // 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]); - - // Reset all spec regeneration state when project changes - useEffect(() => { - // Clear all state when switching projects - setIsCreating(false); - setIsRegenerating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - setErrorMessage(""); - setLogs(""); - logsRef.current = ""; - stateRestoredRef.current = false; - statusCheckRef.current = false; - - // Clear any pending timeout - if (pendingStatusTimeoutRef.current) { - clearTimeout(pendingStatusTimeoutRef.current); - pendingStatusTimeoutRef.current = null; - } - }, [currentProject?.path]); - - // Check if spec regeneration is running when component mounts or project changes - useEffect(() => { - const checkStatus = async () => { - if (!currentProject || statusCheckRef.current) return; - statusCheckRef.current = true; - - try { - const api = getElectronAPI(); - if (!api.specRegeneration) { - statusCheckRef.current = false; - return; - } - - const status = await api.specRegeneration.status(); - console.log( - "[SpecView] Status check on mount:", - status, - "for project:", - currentProject.path - ); - - if (status.success && status.isRunning) { - // Something is running globally, but we can't verify it's for this project - // since the backend doesn't track projectPath in status - // Tentatively show loader - events will confirm if it's for this project - console.log( - "[SpecView] Spec generation is running globally. Tentatively showing loader, waiting for events to confirm project match." - ); - - // Tentatively set state - events will confirm or clear it - setIsCreating(true); - setIsRegenerating(true); - if (status.currentPhase) { - setCurrentPhase(status.currentPhase); - } else { - setCurrentPhase("initialization"); - } - - // Set a timeout to clear state if no events arrive for this project within 3 seconds - if (pendingStatusTimeoutRef.current) { - clearTimeout(pendingStatusTimeoutRef.current); - } - pendingStatusTimeoutRef.current = setTimeout(() => { - // If no events confirmed this is for current project, clear state - console.log( - "[SpecView] No events received for current project - clearing tentative state" - ); - setIsCreating(false); - setIsRegenerating(false); - setCurrentPhase(""); - pendingStatusTimeoutRef.current = null; - }, 3000); - } else if (status.success && !status.isRunning) { - // Not running - clear all state - setIsCreating(false); - setIsRegenerating(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - } - } catch (error) { - console.error("[SpecView] Failed to check status:", error); - } finally { - statusCheckRef.current = false; - } - }; - - // Reset restoration flag when project changes - stateRestoredRef.current = false; - checkStatus(); - }, [currentProject]); - - // Sync state when tab becomes visible (user returns to spec editor) - useEffect(() => { - const handleVisibilityChange = async () => { - if ( - !document.hidden && - currentProject && - (isCreating || isRegenerating || isGeneratingFeatures) - ) { - // Tab became visible and we think we're still generating - verify status from backend - try { - const api = getElectronAPI(); - if (!api.specRegeneration) return; - - const status = await api.specRegeneration.status(); - console.log("[SpecView] Visibility change - status check:", status); - - if (!status.isRunning) { - // Backend says not running - clear state - console.log( - "[SpecView] Visibility change: Backend indicates generation complete - clearing state" - ); - setIsCreating(false); - setIsRegenerating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - loadSpec(); - } else if (status.currentPhase) { - // Still running - update phase from backend - setCurrentPhase(status.currentPhase); - } - } catch (error) { - console.error( - "[SpecView] Failed to check status on visibility change:", - error - ); - } - } - }; - - document.addEventListener("visibilitychange", handleVisibilityChange); - return () => { - document.removeEventListener("visibilitychange", handleVisibilityChange); - }; - }, [ - currentProject, - isCreating, - isRegenerating, - isGeneratingFeatures, - loadSpec, - ]); - - // Periodic status check to ensure state stays in sync (only when we think we're running) - useEffect(() => { - if ( - !currentProject || - (!isCreating && !isRegenerating && !isGeneratingFeatures) - ) - return; - - const intervalId = setInterval(async () => { - try { - const api = getElectronAPI(); - if (!api.specRegeneration) return; - - const status = await api.specRegeneration.status(); - - if (!status.isRunning) { - // Backend says not running - clear state - console.log( - "[SpecView] Periodic check: Backend indicates generation complete - clearing state" - ); - setIsCreating(false); - setIsRegenerating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - loadSpec(); - } else if ( - status.currentPhase && - status.currentPhase !== currentPhase - ) { - // Still running but phase changed - update from backend - console.log("[SpecView] Periodic check: Phase updated from backend", { - old: currentPhase, - new: status.currentPhase, - }); - setCurrentPhase(status.currentPhase); - } - } catch (error) { - console.error("[SpecView] Periodic status check error:", error); - } - }, STATUS_CHECK_INTERVAL_MS); - - return () => { - clearInterval(intervalId); - }; - }, [ - currentProject, - isCreating, - isRegenerating, - isGeneratingFeatures, - currentPhase, - loadSpec, - ]); - - // Subscribe to spec regeneration events - useEffect(() => { - if (!currentProject) return; - - const api = getElectronAPI(); - if (!api.specRegeneration) return; - - const unsubscribe = api.specRegeneration.onEvent( - (event: SpecRegenerationEvent) => { - console.log( - "[SpecView] Regeneration event:", - event.type, - "for project:", - event.projectPath, - "current project:", - currentProject?.path - ); - - // Only handle events for the current project - if (event.projectPath !== currentProject?.path) { - console.log("[SpecView] Ignoring event - not for current project"); - return; - } - - // Clear any pending timeout since we received an event for this project - if (pendingStatusTimeoutRef.current) { - clearTimeout(pendingStatusTimeoutRef.current); - pendingStatusTimeoutRef.current = null; - console.log( - "[SpecView] Event confirmed this is for current project - clearing timeout" - ); - } - - if (event.type === "spec_regeneration_progress") { - // Ensure state is set when we receive events for this project - setIsCreating(true); - setIsRegenerating(true); - - // Extract phase from content if present - const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/); - if (phaseMatch) { - const phase = phaseMatch[1]; - setCurrentPhase(phase); - console.log(`[SpecView] Phase updated: ${phase}`); - - // If phase is "complete", clear running state immediately - if (phase === "complete") { - console.log("[SpecView] Phase is complete - clearing state"); - setIsCreating(false); - setIsRegenerating(false); - stateRestoredRef.current = false; - // Small delay to ensure spec file is written - setTimeout(() => { - loadSpec(); - }, SPEC_FILE_WRITE_DELAY); - } - } - - // Check for completion indicators in content - if ( - event.content.includes("All tasks completed") || - event.content.includes("βœ“ All tasks completed") - ) { - // This indicates everything is done - clear state immediately - console.log( - "[SpecView] Detected completion in progress message - clearing state" - ); - setIsCreating(false); - setIsRegenerating(false); - setCurrentPhase(""); - stateRestoredRef.current = false; - setTimeout(() => { - loadSpec(); - }, SPEC_FILE_WRITE_DELAY); - } - - // Append progress to logs - const newLog = logsRef.current + event.content; - logsRef.current = newLog; - setLogs(newLog); - console.log("[SpecView] Progress:", event.content.substring(0, 100)); - - // Clear error message when we get new progress - if (errorMessage) { - setErrorMessage(""); - } - } else if (event.type === "spec_regeneration_tool") { - // Check if this is a feature creation tool - const isFeatureTool = - event.tool === "mcp__automaker-tools__UpdateFeatureStatus" || - event.tool === "UpdateFeatureStatus" || - event.tool?.includes("Feature"); - - if (isFeatureTool) { - // Ensure we're in feature generation phase - if (currentPhase !== "feature_generation") { - setCurrentPhase("feature_generation"); - setIsCreating(true); - setIsRegenerating(true); - console.log( - "[SpecView] Detected feature creation tool - setting phase to feature_generation" - ); - } - } - - // Log tool usage with details - const toolInput = event.input - ? ` (${JSON.stringify(event.input).substring(0, 100)}...)` - : ""; - const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`; - const newLog = logsRef.current + toolLog; - logsRef.current = newLog; - setLogs(newLog); - console.log("[SpecView] Tool:", event.tool, event.input); - } else if (event.type === "spec_regeneration_complete") { - // Add completion message to logs first - const completionLog = - logsRef.current + `\n[Complete] ${event.message}\n`; - logsRef.current = completionLog; - setLogs(completionLog); - - // --- Completion Detection Logic --- - // The backend sends explicit signals for completion: - // 1. "All tasks completed" in the message - // 2. [Phase: complete] marker in logs - // 3. "Spec regeneration complete!" for regeneration - // 4. "Initial spec creation complete!" for creation without features - const isFinalCompletionMessage = - event.message?.includes("All tasks completed") || - event.message === "All tasks completed!" || - event.message === "All tasks completed" || - event.message === "Spec regeneration complete!" || - event.message === "Initial spec creation complete!"; - - const hasCompletePhase = - logsRef.current.includes("[Phase: complete]"); - - // Intermediate completion means features are being generated after spec creation - const isIntermediateCompletion = - event.message?.includes("Features are being generated") || - event.message?.includes("features are being generated"); - - // Rely solely on explicit backend signals - const shouldComplete = - (isFinalCompletionMessage || hasCompletePhase) && - !isIntermediateCompletion; - - if (shouldComplete) { - // Fully complete - clear all states immediately - console.log( - "[SpecView] Final completion detected - clearing state", - { - isFinalCompletionMessage, - hasCompletePhase, - message: event.message, - } - ); - setIsRegenerating(false); - setIsCreating(false); - setIsGeneratingFeatures(false); - setCurrentPhase(""); - setShowRegenerateDialog(false); - setShowCreateDialog(false); - setProjectDefinition(""); - setProjectOverview(""); - setErrorMessage(""); - stateRestoredRef.current = false; - - // Reload the spec with delay to ensure file is written to disk - setTimeout(() => { - loadSpec(); - }, SPEC_FILE_WRITE_DELAY); - - // Show success toast notification - const isRegeneration = event.message?.includes("regeneration"); - const isFeatureGeneration = - event.message?.includes("Feature generation"); - toast.success( - isFeatureGeneration - ? "Feature Generation Complete" - : isRegeneration - ? "Spec Regeneration Complete" - : "Spec Creation Complete", - { - description: isFeatureGeneration - ? "Features have been created from the app specification." - : "Your app specification has been saved.", - icon: , - } - ); - } else if (isIntermediateCompletion) { - // Intermediate completion - keep state active for feature generation - setIsCreating(true); - setIsRegenerating(true); - setCurrentPhase("feature_generation"); - console.log( - "[SpecView] Intermediate completion, continuing with feature generation" - ); - } - - console.log("[SpecView] Spec generation event:", event.message); - } else if (event.type === "spec_regeneration_error") { - setIsRegenerating(false); - setIsCreating(false); - setIsGeneratingFeatures(false); - setCurrentPhase("error"); - setErrorMessage(event.error); - stateRestoredRef.current = false; // Reset restoration flag - // Add error to logs - const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - console.error("[SpecView] Regeneration error:", event.error); - } - } - ); - - return () => { - unsubscribe(); - }; - }, [currentProject?.path, loadSpec, errorMessage, currentPhase]); - - // 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); - setShowRegenerateDialog(false); - setCurrentPhase("initialization"); - setErrorMessage(""); - // Reset logs when starting new regeneration - logsRef.current = ""; - setLogs(""); - console.log( - "[SpecView] Starting spec regeneration, generateFeatures:", - generateFeaturesOnRegenerate - ); - 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(), - generateFeaturesOnRegenerate, - analyzeProjectOnRegenerate - ); - - if (!result.success) { - const errorMsg = result.error || "Unknown error"; - console.error("[SpecView] Failed to start regeneration:", errorMsg); - setIsRegenerating(false); - setCurrentPhase("error"); - setErrorMessage(errorMsg); - const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - } - // If successful, we'll wait for the events to update the state - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error("[SpecView] Failed to regenerate spec:", errorMsg); - setIsRegenerating(false); - setCurrentPhase("error"); - setErrorMessage(errorMsg); - const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - } - }; - - const handleCreateSpec = async () => { - if (!currentProject || !projectOverview.trim()) return; - - setIsCreating(true); - setShowCreateDialog(false); - setCurrentPhase("initialization"); - setErrorMessage(""); - // Reset logs when starting new generation - logsRef.current = ""; - setLogs(""); - console.log( - "[SpecView] Starting spec creation, generateFeatures:", - generateFeatures - ); - 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, - analyzeProjectOnCreate - ); - - if (!result.success) { - const errorMsg = result.error || "Unknown error"; - console.error("[SpecView] Failed to start spec creation:", errorMsg); - setIsCreating(false); - setCurrentPhase("error"); - setErrorMessage(errorMsg); - const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - } - // If successful, we'll wait for the events to update the state - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error("[SpecView] Failed to create spec:", errorMsg); - setIsCreating(false); - setCurrentPhase("error"); - setErrorMessage(errorMsg); - const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - } - }; - - const handleGenerateFeatures = async () => { - if (!currentProject) return; - - setIsGeneratingFeatures(true); - setShowRegenerateDialog(false); - setCurrentPhase("initialization"); - setErrorMessage(""); - // Reset logs when starting feature generation - logsRef.current = ""; - setLogs(""); - console.log("[SpecView] Starting feature generation from existing spec"); - try { - const api = getElectronAPI(); - if (!api.specRegeneration) { - console.error("[SpecView] Spec regeneration not available"); - setIsGeneratingFeatures(false); - return; - } - const result = await api.specRegeneration.generateFeatures( - currentProject.path - ); - - if (!result.success) { - const errorMsg = result.error || "Unknown error"; - console.error( - "[SpecView] Failed to start feature generation:", - errorMsg - ); - setIsGeneratingFeatures(false); - setCurrentPhase("error"); - setErrorMessage(errorMsg); - const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - } - // If successful, we'll wait for the events to update the state - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error("[SpecView] Failed to generate features:", errorMsg); - setIsGeneratingFeatures(false); - setCurrentPhase("error"); - setErrorMessage(errorMsg); - const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`; - logsRef.current = errorLog; - setLogs(errorLog); - } - }; - - if (!currentProject) { - return ( -
-

No project selected

-
- ); - } - - if (isLoading) { - return ( -
- -
- ); - } - - // Show empty state when no spec exists (isCreating is handled by bottom-right indicator in sidebar) - if (!specExists) { - return ( -
- {/* Header */} -
-
- -
-

App Specification

-

- {currentProject.path}/.automaker/app_spec.txt -

-
-
- {(isCreating || isRegenerating) && ( -
-
- -
-
-
- - {isCreating - ? "Generating Specification" - : "Regenerating Specification"} - - {currentPhase && ( - - {currentPhase === "initialization" && "Initializing..."} - {currentPhase === "setup" && "Setting up tools..."} - {currentPhase === "analysis" && - "Analyzing project structure..."} - {currentPhase === "spec_complete" && - "Spec created! Generating features..."} - {currentPhase === "feature_generation" && - "Creating features from roadmap..."} - {currentPhase === "complete" && "Complete!"} - {currentPhase === "error" && "Error occurred"} - {![ - "initialization", - "setup", - "analysis", - "spec_complete", - "feature_generation", - "complete", - "error", - ].includes(currentPhase) && currentPhase} - - )} -
-
- )} - {errorMessage && ( -
- Error: {errorMessage} -
- )} -
- - {/* Empty State */} -
-
-
-
- {isCreating ? ( - - ) : ( - - )} -
-
-

- {isCreating ? ( - <> -
- Generating App Specification -
- {currentPhase && ( -
- - {currentPhase === "initialization" && "Initializing..."} - {currentPhase === "setup" && "Setting up tools..."} - {currentPhase === "analysis" && - "Analyzing project structure..."} - {currentPhase === "spec_complete" && - "Spec created! Generating features..."} - {currentPhase === "feature_generation" && - "Creating features from roadmap..."} - {currentPhase === "complete" && "Complete!"} - {currentPhase === "error" && "Error occurred"} - {![ - "initialization", - "setup", - "analysis", - "spec_complete", - "feature_generation", - "complete", - "error", - ].includes(currentPhase) && currentPhase} - -
- )} - - ) : ( - "No App Specification Found" - )} -

-

- {isCreating - ? currentPhase === "feature_generation" - ? "The app specification has been created! Now generating features from the implementation roadmap..." - : "We're analyzing your project and generating a comprehensive specification. This may take a few moments..." - : "Create an app specification to help our system understand your project. We'll analyze your codebase and generate a comprehensive spec based on your description."} -

- {errorMessage && ( -
-

Error:

-

{errorMessage}

-
- )} - {!isCreating && ( -
- -
- )} -
-
- - {/* Create Dialog */} - { - if (!open && !isCreating) { - setShowCreateDialog(false); - } - }} - > - - - Create App Specification - - We didn't find an app_spec.txt file. Let us help you - generate your app_spec.txt to help describe your project for our - system. We'll analyze your project's tech stack and - create a comprehensive specification. - - - -
-
- -

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

-