# Migration Plan: Next.js to Vite + Electron + TanStack

> **Document Version**: 1.1
> **Date**: December 2025
> **Status**: Phase 2 Complete - Core Migration Done
> **Branch**: refactor/frontend

---

## 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/ui/ (After Migration)

```
apps/ui/
├── 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<string> {
  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<void> {
  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<boolean> {
  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<fs.Stats | null> {
  try {
    return await fs.promises.stat(filePath);
  } catch {
    return null;
  }
}

/**
 * Recursively removes a directory
 */
export async function removeDirectory(dirPath: string): Promise<void> {
  await fs.promises.rm(dirPath, { recursive: true, force: true });
}

/**
 * Copies a file or directory
 */
export async function copy(src: string, dest: string): Promise<void> {
  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<T = unknown> {
  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<OpenDialogOptions>;
    response: DialogResult<string[]>;
  };
  'dialog:openFile': {
    request: Partial<OpenDialogOptions>;
    response: DialogResult<string[]>;
  };
  'dialog:saveFile': {
    request: Partial<SaveDialogOptions>;
    response: DialogResult<string>;
  };

  // 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<T extends IPCChannel> = IPCSchema[T]['request'];
export type IPCResponse<T extends IPCChannel> = 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<FilePickerResult> {
  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<string, string[]>;
}): Promise<FilePickerResult> {
  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.

- [x] Create `vite.config.mts` with electron plugins
- [x] Create `electron/tsconfig.json` for Electron TypeScript
- [x] Convert `electron/main.js` → `electron/main.ts`
- [x] Convert `electron/preload.js` → `electron/preload.ts`
- [ ] Implement IPC schema and type-safe handlers (deferred - using existing HTTP API)
- [x] Set up TanStack Router configuration
- [ ] Port debug console from starter template (deferred)
- [x] Create `index.html` for Vite entry

**Deliverables**:

- [x] Working Vite dev server
- [x] TypeScript Electron main process
- [ ] Debug console functional (deferred)

### Phase 2: Core Migration (Week 3-4)

**Goal**: Replace Next.js with Vite while maintaining feature parity.

- [x] Create `src/renderer.tsx` entry point
- [x] Create `src/App.tsx` root component
- [x] Set up TanStack Router with file-based routes
- [x] Port all views to route files
- [x] Update environment variables (`NEXT_PUBLIC_*` → `VITE_*`)
- [x] Verify Zustand stores work unchanged
- [x] Verify HTTP API client works unchanged
- [x] Test Electron build
- [ ] Test Web build (needs verification)

**Additional completed tasks**:

- [x] Remove all "use client" directives (not needed in Vite)
- [x] Replace all `setCurrentView()` calls with TanStack Router `navigate()`
- [x] Rename `apps/app` to `apps/ui`
- [x] Update package.json scripts
- [x] Configure memory history for Electron (no URL bar)
- [x] Fix ES module imports (replace `require()` with `import`)
- [x] Remove PostCSS config (using `@tailwindcss/vite` plugin)

**Deliverables**:

- [x] All views accessible via TanStack Router
- [x] Electron build functional
- [ ] Web build functional (needs testing)
- [x] 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                                                                |
| 1.1     | Dec 2025 | Team   | Phase 1 & 2 complete. Updated checkboxes, added completed tasks, noted deferred items |

---

**Next Steps**:

1. ~~Review and approve this plan~~ ✅
2. ~~Wait for `feature/worktrees` branch merge~~ ✅
3. ~~Create migration branch~~ ✅ (refactor/frontend)
4. ~~Complete Phase 1 implementation~~ ✅
5. ~~Complete Phase 2 implementation~~ ✅
6. **Current**: Verify web build works, then begin Phase 3 (Component Refactoring)
7. Consider implementing deferred items: Debug console, IPC schema
