From 9954feafd8b0e8864802cde08df99fd1f63e1d92 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 17 Dec 2025 18:32:04 +0100 Subject: [PATCH 01/52] chore: add migraiton plan and claude md file --- CLAUDE.md | 98 ++ docs/migration-plan-nextjs-to-vite.md | 1750 +++++++++++++++++++++++++ 2 files changed, 1848 insertions(+) create mode 100644 CLAUDE.md create mode 100644 docs/migration-plan-nextjs-to-vite.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..f7265a84 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Automaker is an autonomous AI development studio that orchestrates AI agents to build software. Users describe features on a Kanban board, and AI agents powered by Claude Code automatically implement them. + +## Architecture + +This is a monorepo with npm workspaces containing two main applications: + +### Apps Structure +- **`apps/app`** - Next.js 16 + Electron desktop application (frontend) + - React 19 with Zustand for state management + - Radix UI components with Tailwind CSS 4 + - dnd-kit for drag-and-drop Kanban board + - xterm.js for integrated terminal + - Playwright for E2E testing + +- **`apps/server`** - Express.js backend server (TypeScript, ES modules) + - Uses `@anthropic-ai/claude-agent-sdk` for AI agent orchestration + - WebSocket support for real-time events and terminal communication + - node-pty for terminal sessions + - Vitest for testing + +### Communication +- Frontend communicates with backend via HTTP API (port 3008) and WebSocket +- In Electron mode, the backend server is spawned as a child process +- In web mode, both run independently + +### Key Server Services (`apps/server/src/services/`) +- `AgentService` - Manages Claude agent sessions and conversations +- `AutoModeService` - Orchestrates concurrent feature implementation +- `FeatureLoader` - Persists feature/task data to `.automaker/` directory +- `TerminalService` - Manages PTY terminal sessions + +### Key Frontend State (`apps/app/src/store/`) +- `app-store.ts` - Main Zustand store with persisted state (projects, features, settings, themes) +- Features flow: backlog → in_progress → waiting_approval → verified → completed + +## Development Commands + +```bash +# Install dependencies +npm install + +# Development (prompts for mode selection) +npm run dev + +# Specific development modes +npm run dev:electron # Desktop app with Electron +npm run dev:web # Web browser mode (frontend + backend) +npm run dev:server # Backend server only + +# Testing +npm run test # E2E tests (Playwright, headless) +npm run test:headed # E2E tests with browser visible +npm run test:server # Server unit tests (Vitest) +npm run test:server:coverage # Server tests with coverage + +# Linting +npm run lint # ESLint on frontend + +# Building +npm run build # Build Next.js app +npm run build:electron # Build Electron distributable +``` + +### Running a Single Test +```bash +# Frontend E2E (from apps/app) +npx playwright test tests/specific-test.spec.ts + +# Server unit tests (from apps/server) +npx vitest run tests/unit/specific.test.ts +npx vitest watch tests/unit/specific.test.ts # Watch mode +``` + +## Code Conventions + +- Never use `any` for type declarations - create proper interfaces or use existing types +- Server uses ES modules (`"type": "module"`) - use `.js` extensions in imports +- Frontend components in `src/components/`, organized by feature in `views/` +- UI primitives in `src/components/ui/` following shadcn/ui patterns + +## Environment Variables + +- `ANTHROPIC_API_KEY` - Required for Claude agent functionality +- `PORT` - Backend server port (default: 3008) +- `AUTOMAKER_MOCK_AGENT=true` - Enable mock agent for testing without API calls + +## Project Data Storage + +Features and project state are stored in `.automaker/` directory within each project: +- `features.json` - Kanban features/tasks +- `context/` - Context files for AI agents +- Background images and other project-specific data diff --git a/docs/migration-plan-nextjs-to-vite.md b/docs/migration-plan-nextjs-to-vite.md new file mode 100644 index 00000000..ab617bbe --- /dev/null +++ b/docs/migration-plan-nextjs-to-vite.md @@ -0,0 +1,1750 @@ +# Migration Plan: Next.js to Vite + Electron + TanStack + +> **Document Version**: 1.0 +> **Date**: December 2025 +> **Status**: Planning Phase +> **Branch**: feature/worktrees (awaiting merge before implementation) + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Current Architecture Assessment](#current-architecture-assessment) +3. [Proposed New Architecture](#proposed-new-architecture) +4. [Folder Structure](#folder-structure) +5. [Shared Packages (libs/)](#shared-packages-libs) +6. [Type-Safe Electron Implementation](#type-safe-electron-implementation) +7. [Components Refactoring](#components-refactoring) +8. [Web + Electron Dual Support](#web--electron-dual-support) +9. [Migration Phases](#migration-phases) +10. [Expected Benefits](#expected-benefits) +11. [Risk Mitigation](#risk-mitigation) + +--- + +## Executive Summary + +### Why Migrate? + +Our current Next.js implementation uses **less than 5%** of the framework's capabilities. We're essentially running a static SPA with unnecessary overhead: + +| Next.js Feature | Our Usage | +|-----------------|-----------| +| Server-Side Rendering | ❌ Not used | +| Static Site Generation | ❌ Not used | +| API Routes | ⚠️ Only 2 test endpoints | +| Image Optimization | ❌ Not used | +| Dynamic Routing | ❌ Not used | +| App Router | ⚠️ File structure only | +| Metadata API | ⚠️ Title/description only | +| Static Export | ✅ Used (`output: "export"`) | + +### Migration Benefits + +| Metric | Current (Next.js) | Expected (Vite) | +|--------|-------------------|-----------------| +| Dev server startup | ~8-15s | ~1-3s | +| HMR speed | ~500ms-2s | ~50-100ms | +| Production build | ~45-90s | ~15-30s | +| Bundle overhead | Next.js runtime | None | +| Type safety (Electron) | 0% | 100% | +| Debug capabilities | Limited | Full debug console | + +### Target Stack + +- **Bundler**: Vite +- **Framework**: React 19 +- **Routing**: TanStack Router (file-based) +- **Data Fetching**: TanStack Query +- **State**: Zustand (unchanged) +- **Styling**: Tailwind CSS 4 (unchanged) +- **Desktop**: Electron (TypeScript rewrite) + +--- + +## Current Architecture Assessment + +### Data Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ELECTRON APP │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ HTTP/WS ┌─────────────────┐ │ +│ │ React SPA │ ←──────────────────→ │ Backend Server │ │ +│ │ (Next.js) │ localhost:3008 │ (Express) │ │ +│ │ │ │ │ │ +│ │ • Zustand Store │ │ • AI Providers │ │ +│ │ • 16 Views │ │ • Git/FS Ops │ │ +│ │ • 180+ Comps │ │ • Terminal │ │ +│ └────────┬────────┘ └─────────────────┘ │ +│ │ │ +│ │ IPC (minimal - dialogs/shell only) │ +│ ↓ │ +│ ┌─────────────────┐ │ +│ │ Electron Main │ • File dialogs │ +│ │ (main.js) │ • Shell operations │ +│ │ **NO TYPES** │ • App paths │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Current Electron Layer Issues + +| Issue | Impact | Solution | +|-------|--------|----------| +| Pure JavaScript | No compile-time safety | Migrate to TypeScript | +| Untyped IPC handlers | Runtime errors | IPC Schema with generics | +| String literal channels | Typos cause silent failures | Const enums | +| No debug tooling | Hard to diagnose issues | Debug console feature | +| Monolithic main.js | Hard to maintain | Modular IPC organization | + +### Current Component Structure Issues + +| View File | Lines | Issue | +|-----------|-------|-------| +| spec-view.tsx | 1,230 | Exceeds 500-line threshold | +| analysis-view.tsx | 1,134 | Exceeds 500-line threshold | +| agent-view.tsx | 916 | Exceeds 500-line threshold | +| welcome-view.tsx | 815 | Exceeds 500-line threshold | +| context-view.tsx | 735 | Exceeds 500-line threshold | +| terminal-view.tsx | 697 | Exceeds 500-line threshold | +| interview-view.tsx | 637 | Exceeds 500-line threshold | +| board-view.tsx | 685 | ✅ Already has subfolder structure | + +--- + +## Proposed New Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ MIGRATED ARCHITECTURE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ @automaker/app (Vite + React) │ │ +│ ├──────────────────────────────────────────────────────────────────┤ │ +│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │ │ +│ │ │ TanStack │ │ TanStack │ │ Zustand │ │ │ +│ │ │ Router │ │ Query │ │ Store │ │ │ +│ │ │ (file-based) │ │ (data fetch) │ │ (UI state) │ │ │ +│ │ └────────────────┘ └────────────────┘ └────────────────────┘ │ │ +│ │ │ │ +│ │ src/ │ │ +│ │ ├── routes/ # TanStack file-based routes │ │ +│ │ ├── components/ # Refactored per folder-pattern.md │ │ +│ │ ├── hooks/ # React hooks │ │ +│ │ ├── store/ # Zustand stores │ │ +│ │ ├── lib/ # Utilities │ │ +│ │ └── config/ # Configuration │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ HTTP/WS (unchanged) │ Type-Safe IPC │ +│ ↓ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ Electron Layer (TypeScript) │ │ +│ ├──────────────────────────────────────────────────────────────────┤ │ +│ │ electron/ │ │ +│ │ ├── main.ts # Main process entry │ │ +│ │ ├── preload.ts # Context bridge exposure │ │ +│ │ ├── debug-console/ # Debug console feature │ │ +│ │ └── ipc/ # Modular IPC handlers │ │ +│ │ ├── ipc-schema.ts # Type definitions │ │ +│ │ ├── dialog/ # File dialogs │ │ +│ │ ├── shell/ # Shell operations │ │ +│ │ └── server/ # Server management │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ @automaker/server (unchanged) │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ SHARED PACKAGES (libs/) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ @automaker/types # API contracts, model definitions │ +│ @automaker/utils # Shared utilities (error handling, etc.) │ +│ @automaker/platform # OS-specific utilities, path handling │ +│ @automaker/model-resolver # Model string resolution │ +│ @automaker/ipc-types # IPC channel type definitions │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Folder Structure + +### apps/app/ (After Migration) + +``` +apps/app/ +├── electron/ # Electron main process (TypeScript) +│ ├── main.ts # Main entry point +│ ├── preload.ts # Context bridge +│ ├── tsconfig.json # Electron-specific TS config +│ ├── debug-console/ +│ │ ├── debug-console.html +│ │ ├── debug-console-preload.ts +│ │ └── debug-mode.ts +│ ├── ipc/ +│ │ ├── ipc-schema.ts # Central type definitions +│ │ ├── context-exposer.ts # Exposes all contexts to renderer +│ │ ├── listeners-register.ts # Registers all main process handlers +│ │ ├── dialog/ +│ │ │ ├── dialog-channels.ts # Channel constants +│ │ │ ├── dialog-context.ts # Preload exposure +│ │ │ └── dialog-listeners.ts # Main process handlers +│ │ ├── shell/ +│ │ │ ├── shell-channels.ts +│ │ │ ├── shell-context.ts +│ │ │ └── shell-listeners.ts +│ │ ├── app-info/ +│ │ │ ├── app-info-channels.ts +│ │ │ ├── app-info-context.ts +│ │ │ └── app-info-listeners.ts +│ │ └── server/ +│ │ ├── server-channels.ts +│ │ ├── server-context.ts +│ │ └── server-listeners.ts +│ └── helpers/ +│ ├── server-manager.ts # Backend server spawn/health +│ ├── static-server.ts # Production static file server +│ ├── window-helpers.ts # Window utilities +│ └── window-registry.ts # Multi-window tracking +│ +├── src/ +│ ├── routes/ # TanStack Router (file-based) +│ │ ├── __root.tsx # Root layout +│ │ ├── index.tsx # Welcome/home (default route) +│ │ ├── board.tsx # Board view +│ │ ├── agent.tsx # Agent view +│ │ ├── settings.tsx # Settings view +│ │ ├── setup.tsx # Setup view +│ │ ├── terminal.tsx # Terminal view +│ │ ├── spec.tsx # Spec view +│ │ ├── context.tsx # Context view +│ │ ├── profiles.tsx # Profiles view +│ │ ├── interview.tsx # Interview view +│ │ ├── wiki.tsx # Wiki view +│ │ ├── analysis.tsx # Analysis view +│ │ └── agent-tools.tsx # Agent tools view +│ │ +│ ├── components/ # Refactored per folder-pattern.md +│ │ ├── ui/ # Global UI primitives (unchanged) +│ │ ├── layout/ +│ │ │ ├── sidebar.tsx +│ │ │ ├── base-layout.tsx +│ │ │ └── index.ts +│ │ ├── dialogs/ # Global dialogs +│ │ │ ├── index.ts +│ │ │ ├── new-project-modal.tsx +│ │ │ ├── workspace-picker-modal.tsx +│ │ │ └── file-browser-dialog.tsx +│ │ └── views/ # Complex view components +│ │ ├── board-view/ # ✅ Already structured +│ │ ├── settings-view/ # Needs dialogs reorganization +│ │ ├── setup-view/ # ✅ Already structured +│ │ ├── profiles-view/ # ✅ Already structured +│ │ ├── agent-view/ # NEW: needs subfolder +│ │ │ ├── components/ +│ │ │ │ ├── index.ts +│ │ │ │ ├── message-list.tsx +│ │ │ │ ├── message-input.tsx +│ │ │ │ └── session-sidebar.tsx +│ │ │ ├── dialogs/ +│ │ │ │ ├── index.ts +│ │ │ │ ├── delete-session-dialog.tsx +│ │ │ │ └── delete-all-archived-dialog.tsx +│ │ │ └── hooks/ +│ │ │ ├── index.ts +│ │ │ └── use-agent-state.ts +│ │ ├── spec-view/ # NEW: needs subfolder (1230 lines!) +│ │ ├── analysis-view/ # NEW: needs subfolder (1134 lines!) +│ │ ├── context-view/ # NEW: needs subfolder +│ │ ├── welcome-view/ # NEW: needs subfolder +│ │ ├── interview-view/ # NEW: needs subfolder +│ │ └── terminal-view/ # Expand existing +│ │ +│ ├── hooks/ # Global hooks +│ ├── store/ # Zustand stores +│ ├── lib/ # Utilities +│ ├── config/ # Configuration +│ ├── contexts/ # React contexts +│ ├── types/ # Type definitions +│ ├── App.tsx # Root component +│ ├── renderer.ts # Vite entry point +│ └── routeTree.gen.ts # Generated by TanStack Router +│ +├── index.html # Vite HTML entry +├── vite.config.mts # Vite configuration +├── tsconfig.json # TypeScript config (renderer) +├── package.json +└── tailwind.config.ts +``` + +--- + +## Shared Packages (libs/) + +### Package Overview + +``` +libs/ +├── @automaker/types # API contracts, model definitions +├── @automaker/utils # General utilities (error handling, logger) +├── @automaker/platform # OS-specific utilities, path handling +├── @automaker/model-resolver # Model string resolution +└── @automaker/ipc-types # IPC channel type definitions +``` + +### @automaker/types + +Shared type definitions for API contracts between frontend and backend. + +``` +libs/types/ +├── src/ +│ ├── api.ts # API response types +│ ├── models.ts # ModelDefinition, ProviderStatus +│ ├── features.ts # Feature, FeatureStatus, Priority +│ ├── sessions.ts # Session, Message types +│ ├── agent.ts # Agent types +│ ├── git.ts # Git operation types +│ ├── worktree.ts # Worktree types +│ └── index.ts # Barrel export +├── package.json +└── tsconfig.json +``` + +```typescript +// libs/types/src/models.ts +export interface ModelDefinition { + id: string + name: string + provider: ProviderType + contextWindow: number + maxOutputTokens: number + capabilities: ModelCapabilities +} + +export interface ModelCapabilities { + vision: boolean + toolUse: boolean + streaming: boolean + computerUse: boolean +} + +export type ProviderType = "claude" | "openai" | "gemini" | "ollama" +``` + +### @automaker/utils + +General utilities shared between frontend and backend. + +``` +libs/utils/ +├── src/ +│ ├── error-handler.ts # Error classification & user-friendly messages +│ ├── logger.ts # Logging utilities +│ ├── conversation-utils.ts # Message formatting & history +│ ├── image-utils.ts # Image processing utilities +│ ├── string-utils.ts # String manipulation helpers +│ └── index.ts +├── package.json +└── tsconfig.json +``` + +```typescript +// libs/utils/src/error-handler.ts +export type ErrorType = + | "authentication" + | "rate_limit" + | "network" + | "validation" + | "not_found" + | "server" + | "unknown" + +export interface ErrorInfo { + type: ErrorType + message: string + userMessage: string + retryable: boolean + statusCode?: number +} + +export function classifyError(error: unknown): ErrorInfo +export function getUserFriendlyErrorMessage(error: unknown): string +export function isAbortError(error: unknown): boolean +export function isAuthenticationError(error: unknown): boolean +export function isRateLimitError(error: unknown): boolean +``` + +### @automaker/platform + +**OS-specific utilities, path handling, and cross-platform helpers.** + +``` +libs/platform/ +├── src/ +│ ├── paths/ +│ │ ├── index.ts # Path utilities barrel export +│ │ ├── path-resolver.ts # Cross-platform path resolution +│ │ ├── path-constants.ts # Common path constants +│ │ └── path-validator.ts # Path validation utilities +│ ├── os/ +│ │ ├── index.ts # OS utilities barrel export +│ │ ├── platform-info.ts # Platform detection & info +│ │ ├── shell-commands.ts # OS-specific shell commands +│ │ └── env-utils.ts # Environment variable utilities +│ ├── fs/ +│ │ ├── index.ts # FS utilities barrel export +│ │ ├── safe-fs.ts # Symlink-safe file operations +│ │ ├── temp-files.ts # Temporary file handling +│ │ └── permissions.ts # File permission utilities +│ └── index.ts # Main barrel export +├── package.json +└── tsconfig.json +``` + +```typescript +// libs/platform/src/paths/path-resolver.ts +import path from "path" + +/** + * Platform-aware path separator + */ +export const SEP = path.sep + +/** + * Normalizes a path to use the correct separator for the current OS + */ +export function normalizePath(inputPath: string): string { + return inputPath.replace(/[/\\]/g, SEP) +} + +/** + * Converts a path to POSIX format (forward slashes) + * Useful for consistent storage/comparison + */ +export function toPosixPath(inputPath: string): string { + return inputPath.replace(/\\/g, "/") +} + +/** + * Converts a path to Windows format (backslashes) + */ +export function toWindowsPath(inputPath: string): string { + return inputPath.replace(/\//g, "\\") +} + +/** + * Resolves a path relative to a base, handling platform differences + */ +export function resolvePath(basePath: string, ...segments: string[]): string { + return path.resolve(basePath, ...segments) +} + +/** + * Gets the relative path from one location to another + */ +export function getRelativePath(from: string, to: string): string { + return path.relative(from, to) +} + +/** + * Joins path segments with proper platform separator + */ +export function joinPath(...segments: string[]): string { + return path.join(...segments) +} + +/** + * Extracts directory name from a path + */ +export function getDirname(filePath: string): string { + return path.dirname(filePath) +} + +/** + * Extracts filename from a path + */ +export function getBasename(filePath: string, ext?: string): string { + return path.basename(filePath, ext) +} + +/** + * Extracts file extension from a path + */ +export function getExtension(filePath: string): string { + return path.extname(filePath) +} + +/** + * Checks if a path is absolute + */ +export function isAbsolutePath(inputPath: string): boolean { + return path.isAbsolute(inputPath) +} + +/** + * Ensures a path is absolute, resolving relative to cwd if needed + */ +export function ensureAbsolutePath(inputPath: string, basePath?: string): string { + if (isAbsolutePath(inputPath)) { + return inputPath + } + return resolvePath(basePath || process.cwd(), inputPath) +} +``` + +```typescript +// libs/platform/src/paths/path-constants.ts +import path from "path" +import os from "os" + +/** + * Common system paths + */ +export const SYSTEM_PATHS = { + /** User's home directory */ + home: os.homedir(), + + /** System temporary directory */ + temp: os.tmpdir(), + + /** Current working directory */ + cwd: process.cwd(), +} as const + +/** + * Gets the appropriate app data directory for the current platform + */ +export function getAppDataPath(appName: string): string { + const platform = process.platform + + switch (platform) { + case "win32": + return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), appName) + case "darwin": + return path.join(os.homedir(), "Library", "Application Support", appName) + default: // Linux and others + return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), appName) + } +} + +/** + * Gets the appropriate cache directory for the current platform + */ +export function getCachePath(appName: string): string { + const platform = process.platform + + switch (platform) { + case "win32": + return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), appName, "Cache") + case "darwin": + return path.join(os.homedir(), "Library", "Caches", appName) + default: + return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), appName) + } +} + +/** + * Gets the appropriate logs directory for the current platform + */ +export function getLogsPath(appName: string): string { + const platform = process.platform + + switch (platform) { + case "win32": + return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), appName, "Logs") + case "darwin": + return path.join(os.homedir(), "Library", "Logs", appName) + default: + return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"), appName, "logs") + } +} + +/** + * Gets the user's Documents directory + */ +export function getDocumentsPath(): string { + const platform = process.platform + + switch (platform) { + case "win32": + return process.env.USERPROFILE + ? path.join(process.env.USERPROFILE, "Documents") + : path.join(os.homedir(), "Documents") + case "darwin": + return path.join(os.homedir(), "Documents") + default: + return process.env.XDG_DOCUMENTS_DIR || path.join(os.homedir(), "Documents") + } +} + +/** + * Gets the user's Desktop directory + */ +export function getDesktopPath(): string { + const platform = process.platform + + switch (platform) { + case "win32": + return process.env.USERPROFILE + ? path.join(process.env.USERPROFILE, "Desktop") + : path.join(os.homedir(), "Desktop") + case "darwin": + return path.join(os.homedir(), "Desktop") + default: + return process.env.XDG_DESKTOP_DIR || path.join(os.homedir(), "Desktop") + } +} +``` + +```typescript +// libs/platform/src/paths/path-validator.ts +import path from "path" +import { isAbsolutePath } from "./path-resolver" + +/** + * Characters that are invalid in file/directory names on Windows + */ +const WINDOWS_INVALID_CHARS = /[<>:"|?*\x00-\x1f]/g + +/** + * Reserved names on Windows (case-insensitive) + */ +const WINDOWS_RESERVED_NAMES = [ + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" +] + +export interface PathValidationResult { + valid: boolean + errors: string[] + sanitized?: string +} + +/** + * Validates a filename for the current platform + */ +export function validateFilename(filename: string): PathValidationResult { + const errors: string[] = [] + + if (!filename || filename.trim().length === 0) { + return { valid: false, errors: ["Filename cannot be empty"] } + } + + // Check for path separators (filename shouldn't be a path) + if (filename.includes("/") || filename.includes("\\")) { + errors.push("Filename cannot contain path separators") + } + + // Platform-specific checks + if (process.platform === "win32") { + if (WINDOWS_INVALID_CHARS.test(filename)) { + errors.push("Filename contains invalid characters for Windows") + } + + const nameWithoutExt = filename.split(".")[0].toUpperCase() + if (WINDOWS_RESERVED_NAMES.includes(nameWithoutExt)) { + errors.push(`"${nameWithoutExt}" is a reserved name on Windows`) + } + + if (filename.endsWith(" ") || filename.endsWith(".")) { + errors.push("Filename cannot end with a space or period on Windows") + } + } + + // Check length + if (filename.length > 255) { + errors.push("Filename exceeds maximum length of 255 characters") + } + + return { + valid: errors.length === 0, + errors, + sanitized: errors.length > 0 ? sanitizeFilename(filename) : filename + } +} + +/** + * Sanitizes a filename for cross-platform compatibility + */ +export function sanitizeFilename(filename: string): string { + let sanitized = filename + .replace(WINDOWS_INVALID_CHARS, "_") + .replace(/[/\\]/g, "_") + .trim() + + // Handle Windows reserved names + const nameWithoutExt = sanitized.split(".")[0].toUpperCase() + if (WINDOWS_RESERVED_NAMES.includes(nameWithoutExt)) { + sanitized = "_" + sanitized + } + + // Remove trailing spaces and periods (Windows) + sanitized = sanitized.replace(/[\s.]+$/, "") + + // Ensure not empty + if (!sanitized) { + sanitized = "unnamed" + } + + // Truncate if too long + if (sanitized.length > 255) { + const ext = path.extname(sanitized) + const name = path.basename(sanitized, ext) + sanitized = name.slice(0, 255 - ext.length) + ext + } + + return sanitized +} + +/** + * Validates a full path for the current platform + */ +export function validatePath(inputPath: string): PathValidationResult { + const errors: string[] = [] + + if (!inputPath || inputPath.trim().length === 0) { + return { valid: false, errors: ["Path cannot be empty"] } + } + + // Check total path length + const maxPathLength = process.platform === "win32" ? 260 : 4096 + if (inputPath.length > maxPathLength) { + errors.push(`Path exceeds maximum length of ${maxPathLength} characters`) + } + + // Validate each segment + const segments = inputPath.split(/[/\\]/).filter(Boolean) + for (const segment of segments) { + // Skip drive letters on Windows + if (process.platform === "win32" && /^[a-zA-Z]:$/.test(segment)) { + continue + } + + const segmentValidation = validateFilename(segment) + if (!segmentValidation.valid) { + errors.push(...segmentValidation.errors.map(e => `Segment "${segment}": ${e}`)) + } + } + + return { + valid: errors.length === 0, + errors + } +} + +/** + * Checks if a path is within a base directory (prevents directory traversal) + */ +export function isPathWithin(childPath: string, parentPath: string): boolean { + const resolvedChild = path.resolve(childPath) + const resolvedParent = path.resolve(parentPath) + + return resolvedChild.startsWith(resolvedParent + path.sep) || + resolvedChild === resolvedParent +} +``` + +```typescript +// libs/platform/src/os/platform-info.ts +import os from "os" + +export type Platform = "windows" | "macos" | "linux" | "unknown" +export type Architecture = "x64" | "arm64" | "ia32" | "unknown" + +export interface PlatformInfo { + platform: Platform + arch: Architecture + release: string + hostname: string + username: string + cpus: number + totalMemory: number + freeMemory: number + isWsl: boolean + isDocker: boolean +} + +/** + * Gets the normalized platform name + */ +export function getPlatform(): Platform { + switch (process.platform) { + case "win32": + return "windows" + case "darwin": + return "macos" + case "linux": + return "linux" + default: + return "unknown" + } +} + +/** + * Gets the normalized architecture + */ +export function getArchitecture(): Architecture { + switch (process.arch) { + case "x64": + return "x64" + case "arm64": + return "arm64" + case "ia32": + return "ia32" + default: + return "unknown" + } +} + +/** + * Checks if running on Windows + */ +export function isWindows(): boolean { + return process.platform === "win32" +} + +/** + * Checks if running on macOS + */ +export function isMacOS(): boolean { + return process.platform === "darwin" +} + +/** + * Checks if running on Linux + */ +export function isLinux(): boolean { + return process.platform === "linux" +} + +/** + * Checks if running in WSL (Windows Subsystem for Linux) + */ +export function isWsl(): boolean { + if (process.platform !== "linux") return false + + try { + const release = os.release().toLowerCase() + return release.includes("microsoft") || release.includes("wsl") + } catch { + return false + } +} + +/** + * Checks if running in Docker container + */ +export function isDocker(): boolean { + try { + const fs = require("fs") + return fs.existsSync("/.dockerenv") || + (fs.existsSync("/proc/1/cgroup") && + fs.readFileSync("/proc/1/cgroup", "utf8").includes("docker")) + } catch { + return false + } +} + +/** + * Gets comprehensive platform information + */ +export function getPlatformInfo(): PlatformInfo { + return { + platform: getPlatform(), + arch: getArchitecture(), + release: os.release(), + hostname: os.hostname(), + username: os.userInfo().username, + cpus: os.cpus().length, + totalMemory: os.totalmem(), + freeMemory: os.freemem(), + isWsl: isWsl(), + isDocker: isDocker() + } +} + +/** + * Gets the appropriate line ending for the current platform + */ +export function getLineEnding(): string { + return isWindows() ? "\r\n" : "\n" +} + +/** + * Normalizes line endings to the current platform + */ +export function normalizeLineEndings(text: string): string { + const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n") + return isWindows() ? normalized.replace(/\n/g, "\r\n") : normalized +} +``` + +```typescript +// libs/platform/src/os/shell-commands.ts +import { isWindows, isMacOS } from "./platform-info" + +export interface ShellCommand { + command: string + args: string[] +} + +/** + * Gets the appropriate command to open a file/URL with default application + */ +export function getOpenCommand(target: string): ShellCommand { + if (isWindows()) { + return { command: "cmd", args: ["/c", "start", "", target] } + } else if (isMacOS()) { + return { command: "open", args: [target] } + } else { + return { command: "xdg-open", args: [target] } + } +} + +/** + * Gets the appropriate command to reveal a file in file manager + */ +export function getRevealCommand(filePath: string): ShellCommand { + if (isWindows()) { + return { command: "explorer", args: ["/select,", filePath] } + } else if (isMacOS()) { + return { command: "open", args: ["-R", filePath] } + } else { + // Linux: try multiple file managers + return { command: "xdg-open", args: [require("path").dirname(filePath)] } + } +} + +/** + * Gets the default shell for the current platform + */ +export function getDefaultShell(): string { + if (isWindows()) { + return process.env.COMSPEC || "cmd.exe" + } + return process.env.SHELL || "/bin/sh" +} + +/** + * Gets shell-specific arguments for running a command + */ +export function getShellArgs(command: string): ShellCommand { + if (isWindows()) { + return { command: "cmd.exe", args: ["/c", command] } + } + return { command: "/bin/sh", args: ["-c", command] } +} + +/** + * Escapes a string for safe use in shell commands + */ +export function escapeShellArg(arg: string): string { + if (isWindows()) { + // Windows cmd.exe escaping + return `"${arg.replace(/"/g, '""')}"` + } + // POSIX shell escaping + return `'${arg.replace(/'/g, "'\\''")}'` +} +``` + +```typescript +// libs/platform/src/os/env-utils.ts +import { isWindows } from "./platform-info" + +/** + * Gets an environment variable with a fallback + */ +export function getEnv(key: string, fallback?: string): string | undefined { + return process.env[key] ?? fallback +} + +/** + * Gets an environment variable, throwing if not set + */ +export function requireEnv(key: string): string { + const value = process.env[key] + if (value === undefined) { + throw new Error(`Required environment variable "${key}" is not set`) + } + return value +} + +/** + * Parses a boolean environment variable + */ +export function getBoolEnv(key: string, fallback = false): boolean { + const value = process.env[key] + if (value === undefined) return fallback + return ["true", "1", "yes", "on"].includes(value.toLowerCase()) +} + +/** + * Parses a numeric environment variable + */ +export function getNumericEnv(key: string, fallback: number): number { + const value = process.env[key] + if (value === undefined) return fallback + const parsed = parseInt(value, 10) + return isNaN(parsed) ? fallback : parsed +} + +/** + * Expands environment variables in a string + * Supports both $VAR and ${VAR} syntax, plus %VAR% on Windows + */ +export function expandEnvVars(input: string): string { + let result = input + + // Expand ${VAR} syntax + result = result.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || "") + + // Expand $VAR syntax (not followed by another word char) + result = result.replace(/\$([A-Za-z_][A-Za-z0-9_]*)(?![A-Za-z0-9_])/g, (_, name) => process.env[name] || "") + + // Expand %VAR% syntax (Windows) + if (isWindows()) { + result = result.replace(/%([^%]+)%/g, (_, name) => process.env[name] || "") + } + + return result +} + +/** + * Gets the PATH environment variable as an array + */ +export function getPathEntries(): string[] { + const pathVar = process.env.PATH || process.env.Path || "" + const separator = isWindows() ? ";" : ":" + return pathVar.split(separator).filter(Boolean) +} + +/** + * Checks if a command is available in PATH + */ +export function isCommandInPath(command: string): boolean { + const pathEntries = getPathEntries() + const extensions = isWindows() ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";") : [""] + const path = require("path") + const fs = require("fs") + + for (const dir of pathEntries) { + for (const ext of extensions) { + const fullPath = path.join(dir, command + ext) + try { + fs.accessSync(fullPath, fs.constants.X_OK) + return true + } catch { + // Continue searching + } + } + } + + return false +} +``` + +```typescript +// libs/platform/src/fs/safe-fs.ts +import fs from "fs" +import path from "path" + +/** + * Safely reads a file, following symlinks but preventing escape from base directory + */ +export async function safeReadFile( + filePath: string, + basePath: string, + encoding: BufferEncoding = "utf8" +): Promise { + const resolvedPath = path.resolve(filePath) + const resolvedBase = path.resolve(basePath) + + // Resolve symlinks + const realPath = await fs.promises.realpath(resolvedPath) + const realBase = await fs.promises.realpath(resolvedBase) + + // Ensure resolved path is within base + if (!realPath.startsWith(realBase + path.sep) && realPath !== realBase) { + throw new Error(`Path "${filePath}" resolves outside of allowed directory`) + } + + return fs.promises.readFile(realPath, encoding) +} + +/** + * Safely writes a file, preventing writes outside base directory + */ +export async function safeWriteFile( + filePath: string, + basePath: string, + content: string +): Promise { + const resolvedPath = path.resolve(filePath) + const resolvedBase = path.resolve(basePath) + + // Ensure path is within base before any symlink resolution + if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) { + throw new Error(`Path "${filePath}" is outside of allowed directory`) + } + + // Check parent directory exists and is within base + const parentDir = path.dirname(resolvedPath) + + try { + const realParent = await fs.promises.realpath(parentDir) + const realBase = await fs.promises.realpath(resolvedBase) + + if (!realParent.startsWith(realBase + path.sep) && realParent !== realBase) { + throw new Error(`Parent directory resolves outside of allowed directory`) + } + } catch (error) { + // Parent doesn't exist, that's OK - we'll create it + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error + } + } + + await fs.promises.mkdir(path.dirname(resolvedPath), { recursive: true }) + await fs.promises.writeFile(resolvedPath, content, "utf8") +} + +/** + * Checks if a path exists and is accessible + */ +export async function pathExists(filePath: string): Promise { + try { + await fs.promises.access(filePath) + return true + } catch { + return false + } +} + +/** + * Gets file stats, returning null if file doesn't exist + */ +export async function safeStat(filePath: string): Promise { + try { + return await fs.promises.stat(filePath) + } catch { + return null + } +} + +/** + * Recursively removes a directory + */ +export async function removeDirectory(dirPath: string): Promise { + await fs.promises.rm(dirPath, { recursive: true, force: true }) +} + +/** + * Copies a file or directory + */ +export async function copy(src: string, dest: string): Promise { + const stats = await fs.promises.stat(src) + + if (stats.isDirectory()) { + await fs.promises.mkdir(dest, { recursive: true }) + const entries = await fs.promises.readdir(src, { withFileTypes: true }) + + for (const entry of entries) { + await copy( + path.join(src, entry.name), + path.join(dest, entry.name) + ) + } + } else { + await fs.promises.copyFile(src, dest) + } +} +``` + +```typescript +// libs/platform/src/index.ts +// Main barrel export + +// Path utilities +export * from "./paths/path-resolver" +export * from "./paths/path-constants" +export * from "./paths/path-validator" + +// OS utilities +export * from "./os/platform-info" +export * from "./os/shell-commands" +export * from "./os/env-utils" + +// File system utilities +export * from "./fs/safe-fs" +``` + +### @automaker/model-resolver + +Model string resolution shared between frontend and backend. + +``` +libs/model-resolver/ +├── src/ +│ ├── model-map.ts # CLAUDE_MODEL_MAP, DEFAULT_MODELS +│ ├── resolver.ts # resolveModelString, getEffectiveModel +│ └── index.ts +├── package.json +└── tsconfig.json +``` + +### @automaker/ipc-types + +IPC channel type definitions for type-safe Electron communication. + +``` +libs/ipc-types/ +├── src/ +│ ├── schema.ts # IPCSchema interface +│ ├── channels.ts # Channel constant enums +│ ├── helpers.ts # Type helper functions +│ └── index.ts +├── package.json +└── tsconfig.json +``` + +--- + +## Type-Safe Electron Implementation + +### IPC Schema Definition + +```typescript +// electron/ipc/ipc-schema.ts +import type { OpenDialogOptions, SaveDialogOptions } from "electron" + +// Dialog result types +export interface DialogResult { + canceled: boolean + filePaths?: string[] + filePath?: string + data?: T +} + +// App path names (from Electron) +export type AppPathName = + | "home" | "appData" | "userData" | "sessionData" + | "temp" | "exe" | "module" | "desktop" + | "documents" | "downloads" | "music" + | "pictures" | "videos" | "recent" | "logs" | "crashDumps" + +// Complete IPC Schema with request/response types +export interface IPCSchema { + // Dialog operations + "dialog:openDirectory": { + request: Partial + response: DialogResult + } + "dialog:openFile": { + request: Partial + response: DialogResult + } + "dialog:saveFile": { + request: Partial + response: DialogResult + } + + // Shell operations + "shell:openExternal": { + request: { url: string } + response: { success: boolean; error?: string } + } + "shell:openPath": { + request: { path: string } + response: { success: boolean; error?: string } + } + + // App info + "app:getPath": { + request: { name: AppPathName } + response: string + } + "app:getVersion": { + request: void + response: string + } + "app:isPackaged": { + request: void + response: boolean + } + + // Server management + "server:getUrl": { + request: void + response: string + } + + // Connection test + "ping": { + request: void + response: "pong" + } + + // Debug console + "debug:log": { + request: { + level: DebugLogLevel + category: DebugCategory + message: string + args: unknown[] + } + response: void + } +} + +export type DebugLogLevel = "info" | "warn" | "error" | "debug" | "success" +export type DebugCategory = + | "general" | "ipc" | "route" | "network" + | "perf" | "state" | "lifecycle" | "updater" + +// Type extractors +export type IPCChannel = keyof IPCSchema +export type IPCRequest = IPCSchema[T]["request"] +export type IPCResponse = IPCSchema[T]["response"] +``` + +### Modular IPC Organization + +```typescript +// electron/ipc/dialog/dialog-channels.ts +export const DIALOG_CHANNELS = { + OPEN_DIRECTORY: "dialog:openDirectory", + OPEN_FILE: "dialog:openFile", + SAVE_FILE: "dialog:saveFile", +} as const + +// electron/ipc/dialog/dialog-context.ts +import { contextBridge, ipcRenderer } from "electron" +import { DIALOG_CHANNELS } from "./dialog-channels" +import type { IPCRequest, IPCResponse } from "../ipc-schema" + +export function exposeDialogContext(): void { + contextBridge.exposeInMainWorld("dialogAPI", { + openDirectory: (options?: IPCRequest<"dialog:openDirectory">) => + ipcRenderer.invoke(DIALOG_CHANNELS.OPEN_DIRECTORY, options), + + openFile: (options?: IPCRequest<"dialog:openFile">) => + ipcRenderer.invoke(DIALOG_CHANNELS.OPEN_FILE, options), + + saveFile: (options?: IPCRequest<"dialog:saveFile">) => + ipcRenderer.invoke(DIALOG_CHANNELS.SAVE_FILE, options), + }) +} + +// electron/ipc/dialog/dialog-listeners.ts +import { ipcMain, dialog, BrowserWindow } from "electron" +import { DIALOG_CHANNELS } from "./dialog-channels" +import type { IPCRequest, IPCResponse } from "../ipc-schema" +import { debugLog } from "../../helpers/debug-mode" + +export function addDialogEventListeners(mainWindow: BrowserWindow): void { + ipcMain.handle( + DIALOG_CHANNELS.OPEN_DIRECTORY, + async (_, options: IPCRequest<"dialog:openDirectory"> = {}) => { + debugLog.ipc(`OPEN_DIRECTORY called with options: ${JSON.stringify(options)}`) + + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openDirectory", "createDirectory"], + ...options, + }) + + debugLog.ipc(`OPEN_DIRECTORY result: canceled=${result.canceled}, paths=${result.filePaths.length}`) + + return { + canceled: result.canceled, + filePaths: result.filePaths, + } satisfies IPCResponse<"dialog:openDirectory"> + } + ) + + ipcMain.handle( + DIALOG_CHANNELS.OPEN_FILE, + async (_, options: IPCRequest<"dialog:openFile"> = {}) => { + debugLog.ipc(`OPEN_FILE called`) + + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }) + + return { + canceled: result.canceled, + filePaths: result.filePaths, + } satisfies IPCResponse<"dialog:openFile"> + } + ) + + ipcMain.handle( + DIALOG_CHANNELS.SAVE_FILE, + async (_, options: IPCRequest<"dialog:saveFile"> = {}) => { + debugLog.ipc(`SAVE_FILE called`) + + const result = await dialog.showSaveDialog(mainWindow, options) + + return { + canceled: result.canceled, + filePath: result.filePath, + } satisfies IPCResponse<"dialog:saveFile"> + } + ) +} +``` + +--- + +## Components Refactoring + +### Priority Matrix + +| Priority | View | Lines | Action Required | +|----------|------|-------|-----------------| +| 🔴 P0 | spec-view | 1,230 | Create subfolder with components/, dialogs/, hooks/ | +| 🔴 P0 | analysis-view | 1,134 | Create subfolder with components/, dialogs/, hooks/ | +| 🔴 P0 | agent-view | 916 | Create subfolder, extract message list, input, sidebar | +| 🟡 P1 | welcome-view | 815 | Create subfolder, extract sections | +| 🟡 P1 | context-view | 735 | Create subfolder, extract components | +| 🟡 P1 | terminal-view | 697 | Expand existing subfolder | +| 🟡 P1 | interview-view | 637 | Create subfolder | +| 🟢 P2 | settings-view | 178 | Move dialogs from components/ to dialogs/ | +| ✅ Done | board-view | 685 | Already properly structured | +| ✅ Done | setup-view | 144 | Already properly structured | +| ✅ Done | profiles-view | 300 | Already properly structured | + +### Immediate Dialog Reorganization + +```bash +# Settings-view: Move dialogs to proper location +mv settings-view/components/keyboard-map-dialog.tsx → settings-view/dialogs/ +mv settings-view/components/delete-project-dialog.tsx → settings-view/dialogs/ + +# Root components: Organize global dialogs +mv components/dialogs/board-background-modal.tsx → board-view/dialogs/ + +# Agent-related dialogs: Move to agent-view +mv components/delete-session-dialog.tsx → agent-view/dialogs/ +mv components/delete-all-archived-sessions-dialog.tsx → agent-view/dialogs/ +``` + +--- + +## Web + Electron Dual Support + +### Platform Detection + +```typescript +// src/lib/platform.ts +export const isElectron = typeof window !== "undefined" && + "electronAPI" in window + +export const platform = { + isElectron, + isWeb: !isElectron, + isMac: isElectron ? window.electronAPI.platform === "darwin" : false, + isWindows: isElectron ? window.electronAPI.platform === "win32" : false, + isLinux: isElectron ? window.electronAPI.platform === "linux" : false, +} +``` + +### API Abstraction Layer + +```typescript +// src/lib/api/file-picker.ts +import { platform } from "../platform" + +export interface FilePickerResult { + canceled: boolean + paths: string[] +} + +export async function pickDirectory(): Promise { + if (platform.isElectron) { + const result = await window.dialogAPI.openDirectory() + return { canceled: result.canceled, paths: result.filePaths || [] } + } + + // Web fallback using File System Access API + try { + const handle = await window.showDirectoryPicker() + return { canceled: false, paths: [handle.name] } + } catch (error) { + if ((error as Error).name === "AbortError") { + return { canceled: true, paths: [] } + } + throw error + } +} + +export async function pickFile(options?: { + accept?: Record +}): Promise { + if (platform.isElectron) { + const result = await window.dialogAPI.openFile({ + filters: options?.accept + ? Object.entries(options.accept).map(([name, extensions]) => ({ name, extensions })) + : undefined + }) + return { canceled: result.canceled, paths: result.filePaths || [] } + } + + // Web fallback + try { + const [handle] = await window.showOpenFilePicker({ + types: options?.accept + ? Object.entries(options.accept).map(([description, accept]) => ({ description, accept: { "application/*": accept } })) + : undefined + }) + return { canceled: false, paths: [handle.name] } + } catch (error) { + if ((error as Error).name === "AbortError") { + return { canceled: true, paths: [] } + } + throw error + } +} +``` + +--- + +## Migration Phases + +### Phase 1: Foundation (Week 1-2) + +**Goal**: Set up new build infrastructure without breaking existing functionality. + +- [ ] Create `vite.config.mts` with electron plugins +- [ ] Create `electron/tsconfig.json` for Electron TypeScript +- [ ] Convert `electron/main.js` → `electron/main.ts` +- [ ] Convert `electron/preload.js` → `electron/preload.ts` +- [ ] Implement IPC schema and type-safe handlers +- [ ] Set up TanStack Router configuration +- [ ] Port debug console from starter template +- [ ] Create `index.html` for Vite entry + +**Deliverables**: +- Working Vite dev server +- Type-safe Electron main process +- Debug console functional + +### Phase 2: Core Migration (Week 3-4) + +**Goal**: Replace Next.js with Vite while maintaining feature parity. + +- [ ] Create `src/renderer.ts` entry point +- [ ] Create `src/App.tsx` root component +- [ ] Set up TanStack Router with file-based routes +- [ ] Port all views to route files +- [ ] Update environment variables (`NEXT_PUBLIC_*` → `VITE_*`) +- [ ] Verify Zustand stores work unchanged +- [ ] Verify HTTP API client works unchanged +- [ ] Test both Electron and Web builds + +**Deliverables**: +- All views accessible via TanStack Router +- Both Electron and web builds functional +- No regression in existing functionality + +### Phase 3: Component Refactoring (Week 5-7) + +**Goal**: Refactor large view files to follow folder-pattern.md. + +- [ ] Refactor `spec-view.tsx` (1,230 lines) +- [ ] Refactor `analysis-view.tsx` (1,134 lines) +- [ ] Refactor `agent-view.tsx` (916 lines) +- [ ] Refactor `welcome-view.tsx` (815 lines) +- [ ] Refactor `context-view.tsx` (735 lines) +- [ ] Refactor `terminal-view.tsx` (697 lines) +- [ ] Refactor `interview-view.tsx` (637 lines) +- [ ] Reorganize `settings-view` dialogs + +**Deliverables**: +- All views under 500 lines +- Consistent folder structure across all views +- Barrel exports for all component folders + +### Phase 4: Package Extraction (Week 8) + +**Goal**: Create shared packages for better modularity. + +- [ ] Create `libs/types/` package +- [ ] Create `libs/utils/` package +- [ ] Create `libs/platform/` package +- [ ] Create `libs/model-resolver/` package +- [ ] Create `libs/ipc-types/` package +- [ ] Update imports across apps + +**Deliverables**: +- 5 new shared packages +- No code duplication between apps +- Clean dependency graph + +### Phase 5: Polish & Testing (Week 9-10) + +**Goal**: Ensure production readiness. + +- [ ] Write E2E tests with Playwright +- [ ] Performance benchmarking +- [ ] Bundle size optimization +- [ ] Documentation updates +- [ ] CI/CD pipeline updates +- [ ] Remove Next.js dependencies + +**Deliverables**: +- Comprehensive test coverage +- Performance metrics documentation +- Updated CI/CD configuration +- Clean package.json (no Next.js) + +--- + +## Expected Benefits + +### Developer Experience + +| Aspect | Before | After | +|--------|--------|-------| +| Dev server startup | 8-15 seconds | 1-3 seconds | +| Hot Module Replacement | 500ms-2s | 50-100ms | +| TypeScript in Electron | Not supported | Full support | +| Debug tooling | Limited | Full debug console | +| Build times | 45-90 seconds | 15-30 seconds | + +### Code Quality + +| Aspect | Before | After | +|--------|--------|-------| +| Electron type safety | 0% | 100% | +| Component organization | Inconsistent | Standardized | +| Code sharing | None | 5 shared packages | +| Path handling | Ad-hoc | Centralized utilities | + +### Bundle Size + +| Aspect | Before | After | +|--------|--------|-------| +| Next.js runtime | ~200KB | 0KB | +| Framework overhead | High | Minimal | +| Tree shaking | Limited | Full | + +--- + +## Risk Mitigation + +### Rollback Strategy + +1. **Branch-based development**: All work on feature branch +2. **Parallel running**: Keep Next.js functional until migration complete +3. **Feature flags**: Toggle between old/new implementations +4. **Comprehensive testing**: E2E tests before/after comparison + +### Known Challenges + +| Challenge | Mitigation | +|-----------|------------| +| Route migration | TanStack Router has similar file-based routing | +| Environment variables | Simple search/replace (`NEXT_PUBLIC_` → `VITE_`) | +| Build configuration | Reference electron-starter-template | +| SSR considerations | N/A - we don't use SSR | + +### Testing Strategy + +1. **Unit tests**: Vitest for component/utility testing +2. **Integration tests**: Test IPC communication +3. **E2E tests**: Playwright for full application testing +4. **Manual testing**: QA checklist for each view + +--- + +## Appendix: Vite Configuration Reference + +```typescript +// vite.config.mts +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react" +import electron from "vite-plugin-electron" +import renderer from "vite-plugin-electron-renderer" +import { TanStackRouterVite } from "@tanstack/router-plugin/vite" +import tailwindcss from "@tailwindcss/vite" +import path from "path" + +export default defineConfig({ + plugins: [ + react({ + babel: { + plugins: [["babel-plugin-react-compiler", {}]], + }, + }), + TanStackRouterVite({ + routesDirectory: "./src/routes", + generatedRouteTree: "./src/routeTree.gen.ts", + autoCodeSplitting: true, + }), + tailwindcss(), + electron([ + { + entry: "electron/main.ts", + vite: { + build: { + outDir: "dist-electron", + rollupOptions: { + external: ["electron"], + }, + }, + }, + }, + { + entry: "electron/preload.ts", + onstart: ({ reload }) => reload(), + vite: { + build: { + outDir: "dist-electron", + rollupOptions: { + external: ["electron"], + }, + }, + }, + }, + ]), + renderer(), + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + "@electron": path.resolve(__dirname, "electron"), + }, + }, + build: { + outDir: "dist", + }, +}) +``` + +--- + +## Document History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.0 | Dec 2025 | Team | Initial migration plan | + +--- + +**Next Steps**: +1. Review and approve this plan +2. Wait for `feature/worktrees` branch merge +3. Create `feature/vite-migration` branch +4. Begin Phase 1 implementation From 5136c32b68fe961d39a998f0b18aa23440ee0c97 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 17 Dec 2025 20:11:16 +0100 Subject: [PATCH 02/52] refactor: move from next js to vite and tanstack router --- apps/app/electron/.eslintrc.js | 5 - apps/app/electron/preload.js | 37 - apps/app/eslint.config.mjs | 20 - apps/app/next.config.ts | 7 - apps/app/postcss.config.mjs | 7 - apps/app/src/app/api/claude/test/route.ts | 97 - apps/app/src/app/api/gemini/test/route.ts | 191 - apps/app/src/app/favicon.ico | Bin 25931 -> 0 bytes apps/app/src/app/layout.tsx | 26 - apps/app/src/app/page.tsx | 255 - apps/app/tsconfig.json | 34 - apps/{app => ui}/.gitignore | 18 +- apps/{app => ui}/components.json | 0 apps/{app => ui}/docs/AGENT_ARCHITECTURE.md | 0 apps/{app => ui}/docs/SESSION_MANAGEMENT.md | 0 apps/ui/eslint.config.mjs | 36 + apps/ui/index.html | 31 + apps/{app => ui}/package.json | 50 +- apps/{app => ui}/playwright.config.ts | 8 +- apps/{app => ui}/public/automaker.svg | 0 apps/{app => ui}/public/file.svg | 0 apps/{app => ui}/public/globe.svg | 0 apps/{app => ui}/public/icon.ico | Bin apps/{app => ui}/public/logo.png | Bin apps/{app => ui}/public/logo_larger.png | Bin apps/{app => ui}/public/next.svg | 0 apps/{app => ui}/public/readme_logo.png | Bin apps/{app => ui}/public/sounds/ding.mp3 | Bin apps/{app => ui}/public/vercel.svg | 0 apps/{app => ui}/public/window.svg | 0 apps/{app => ui}/scripts/prepare-server.js | 0 .../scripts/rebuild-server-natives.js | 0 .../{app => ui}/scripts/setup-e2e-fixtures.js | 0 apps/ui/src/App.tsx | 7 + .../delete-all-archived-sessions-dialog.tsx | 1 - .../src/components/delete-session-dialog.tsx | 0 .../dialogs/board-background-modal.tsx | 3 +- .../dialogs/file-browser-dialog.tsx | 3 +- .../layout/project-setup-dialog.tsx | 1 - .../src/components/layout/sidebar.tsx | 42 +- .../src/components/new-project-modal.tsx | 1 - .../src/components/session-manager.tsx | 1 - .../src/components/ui/accordion.tsx | 1 - .../src/components/ui/autocomplete.tsx | 1 - apps/{app => ui}/src/components/ui/badge.tsx | 0 .../src/components/ui/branch-autocomplete.tsx | 1 - apps/{app => ui}/src/components/ui/button.tsx | 0 apps/{app => ui}/src/components/ui/card.tsx | 0 .../components/ui/category-autocomplete.tsx | 1 - .../src/components/ui/checkbox.tsx | 1 - .../{app => ui}/src/components/ui/command.tsx | 1 - .../src/components/ui/count-up-timer.tsx | 1 - .../src/components/ui/course-promo-badge.tsx | 1 - .../components/ui/delete-confirm-dialog.tsx | 0 .../ui/description-image-dropzone.tsx | 3 +- apps/{app => ui}/src/components/ui/dialog.tsx | 1 - .../src/components/ui/dropdown-menu.tsx | 1 - .../components/ui/feature-image-upload.tsx | 1 - .../src/components/ui/git-diff-panel.tsx | 1 - .../src/components/ui/hotkey-button.tsx | 1 - .../src/components/ui/image-drop-zone.tsx | 1 - apps/{app => ui}/src/components/ui/input.tsx | 0 .../src/components/ui/keyboard-map.tsx | 1 - apps/{app => ui}/src/components/ui/label.tsx | 1 - .../src/components/ui/log-viewer.tsx | 1 - .../src/components/ui/markdown.tsx | 1 - .../{app => ui}/src/components/ui/popover.tsx | 1 - apps/{app => ui}/src/components/ui/sheet.tsx | 1 - apps/{app => ui}/src/components/ui/slider.tsx | 1 - apps/{app => ui}/src/components/ui/tabs.tsx | 1 - .../src/components/ui/textarea.tsx | 0 .../{app => ui}/src/components/ui/tooltip.tsx | 1 - .../src/components/ui/xml-syntax-editor.tsx | 1 - .../src/components/views/agent-tools-view.tsx | 1 - .../src/components/views/agent-view.tsx | 1 - .../src/components/views/analysis-view.tsx | 1 - .../src/components/views/board-view.tsx | 1 - .../views/board-view/board-controls.tsx | 1 - .../views/board-view/board-header.tsx | 1 - .../views/board-view/board-search-bar.tsx | 1 - .../views/board-view/components/index.ts | 0 .../board-view/components/kanban-card.tsx | 1 - .../board-view/components/kanban-column.tsx | 1 - .../components/worktree-selector.tsx | 1 - .../components/views/board-view/constants.ts | 0 .../board-view/dialogs/add-feature-dialog.tsx | 5 +- .../board-view/dialogs/agent-output-modal.tsx | 1 - .../dialogs/commit-worktree-dialog.tsx | 1 - .../dialogs/completed-features-modal.tsx | 1 - .../dialogs/create-branch-dialog.tsx | 1 - .../board-view/dialogs/create-pr-dialog.tsx | 1 - .../dialogs/create-worktree-dialog.tsx | 1 - .../dialogs/delete-all-verified-dialog.tsx | 1 - .../delete-completed-feature-dialog.tsx | 1 - .../dialogs/delete-worktree-dialog.tsx | 1 - .../dialogs/dependency-tree-dialog.tsx | 1 - .../dialogs/edit-feature-dialog.tsx | 1 - .../dialogs/feature-suggestions-dialog.tsx | 1 - .../board-view/dialogs/follow-up-dialog.tsx | 1 - .../views/board-view/dialogs/index.ts | 0 .../views/board-view/hooks/index.ts | 0 .../board-view/hooks/use-board-actions.ts | 0 .../board-view/hooks/use-board-background.ts | 2 +- .../hooks/use-board-column-features.ts | 0 .../board-view/hooks/use-board-drag-drop.ts | 0 .../board-view/hooks/use-board-effects.ts | 0 .../board-view/hooks/use-board-features.ts | 0 .../hooks/use-board-keyboard-shortcuts.ts | 0 .../board-view/hooks/use-board-persistence.ts | 0 .../board-view/hooks/use-follow-up-state.ts | 0 .../board-view/hooks/use-suggestions-state.ts | 0 .../views/board-view/kanban-board.tsx | 1 - .../views/board-view/shared/index.ts | 0 .../board-view/shared/model-constants.ts | 0 .../board-view/shared/model-selector.tsx | 1 - .../board-view/shared/priority-selector.tsx | 1 - .../shared/profile-quick-select.tsx | 1 - .../board-view/shared/testing-tab-content.tsx | 1 - .../shared/thinking-level-selector.tsx | 1 - .../src/components/views/chat-history.tsx | 1 - .../src/components/views/code-view.tsx | 1 - .../src/components/views/context-view.tsx | 1 - .../src/components/views/interview-view.tsx | 7 +- .../src/components/views/profiles-view.tsx | 1 - .../views/profiles-view/components/index.ts | 0 .../profiles-view/components/profile-form.tsx | 1 - .../components/profiles-header.tsx | 0 .../components/sortable-profile-card.tsx | 0 .../views/profiles-view/constants.ts | 0 .../components/views/profiles-view/utils.ts | 0 .../components/views/running-agents-view.tsx | 9 +- .../src/components/views/settings-view.tsx | 1 - .../ai-enhancement/ai-enhancement-section.tsx | 0 .../settings-view/ai-enhancement/index.ts | 0 .../settings-view/api-keys/api-key-field.tsx | 0 .../api-keys/api-keys-section.tsx | 5 +- .../authentication-status-display.tsx | 0 .../api-keys/hooks/use-api-key-management.ts | 0 .../api-keys/security-notice.tsx | 0 .../appearance/appearance-section.tsx | 0 .../settings-view/audio/audio-section.tsx | 0 .../cli-status/claude-cli-status.tsx | 0 .../components/delete-project-dialog.tsx | 0 .../components/keyboard-map-dialog.tsx | 0 .../components/settings-header.tsx | 0 .../components/settings-navigation.tsx | 0 .../views/settings-view/config/navigation.ts | 0 .../danger-zone/danger-zone-section.tsx | 0 .../feature-defaults-section.tsx | 0 .../views/settings-view/hooks/index.ts | 0 .../settings-view/hooks/use-cli-status.ts | 0 .../settings-view/hooks/use-settings-view.ts | 0 .../keyboard-shortcuts-section.tsx | 0 .../views/settings-view/shared/types.ts | 0 .../src/components/views/setup-view.tsx | 8 +- .../components/auth-method-selector.tsx | 0 .../components/cli-installation-card.tsx | 0 .../components/copyable-command-field.tsx | 0 .../views/setup-view/components/index.ts | 0 .../components/ready-state-card.tsx | 0 .../setup-view/components/status-badge.tsx | 0 .../setup-view/components/status-row.tsx | 0 .../setup-view/components/step-indicator.tsx | 0 .../setup-view/components/terminal-output.tsx | 0 .../views/setup-view/dialogs/index.ts | 0 .../views/setup-view/hooks/index.ts | 0 .../setup-view/hooks/use-cli-installation.ts | 0 .../views/setup-view/hooks/use-cli-status.ts | 0 .../views/setup-view/hooks/use-token-save.ts | 0 .../setup-view/steps/claude-setup-step.tsx | 1 - .../views/setup-view/steps/complete-step.tsx | 0 .../setup-view/steps/github-setup-step.tsx | 1 - .../views/setup-view/steps/index.ts | 0 .../views/setup-view/steps/welcome-step.tsx | 0 .../src/components/views/spec-view.tsx | 1 - .../src/components/views/terminal-view.tsx | 3 +- .../views/terminal-view/terminal-panel.tsx | 3 +- .../src/components/views/welcome-view.tsx | 10 +- .../src/components/views/wiki-view.tsx | 1 - .../src/components/workspace-picker-modal.tsx | 1 - apps/{app => ui}/src/config/api-providers.ts | 0 apps/{app => ui}/src/config/app-config.ts | 0 apps/{app => ui}/src/config/model-config.ts | 0 .../{app => ui}/src/config/terminal-themes.ts | 0 apps/{app => ui}/src/config/theme-options.ts | 0 .../src/contexts/file-browser-context.tsx | 1 - apps/{app => ui}/src/hooks/use-auto-mode.ts | 0 .../src/hooks/use-electron-agent.ts | 0 .../src/hooks/use-keyboard-shortcuts.ts | 1 - .../src/hooks/use-message-queue.ts | 0 .../src/hooks/use-scroll-tracking.ts | 0 .../{app => ui}/src/hooks/use-window-state.ts | 0 .../src/lib/agent-context-parser.ts | 0 apps/{app => ui}/src/lib/electron.ts | 6 +- apps/{app => ui}/src/lib/file-picker.ts | 0 apps/{app => ui}/src/lib/http-api-client.ts | 4 +- apps/{app => ui}/src/lib/log-parser.ts | 0 apps/{app => ui}/src/lib/project-init.ts | 0 apps/{app => ui}/src/lib/templates.ts | 0 apps/{app => ui}/src/lib/utils.ts | 0 apps/{app/electron/main.js => ui/src/main.ts} | 159 +- apps/ui/src/preload.ts | 42 + apps/ui/src/renderer.tsx | 9 + apps/ui/src/routes/__root.tsx | 146 + apps/ui/src/routes/agent.tsx | 6 + apps/ui/src/routes/board.tsx | 6 + apps/ui/src/routes/context.tsx | 6 + apps/ui/src/routes/index.tsx | 6 + apps/ui/src/routes/interview.tsx | 6 + apps/ui/src/routes/profiles.tsx | 6 + apps/ui/src/routes/running-agents.tsx | 6 + apps/ui/src/routes/settings.tsx | 6 + apps/ui/src/routes/setup.tsx | 6 + apps/ui/src/routes/spec.tsx | 6 + apps/ui/src/routes/terminal.tsx | 6 + apps/ui/src/routes/wiki.tsx | 6 + apps/{app => ui}/src/store/app-store.ts | 0 apps/{app => ui}/src/store/setup-store.ts | 2 +- .../globals.css => ui/src/styles/global.css} | 0 apps/{app => ui}/src/types/css.d.ts | 0 apps/{app => ui}/src/types/electron.d.ts | 0 apps/{app => ui}/src/types/session.ts | 0 apps/ui/src/utils/router.ts | 14 + apps/{app => ui}/tests/context-view.spec.ts | 0 .../tests/feature-lifecycle.spec.ts | 0 apps/{app => ui}/tests/profiles-view.spec.ts | 0 .../tests/spec-editor-persistence.spec.ts | 0 apps/{app => ui}/tests/utils/api/client.ts | 0 .../tests/utils/components/autocomplete.ts | 0 .../tests/utils/components/dialogs.ts | 0 .../tests/utils/components/modals.ts | 0 .../tests/utils/components/toasts.ts | 0 .../{app => ui}/tests/utils/core/constants.ts | 0 apps/{app => ui}/tests/utils/core/elements.ts | 0 .../tests/utils/core/interactions.ts | 0 apps/{app => ui}/tests/utils/core/waiting.ts | 0 .../tests/utils/features/kanban.ts | 0 .../tests/utils/features/skip-tests.ts | 0 .../tests/utils/features/timers.ts | 0 .../tests/utils/features/waiting-approval.ts | 0 .../tests/utils/files/drag-drop.ts | 0 apps/{app => ui}/tests/utils/git/worktree.ts | 0 .../tests/utils/helpers/concurrency.ts | 0 .../tests/utils/helpers/log-viewer.ts | 0 .../{app => ui}/tests/utils/helpers/scroll.ts | 0 apps/{app => ui}/tests/utils/index.ts | 0 .../tests/utils/navigation/views.ts | 0 .../tests/utils/project/fixtures.ts | 0 apps/{app => ui}/tests/utils/project/setup.ts | 0 apps/{app => ui}/tests/utils/views/agent.ts | 0 apps/{app => ui}/tests/utils/views/board.ts | 0 apps/{app => ui}/tests/utils/views/context.ts | 0 .../{app => ui}/tests/utils/views/profiles.ts | 0 .../{app => ui}/tests/utils/views/settings.ts | 0 apps/{app => ui}/tests/utils/views/setup.ts | 0 .../tests/utils/views/spec-editor.ts | 0 .../tests/worktree-integration.spec.ts | 0 apps/ui/tsconfig.json | 22 + apps/ui/vite.config.mts | 57 + docs/migration-plan-nextjs-to-vite.md | 4 +- init.sh | 6 +- package-lock.json | 19851 ++++++++-------- package.json | 28 +- 263 files changed, 11148 insertions(+), 10276 deletions(-) delete mode 100644 apps/app/electron/.eslintrc.js delete mode 100644 apps/app/electron/preload.js delete mode 100644 apps/app/eslint.config.mjs delete mode 100644 apps/app/next.config.ts delete mode 100644 apps/app/postcss.config.mjs delete mode 100644 apps/app/src/app/api/claude/test/route.ts delete mode 100644 apps/app/src/app/api/gemini/test/route.ts delete mode 100644 apps/app/src/app/favicon.ico delete mode 100644 apps/app/src/app/layout.tsx delete mode 100644 apps/app/src/app/page.tsx delete mode 100644 apps/app/tsconfig.json rename apps/{app => ui}/.gitignore (86%) rename apps/{app => ui}/components.json (100%) rename apps/{app => ui}/docs/AGENT_ARCHITECTURE.md (100%) rename apps/{app => ui}/docs/SESSION_MANAGEMENT.md (100%) create mode 100644 apps/ui/eslint.config.mjs create mode 100644 apps/ui/index.html rename apps/{app => ui}/package.json (77%) rename apps/{app => ui}/playwright.config.ts (90%) rename apps/{app => ui}/public/automaker.svg (100%) rename apps/{app => ui}/public/file.svg (100%) rename apps/{app => ui}/public/globe.svg (100%) rename apps/{app => ui}/public/icon.ico (100%) rename apps/{app => ui}/public/logo.png (100%) rename apps/{app => ui}/public/logo_larger.png (100%) rename apps/{app => ui}/public/next.svg (100%) rename apps/{app => ui}/public/readme_logo.png (100%) rename apps/{app => ui}/public/sounds/ding.mp3 (100%) rename apps/{app => ui}/public/vercel.svg (100%) rename apps/{app => ui}/public/window.svg (100%) rename apps/{app => ui}/scripts/prepare-server.js (100%) rename apps/{app => ui}/scripts/rebuild-server-natives.js (100%) rename apps/{app => ui}/scripts/setup-e2e-fixtures.js (100%) create mode 100644 apps/ui/src/App.tsx rename apps/{app => ui}/src/components/delete-all-archived-sessions-dialog.tsx (99%) rename apps/{app => ui}/src/components/delete-session-dialog.tsx (100%) rename apps/{app => ui}/src/components/dialogs/board-background-modal.tsx (99%) rename apps/{app => ui}/src/components/dialogs/file-browser-dialog.tsx (99%) rename apps/{app => ui}/src/components/layout/project-setup-dialog.tsx (99%) rename apps/{app => ui}/src/components/layout/sidebar.tsx (98%) rename apps/{app => ui}/src/components/new-project-modal.tsx (99%) rename apps/{app => ui}/src/components/session-manager.tsx (99%) rename apps/{app => ui}/src/components/ui/accordion.tsx (99%) rename apps/{app => ui}/src/components/ui/autocomplete.tsx (99%) rename apps/{app => ui}/src/components/ui/badge.tsx (100%) rename apps/{app => ui}/src/components/ui/branch-autocomplete.tsx (98%) rename apps/{app => ui}/src/components/ui/button.tsx (100%) rename apps/{app => ui}/src/components/ui/card.tsx (100%) rename apps/{app => ui}/src/components/ui/category-autocomplete.tsx (98%) rename apps/{app => ui}/src/components/ui/checkbox.tsx (99%) rename apps/{app => ui}/src/components/ui/command.tsx (99%) rename apps/{app => ui}/src/components/ui/count-up-timer.tsx (99%) rename apps/{app => ui}/src/components/ui/course-promo-badge.tsx (99%) rename apps/{app => ui}/src/components/ui/delete-confirm-dialog.tsx (100%) rename apps/{app => ui}/src/components/ui/description-image-dropzone.tsx (99%) rename apps/{app => ui}/src/components/ui/dialog.tsx (99%) rename apps/{app => ui}/src/components/ui/dropdown-menu.tsx (99%) rename apps/{app => ui}/src/components/ui/feature-image-upload.tsx (99%) rename apps/{app => ui}/src/components/ui/git-diff-panel.tsx (99%) rename apps/{app => ui}/src/components/ui/hotkey-button.tsx (99%) rename apps/{app => ui}/src/components/ui/image-drop-zone.tsx (99%) rename apps/{app => ui}/src/components/ui/input.tsx (100%) rename apps/{app => ui}/src/components/ui/keyboard-map.tsx (99%) rename apps/{app => ui}/src/components/ui/label.tsx (97%) rename apps/{app => ui}/src/components/ui/log-viewer.tsx (99%) rename apps/{app => ui}/src/components/ui/markdown.tsx (99%) rename apps/{app => ui}/src/components/ui/popover.tsx (99%) rename apps/{app => ui}/src/components/ui/sheet.tsx (99%) rename apps/{app => ui}/src/components/ui/slider.tsx (99%) rename apps/{app => ui}/src/components/ui/tabs.tsx (99%) rename apps/{app => ui}/src/components/ui/textarea.tsx (100%) rename apps/{app => ui}/src/components/ui/tooltip.tsx (99%) rename apps/{app => ui}/src/components/ui/xml-syntax-editor.tsx (99%) rename apps/{app => ui}/src/components/views/agent-tools-view.tsx (99%) rename apps/{app => ui}/src/components/views/agent-view.tsx (99%) rename apps/{app => ui}/src/components/views/analysis-view.tsx (99%) rename apps/{app => ui}/src/components/views/board-view.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/board-controls.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/board-header.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/board-search-bar.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/components/index.ts (100%) rename apps/{app => ui}/src/components/views/board-view/components/kanban-card.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/components/kanban-column.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/components/worktree-selector.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/constants.ts (100%) rename apps/{app => ui}/src/components/views/board-view/dialogs/add-feature-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/agent-output-modal.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/completed-features-modal.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/create-branch-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/create-pr-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/create-worktree-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/delete-all-verified-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/delete-completed-feature-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/dependency-tree-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/edit-feature-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/feature-suggestions-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/follow-up-dialog.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/dialogs/index.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/index.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-actions.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-background.ts (95%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-column-features.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-drag-drop.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-effects.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-features.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-keyboard-shortcuts.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-board-persistence.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-follow-up-state.ts (100%) rename apps/{app => ui}/src/components/views/board-view/hooks/use-suggestions-state.ts (100%) rename apps/{app => ui}/src/components/views/board-view/kanban-board.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/shared/index.ts (100%) rename apps/{app => ui}/src/components/views/board-view/shared/model-constants.ts (100%) rename apps/{app => ui}/src/components/views/board-view/shared/model-selector.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/shared/priority-selector.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/shared/profile-quick-select.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/shared/testing-tab-content.tsx (99%) rename apps/{app => ui}/src/components/views/board-view/shared/thinking-level-selector.tsx (99%) rename apps/{app => ui}/src/components/views/chat-history.tsx (99%) rename apps/{app => ui}/src/components/views/code-view.tsx (99%) rename apps/{app => ui}/src/components/views/context-view.tsx (99%) rename apps/{app => ui}/src/components/views/interview-view.tsx (99%) rename apps/{app => ui}/src/components/views/profiles-view.tsx (99%) rename apps/{app => ui}/src/components/views/profiles-view/components/index.ts (100%) rename apps/{app => ui}/src/components/views/profiles-view/components/profile-form.tsx (99%) rename apps/{app => ui}/src/components/views/profiles-view/components/profiles-header.tsx (100%) rename apps/{app => ui}/src/components/views/profiles-view/components/sortable-profile-card.tsx (100%) rename apps/{app => ui}/src/components/views/profiles-view/constants.ts (100%) rename apps/{app => ui}/src/components/views/profiles-view/utils.ts (100%) rename apps/{app => ui}/src/components/views/running-agents-view.tsx (97%) rename apps/{app => ui}/src/components/views/settings-view.tsx (99%) rename apps/{app => ui}/src/components/views/settings-view/ai-enhancement/ai-enhancement-section.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/ai-enhancement/index.ts (100%) rename apps/{app => ui}/src/components/views/settings-view/api-keys/api-key-field.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/api-keys/api-keys-section.tsx (97%) rename apps/{app => ui}/src/components/views/settings-view/api-keys/authentication-status-display.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts (100%) rename apps/{app => ui}/src/components/views/settings-view/api-keys/security-notice.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/appearance/appearance-section.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/audio/audio-section.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/cli-status/claude-cli-status.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/components/delete-project-dialog.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/components/keyboard-map-dialog.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/components/settings-header.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/components/settings-navigation.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/config/navigation.ts (100%) rename apps/{app => ui}/src/components/views/settings-view/danger-zone/danger-zone-section.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/hooks/index.ts (100%) rename apps/{app => ui}/src/components/views/settings-view/hooks/use-cli-status.ts (100%) rename apps/{app => ui}/src/components/views/settings-view/hooks/use-settings-view.ts (100%) rename apps/{app => ui}/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx (100%) rename apps/{app => ui}/src/components/views/settings-view/shared/types.ts (100%) rename apps/{app => ui}/src/components/views/setup-view.tsx (95%) rename apps/{app => ui}/src/components/views/setup-view/components/auth-method-selector.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/cli-installation-card.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/copyable-command-field.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/index.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/components/ready-state-card.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/status-badge.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/status-row.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/step-indicator.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/components/terminal-output.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/dialogs/index.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/hooks/index.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/hooks/use-cli-installation.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/hooks/use-cli-status.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/hooks/use-token-save.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/steps/claude-setup-step.tsx (99%) rename apps/{app => ui}/src/components/views/setup-view/steps/complete-step.tsx (100%) rename apps/{app => ui}/src/components/views/setup-view/steps/github-setup-step.tsx (99%) rename apps/{app => ui}/src/components/views/setup-view/steps/index.ts (100%) rename apps/{app => ui}/src/components/views/setup-view/steps/welcome-step.tsx (100%) rename apps/{app => ui}/src/components/views/spec-view.tsx (99%) rename apps/{app => ui}/src/components/views/terminal-view.tsx (99%) rename apps/{app => ui}/src/components/views/terminal-view/terminal-panel.tsx (99%) rename apps/{app => ui}/src/components/views/welcome-view.tsx (99%) rename apps/{app => ui}/src/components/views/wiki-view.tsx (99%) rename apps/{app => ui}/src/components/workspace-picker-modal.tsx (99%) rename apps/{app => ui}/src/config/api-providers.ts (100%) rename apps/{app => ui}/src/config/app-config.ts (100%) rename apps/{app => ui}/src/config/model-config.ts (100%) rename apps/{app => ui}/src/config/terminal-themes.ts (100%) rename apps/{app => ui}/src/config/theme-options.ts (100%) rename apps/{app => ui}/src/contexts/file-browser-context.tsx (99%) rename apps/{app => ui}/src/hooks/use-auto-mode.ts (100%) rename apps/{app => ui}/src/hooks/use-electron-agent.ts (100%) rename apps/{app => ui}/src/hooks/use-keyboard-shortcuts.ts (99%) rename apps/{app => ui}/src/hooks/use-message-queue.ts (100%) rename apps/{app => ui}/src/hooks/use-scroll-tracking.ts (100%) rename apps/{app => ui}/src/hooks/use-window-state.ts (100%) rename apps/{app => ui}/src/lib/agent-context-parser.ts (100%) rename apps/{app => ui}/src/lib/electron.ts (99%) rename apps/{app => ui}/src/lib/file-picker.ts (100%) rename apps/{app => ui}/src/lib/http-api-client.ts (99%) rename apps/{app => ui}/src/lib/log-parser.ts (100%) rename apps/{app => ui}/src/lib/project-init.ts (100%) rename apps/{app => ui}/src/lib/templates.ts (100%) rename apps/{app => ui}/src/lib/utils.ts (100%) rename apps/{app/electron/main.js => ui/src/main.ts} (69%) create mode 100644 apps/ui/src/preload.ts create mode 100644 apps/ui/src/renderer.tsx create mode 100644 apps/ui/src/routes/__root.tsx create mode 100644 apps/ui/src/routes/agent.tsx create mode 100644 apps/ui/src/routes/board.tsx create mode 100644 apps/ui/src/routes/context.tsx create mode 100644 apps/ui/src/routes/index.tsx create mode 100644 apps/ui/src/routes/interview.tsx create mode 100644 apps/ui/src/routes/profiles.tsx create mode 100644 apps/ui/src/routes/running-agents.tsx create mode 100644 apps/ui/src/routes/settings.tsx create mode 100644 apps/ui/src/routes/setup.tsx create mode 100644 apps/ui/src/routes/spec.tsx create mode 100644 apps/ui/src/routes/terminal.tsx create mode 100644 apps/ui/src/routes/wiki.tsx rename apps/{app => ui}/src/store/app-store.ts (100%) rename apps/{app => ui}/src/store/setup-store.ts (98%) rename apps/{app/src/app/globals.css => ui/src/styles/global.css} (100%) rename apps/{app => ui}/src/types/css.d.ts (100%) rename apps/{app => ui}/src/types/electron.d.ts (100%) rename apps/{app => ui}/src/types/session.ts (100%) create mode 100644 apps/ui/src/utils/router.ts rename apps/{app => ui}/tests/context-view.spec.ts (100%) rename apps/{app => ui}/tests/feature-lifecycle.spec.ts (100%) rename apps/{app => ui}/tests/profiles-view.spec.ts (100%) rename apps/{app => ui}/tests/spec-editor-persistence.spec.ts (100%) rename apps/{app => ui}/tests/utils/api/client.ts (100%) rename apps/{app => ui}/tests/utils/components/autocomplete.ts (100%) rename apps/{app => ui}/tests/utils/components/dialogs.ts (100%) rename apps/{app => ui}/tests/utils/components/modals.ts (100%) rename apps/{app => ui}/tests/utils/components/toasts.ts (100%) rename apps/{app => ui}/tests/utils/core/constants.ts (100%) rename apps/{app => ui}/tests/utils/core/elements.ts (100%) rename apps/{app => ui}/tests/utils/core/interactions.ts (100%) rename apps/{app => ui}/tests/utils/core/waiting.ts (100%) rename apps/{app => ui}/tests/utils/features/kanban.ts (100%) rename apps/{app => ui}/tests/utils/features/skip-tests.ts (100%) rename apps/{app => ui}/tests/utils/features/timers.ts (100%) rename apps/{app => ui}/tests/utils/features/waiting-approval.ts (100%) rename apps/{app => ui}/tests/utils/files/drag-drop.ts (100%) rename apps/{app => ui}/tests/utils/git/worktree.ts (100%) rename apps/{app => ui}/tests/utils/helpers/concurrency.ts (100%) rename apps/{app => ui}/tests/utils/helpers/log-viewer.ts (100%) rename apps/{app => ui}/tests/utils/helpers/scroll.ts (100%) rename apps/{app => ui}/tests/utils/index.ts (100%) rename apps/{app => ui}/tests/utils/navigation/views.ts (100%) rename apps/{app => ui}/tests/utils/project/fixtures.ts (100%) rename apps/{app => ui}/tests/utils/project/setup.ts (100%) rename apps/{app => ui}/tests/utils/views/agent.ts (100%) rename apps/{app => ui}/tests/utils/views/board.ts (100%) rename apps/{app => ui}/tests/utils/views/context.ts (100%) rename apps/{app => ui}/tests/utils/views/profiles.ts (100%) rename apps/{app => ui}/tests/utils/views/settings.ts (100%) rename apps/{app => ui}/tests/utils/views/setup.ts (100%) rename apps/{app => ui}/tests/utils/views/spec-editor.ts (100%) rename apps/{app => ui}/tests/worktree-integration.spec.ts (100%) create mode 100644 apps/ui/tsconfig.json create mode 100644 apps/ui/vite.config.mts 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 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m 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 29a74578..00000000 --- a/apps/app/src/app/page.tsx +++ /dev/null @@ -1,255 +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; - root.classList.remove( - "dark", - "retro", - "light", - "dracula", - "nord", - "monokai", - "tokyonight", - "solarized", - "gruvbox", - "catppuccin", - "onedark", - "synthwave", - "red" - ); - - if (effectiveTheme === "dark") { - root.classList.add("dark"); - } else if (effectiveTheme === "retro") { - root.classList.add("retro"); - } else if (effectiveTheme === "dracula") { - root.classList.add("dracula"); - } else if (effectiveTheme === "nord") { - root.classList.add("nord"); - } else if (effectiveTheme === "monokai") { - root.classList.add("monokai"); - } else if (effectiveTheme === "tokyonight") { - root.classList.add("tokyonight"); - } else if (effectiveTheme === "solarized") { - root.classList.add("solarized"); - } else if (effectiveTheme === "gruvbox") { - root.classList.add("gruvbox"); - } else if (effectiveTheme === "catppuccin") { - root.classList.add("catppuccin"); - } else if (effectiveTheme === "onedark") { - root.classList.add("onedark"); - } else if (effectiveTheme === "synthwave") { - root.classList.add("synthwave"); - } else if (effectiveTheme === "red") { - root.classList.add("red"); - } else if (effectiveTheme === "light") { - root.classList.add("light"); - } else if (effectiveTheme === "system") { - // System theme - const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - if (isDark) { - root.classList.add("dark"); - } else { - root.classList.add("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/tsconfig.json b/apps/app/tsconfig.json deleted file mode 100644 index cf9c65d3..00000000 --- a/apps/app/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts", - "**/*.mts" - ], - "exclude": ["node_modules"] -} diff --git a/apps/app/.gitignore b/apps/ui/.gitignore similarity index 86% rename from apps/app/.gitignore rename to apps/ui/.gitignore index cb9812cb..7ea8a360 100644 --- a/apps/app/.gitignore +++ b/apps/ui/.gitignore @@ -13,12 +13,9 @@ # testing /coverage -# next.js -/.next/ -/out/ - -# production -/build +# Vite +/dist/ +/dist-electron/ # misc .DS_Store @@ -33,12 +30,8 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* -# vercel -.vercel - # typescript *.tsbuildinfo -next-env.d.ts # Playwright /test-results/ @@ -47,5 +40,8 @@ next-env.d.ts /playwright/.cache/ # Electron -/dist/ +/release/ /server-bundle/ + +# TanStack Router generated +src/routeTree.gen.ts diff --git a/apps/app/components.json b/apps/ui/components.json similarity index 100% rename from apps/app/components.json rename to apps/ui/components.json diff --git a/apps/app/docs/AGENT_ARCHITECTURE.md b/apps/ui/docs/AGENT_ARCHITECTURE.md similarity index 100% rename from apps/app/docs/AGENT_ARCHITECTURE.md rename to apps/ui/docs/AGENT_ARCHITECTURE.md diff --git a/apps/app/docs/SESSION_MANAGEMENT.md b/apps/ui/docs/SESSION_MANAGEMENT.md similarity index 100% rename from apps/app/docs/SESSION_MANAGEMENT.md rename to apps/ui/docs/SESSION_MANAGEMENT.md diff --git a/apps/ui/eslint.config.mjs b/apps/ui/eslint.config.mjs new file mode 100644 index 00000000..150f0bad --- /dev/null +++ b/apps/ui/eslint.config.mjs @@ -0,0 +1,36 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; + +const eslintConfig = defineConfig([ + js.configs.recommended, + { + files: ["**/*.ts", "**/*.tsx"], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + }, + plugins: { + "@typescript-eslint": ts, + }, + rules: { + ...ts.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "warn", + }, + }, + globalIgnores([ + "dist/**", + "dist-electron/**", + "node_modules/**", + "server-bundle/**", + "release/**", + "src/routeTree.gen.ts", + ]), +]); + +export default eslintConfig; diff --git a/apps/ui/index.html b/apps/ui/index.html new file mode 100644 index 00000000..02e2e0be --- /dev/null +++ b/apps/ui/index.html @@ -0,0 +1,31 @@ + + + + + Automaker - Autonomous AI Development Studio + + + + + + +
+ + + diff --git a/apps/app/package.json b/apps/ui/package.json similarity index 77% rename from apps/app/package.json rename to apps/ui/package.json index ad9100db..2bb464ed 100644 --- a/apps/app/package.json +++ b/apps/ui/package.json @@ -1,5 +1,5 @@ { - "name": "@automaker/app", + "name": "@automaker/ui", "version": "0.1.0", "description": "An autonomous AI development studio that helps you build software faster using AI-powered agents", "homepage": "https://github.com/AutoMaker-Org/automaker", @@ -13,25 +13,25 @@ }, "private": true, "license": "Unlicense", - "main": "electron/main.js", + "main": "dist-electron/main.js", "scripts": { - "dev": "next dev -p 3007", - "dev:web": "next dev -p 3007", - "dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"", - "dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"", - "build": "next build", - "build:electron": "node scripts/prepare-server.js && next build && electron-builder", - "build:electron:win": "node scripts/prepare-server.js && next build && electron-builder --win", - "build:electron:mac": "node scripts/prepare-server.js && next build && electron-builder --mac", - "build:electron:linux": "node scripts/prepare-server.js && next build && electron-builder --linux", + "dev": "vite", + "dev:web": "vite", + "dev:electron": "vite", + "dev:electron:debug": "cross-env OPEN_DEVTOOLS=true vite", + "build": "vite build", + "build:electron": "node scripts/prepare-server.js && vite build && electron-builder", + "build:electron:win": "node scripts/prepare-server.js && vite build && electron-builder --win", + "build:electron:mac": "node scripts/prepare-server.js && vite build && electron-builder --mac", + "build:electron:linux": "node scripts/prepare-server.js && vite build && electron-builder --linux", "postinstall": "electron-builder install-app-deps", - "start": "next start", + "preview": "vite preview", "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\"" + "dev:electron:wsl": "cross-env vite", + "dev:electron:wsl:gpu": "cross-env MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA vite" }, "dependencies": { "@codemirror/lang-xml": "^6.1.0", @@ -50,6 +50,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.12", + "@tanstack/react-router": "^1.132.41", "@uiw/react-codemirror": "^4.25.4", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-webgl": "^0.18.0", @@ -60,7 +61,6 @@ "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", @@ -82,20 +82,26 @@ }, "devDependencies": { "@electron/rebuild": "^4.0.2", + "@eslint/js": "^9.0.0", "@playwright/test": "^1.57.0", - "@tailwindcss/postcss": "^4", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/router-plugin": "^1.132.41", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "concurrently": "^9.2.1", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.5.2", + "cross-env": "^7.0.3", "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" + "vite": "^6.3.5", + "vite-plugin-electron": "^0.29.0", + "vite-plugin-electron-renderer": "^0.14.6" }, "build": { "appId": "com.automaker.app", @@ -103,11 +109,11 @@ "artifactName": "${productName}-${version}-${arch}.${ext}", "afterPack": "./scripts/rebuild-server-natives.js", "directories": { - "output": "dist" + "output": "release" }, "files": [ - "electron/**/*", - "out/**/*", + "dist/**/*", + "dist-electron/**/*", "public/**/*", "!node_modules/**/*" ], diff --git a/apps/app/playwright.config.ts b/apps/ui/playwright.config.ts similarity index 90% rename from apps/app/playwright.config.ts rename to apps/ui/playwright.config.ts index 26f06499..9ffead44 100644 --- a/apps/app/playwright.config.ts +++ b/apps/ui/playwright.config.ts @@ -1,6 +1,6 @@ import { defineConfig, devices } from "@playwright/test"; -const port = process.env.TEST_PORT || 3007; +const port = process.env.TEST_PORT || 5173; 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"; @@ -43,15 +43,15 @@ export default defineConfig({ ALLOWED_PROJECT_DIRS: "/Users,/home,/tmp,/var/folders", }, }, - // Frontend Next.js server + // Frontend Vite dev server { - command: `npx next dev -p ${port}`, + command: `npm run dev`, url: `http://localhost:${port}`, reuseExistingServer: true, timeout: 120000, env: { ...process.env, - NEXT_PUBLIC_SKIP_SETUP: "true", + VITE_SKIP_SETUP: "true", }, }, ], diff --git a/apps/app/public/automaker.svg b/apps/ui/public/automaker.svg similarity index 100% rename from apps/app/public/automaker.svg rename to apps/ui/public/automaker.svg diff --git a/apps/app/public/file.svg b/apps/ui/public/file.svg similarity index 100% rename from apps/app/public/file.svg rename to apps/ui/public/file.svg diff --git a/apps/app/public/globe.svg b/apps/ui/public/globe.svg similarity index 100% rename from apps/app/public/globe.svg rename to apps/ui/public/globe.svg diff --git a/apps/app/public/icon.ico b/apps/ui/public/icon.ico similarity index 100% rename from apps/app/public/icon.ico rename to apps/ui/public/icon.ico diff --git a/apps/app/public/logo.png b/apps/ui/public/logo.png similarity index 100% rename from apps/app/public/logo.png rename to apps/ui/public/logo.png diff --git a/apps/app/public/logo_larger.png b/apps/ui/public/logo_larger.png similarity index 100% rename from apps/app/public/logo_larger.png rename to apps/ui/public/logo_larger.png diff --git a/apps/app/public/next.svg b/apps/ui/public/next.svg similarity index 100% rename from apps/app/public/next.svg rename to apps/ui/public/next.svg diff --git a/apps/app/public/readme_logo.png b/apps/ui/public/readme_logo.png similarity index 100% rename from apps/app/public/readme_logo.png rename to apps/ui/public/readme_logo.png diff --git a/apps/app/public/sounds/ding.mp3 b/apps/ui/public/sounds/ding.mp3 similarity index 100% rename from apps/app/public/sounds/ding.mp3 rename to apps/ui/public/sounds/ding.mp3 diff --git a/apps/app/public/vercel.svg b/apps/ui/public/vercel.svg similarity index 100% rename from apps/app/public/vercel.svg rename to apps/ui/public/vercel.svg diff --git a/apps/app/public/window.svg b/apps/ui/public/window.svg similarity index 100% rename from apps/app/public/window.svg rename to apps/ui/public/window.svg diff --git a/apps/app/scripts/prepare-server.js b/apps/ui/scripts/prepare-server.js similarity index 100% rename from apps/app/scripts/prepare-server.js rename to apps/ui/scripts/prepare-server.js diff --git a/apps/app/scripts/rebuild-server-natives.js b/apps/ui/scripts/rebuild-server-natives.js similarity index 100% rename from apps/app/scripts/rebuild-server-natives.js rename to apps/ui/scripts/rebuild-server-natives.js diff --git a/apps/app/scripts/setup-e2e-fixtures.js b/apps/ui/scripts/setup-e2e-fixtures.js similarity index 100% rename from apps/app/scripts/setup-e2e-fixtures.js rename to apps/ui/scripts/setup-e2e-fixtures.js diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx new file mode 100644 index 00000000..a38bfb42 --- /dev/null +++ b/apps/ui/src/App.tsx @@ -0,0 +1,7 @@ +import { RouterProvider } from "@tanstack/react-router"; +import { router } from "./utils/router"; +import "./styles/global.css"; + +export default function App() { + return ; +} diff --git a/apps/app/src/components/delete-all-archived-sessions-dialog.tsx b/apps/ui/src/components/delete-all-archived-sessions-dialog.tsx similarity index 99% rename from apps/app/src/components/delete-all-archived-sessions-dialog.tsx rename to apps/ui/src/components/delete-all-archived-sessions-dialog.tsx index 34d5907a..66b0bae6 100644 --- a/apps/app/src/components/delete-all-archived-sessions-dialog.tsx +++ b/apps/ui/src/components/delete-all-archived-sessions-dialog.tsx @@ -1,4 +1,3 @@ -"use client"; import { Dialog, diff --git a/apps/app/src/components/delete-session-dialog.tsx b/apps/ui/src/components/delete-session-dialog.tsx similarity index 100% rename from apps/app/src/components/delete-session-dialog.tsx rename to apps/ui/src/components/delete-session-dialog.tsx diff --git a/apps/app/src/components/dialogs/board-background-modal.tsx b/apps/ui/src/components/dialogs/board-background-modal.tsx similarity index 99% rename from apps/app/src/components/dialogs/board-background-modal.tsx rename to apps/ui/src/components/dialogs/board-background-modal.tsx index ad1207eb..bf3ccbd4 100644 --- a/apps/app/src/components/dialogs/board-background-modal.tsx +++ b/apps/ui/src/components/dialogs/board-background-modal.tsx @@ -1,4 +1,3 @@ -"use client"; import { useState, useRef, useCallback, useEffect } from "react"; import { ImageIcon, Upload, Loader2, Trash2 } from "lucide-react"; @@ -72,7 +71,7 @@ export function BoardBackgroundModal({ useEffect(() => { if (currentProject && backgroundSettings.imagePath) { const serverUrl = - process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008"; + import.meta.env.VITE_SERVER_URL || "http://localhost:3008"; // Add cache-busting query parameter to force browser to reload image const cacheBuster = imageVersion ? `&v=${imageVersion}` diff --git a/apps/app/src/components/dialogs/file-browser-dialog.tsx b/apps/ui/src/components/dialogs/file-browser-dialog.tsx similarity index 99% rename from apps/app/src/components/dialogs/file-browser-dialog.tsx rename to apps/ui/src/components/dialogs/file-browser-dialog.tsx index 351534d5..2103b622 100644 --- a/apps/app/src/components/dialogs/file-browser-dialog.tsx +++ b/apps/ui/src/components/dialogs/file-browser-dialog.tsx @@ -1,4 +1,3 @@ -"use client"; import { useState, useEffect, useRef } from "react"; import { @@ -71,7 +70,7 @@ export function FileBrowserDialog({ try { // Get server URL from environment or default const serverUrl = - process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008"; + import.meta.env.VITE_SERVER_URL || "http://localhost:3008"; const response = await fetch(`${serverUrl}/api/fs/browse`, { method: "POST", diff --git a/apps/app/src/components/layout/project-setup-dialog.tsx b/apps/ui/src/components/layout/project-setup-dialog.tsx similarity index 99% rename from apps/app/src/components/layout/project-setup-dialog.tsx rename to apps/ui/src/components/layout/project-setup-dialog.tsx index 82453203..d054cd0c 100644 --- a/apps/app/src/components/layout/project-setup-dialog.tsx +++ b/apps/ui/src/components/layout/project-setup-dialog.tsx @@ -1,4 +1,3 @@ -"use client"; import { Sparkles, Clock } from "lucide-react"; import { diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar.tsx similarity index 98% rename from apps/app/src/components/layout/sidebar.tsx rename to apps/ui/src/components/layout/sidebar.tsx index 6f534db4..0df24d55 100644 --- a/apps/app/src/components/layout/sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar.tsx @@ -1,6 +1,5 @@ -"use client"; - import { useState, useMemo, useEffect, useCallback, useRef } from "react"; +import { useNavigate, useLocation } from "@tanstack/react-router"; import { cn } from "@/lib/utils"; import { useAppStore, formatShortcut, type ThemeMode } from "@/store/app-store"; import { CoursePromoBadge } from "@/components/ui/course-promo-badge"; @@ -223,16 +222,17 @@ const BugReportButton = ({ }; export function Sidebar() { + const navigate = useNavigate(); + const location = useLocation(); + const { projects, trashedProjects, currentProject, - currentView, sidebarOpen, projectHistory, upsertAndSetCurrentProject, setCurrentProject, - setCurrentView, toggleSidebar, restoreTrashedProject, deleteTrashedProject, @@ -251,14 +251,13 @@ export function Sidebar() { } = useAppStore(); // Environment variable flags for hiding sidebar items - // Note: Next.js requires static access to process.env variables (no dynamic keys) - const hideTerminal = process.env.NEXT_PUBLIC_HIDE_TERMINAL === "true"; - const hideWiki = process.env.NEXT_PUBLIC_HIDE_WIKI === "true"; + const hideTerminal = import.meta.env.VITE_HIDE_TERMINAL === "true"; + const hideWiki = import.meta.env.VITE_HIDE_WIKI === "true"; const hideRunningAgents = - process.env.NEXT_PUBLIC_HIDE_RUNNING_AGENTS === "true"; - const hideContext = process.env.NEXT_PUBLIC_HIDE_CONTEXT === "true"; - const hideSpecEditor = process.env.NEXT_PUBLIC_HIDE_SPEC_EDITOR === "true"; - const hideAiProfiles = process.env.NEXT_PUBLIC_HIDE_AI_PROFILES === "true"; + import.meta.env.VITE_HIDE_RUNNING_AGENTS === "true"; + const hideContext = import.meta.env.VITE_HIDE_CONTEXT === "true"; + const hideSpecEditor = import.meta.env.VITE_HIDE_SPEC_EDITOR === "true"; + const hideAiProfiles = import.meta.env.VITE_HIDE_AI_PROFILES === "true"; // Get customizable keyboard shortcuts const shortcuts = useKeyboardShortcutsConfig(); @@ -429,7 +428,6 @@ export function Sidebar() { unsubscribe(); }; }, [ - setCurrentView, creatingSpecProjectPath, setupProjectPath, setSpecCreatingForProject, @@ -1177,7 +1175,7 @@ export function Sidebar() { if (item.shortcut) { shortcutsList.push({ key: item.shortcut, - action: () => setCurrentView(item.id as any), + action: () => navigate({ to: `/${item.id}` as const }), description: `Navigate to ${item.label}`, }); } @@ -1187,7 +1185,7 @@ export function Sidebar() { // Add settings shortcut shortcutsList.push({ key: shortcuts.settings, - action: () => setCurrentView("settings"), + action: () => navigate({ to: "/settings" }), description: "Navigate to Settings", }); } @@ -1196,7 +1194,7 @@ export function Sidebar() { }, [ shortcuts, currentProject, - setCurrentView, + navigate, toggleSidebar, projects.length, handleOpenFolder, @@ -1210,7 +1208,9 @@ export function Sidebar() { useKeyboardShortcuts(navigationShortcuts); const isActiveRoute = (id: string) => { - return currentView === id; + // Map view IDs to route paths + const routePath = id === "welcome" ? "/" : `/${id}`; + return location.pathname === routePath; }; return ( @@ -1289,7 +1289,7 @@ export function Sidebar() { "flex items-center gap-3 titlebar-no-drag cursor-pointer group", !sidebarOpen && "flex-col gap-1" )} - onClick={() => setCurrentView("welcome")} + onClick={() => navigate({ to: "/" })} data-testid="logo-button" > {!sidebarOpen ? ( @@ -1847,7 +1847,7 @@ export function Sidebar() { return (