diff --git a/apps/app/electron/.eslintrc.js b/apps/app/electron/.eslintrc.js
deleted file mode 100644
index 5c4bdfee..00000000
--- a/apps/app/electron/.eslintrc.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- rules: {
- "@typescript-eslint/no-require-imports": "off",
- },
-};
diff --git a/apps/app/electron/preload.js b/apps/app/electron/preload.js
deleted file mode 100644
index 289d2cd7..00000000
--- a/apps/app/electron/preload.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Simplified Electron preload script
- *
- * Only exposes native features (dialogs, shell) and server URL.
- * All other operations go through HTTP API.
- */
-
-const { contextBridge, ipcRenderer } = require("electron");
-
-// Expose minimal API for native features
-contextBridge.exposeInMainWorld("electronAPI", {
- // Platform info
- platform: process.platform,
- isElectron: true,
-
- // Connection check
- ping: () => ipcRenderer.invoke("ping"),
-
- // Get server URL for HTTP client
- getServerUrl: () => ipcRenderer.invoke("server:getUrl"),
-
- // Native dialogs - better UX than prompt()
- openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
- openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
- saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options),
-
- // Shell operations
- openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
- openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath),
-
- // App info
- getPath: (name) => ipcRenderer.invoke("app:getPath", name),
- getVersion: () => ipcRenderer.invoke("app:getVersion"),
- isPackaged: () => ipcRenderer.invoke("app:isPackaged"),
-});
-
-console.log("[Preload] Electron API exposed (simplified mode)");
diff --git a/apps/app/eslint.config.mjs b/apps/app/eslint.config.mjs
deleted file mode 100644
index 6c419a68..00000000
--- a/apps/app/eslint.config.mjs
+++ /dev/null
@@ -1,20 +0,0 @@
-import { defineConfig, globalIgnores } from "eslint/config";
-import nextVitals from "eslint-config-next/core-web-vitals";
-import nextTs from "eslint-config-next/typescript";
-
-const eslintConfig = defineConfig([
- ...nextVitals,
- ...nextTs,
- // Override default ignores of eslint-config-next.
- globalIgnores([
- // Default ignores of eslint-config-next:
- ".next/**",
- "out/**",
- "build/**",
- "next-env.d.ts",
- // Electron files use CommonJS
- "electron/**",
- ]),
-]);
-
-export default eslintConfig;
diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts
deleted file mode 100644
index 65c102b9..00000000
--- a/apps/app/next.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { NextConfig } from "next";
-
-const nextConfig: NextConfig = {
- output: "export",
-};
-
-export default nextConfig;
diff --git a/apps/app/postcss.config.mjs b/apps/app/postcss.config.mjs
deleted file mode 100644
index 61e36849..00000000
--- a/apps/app/postcss.config.mjs
+++ /dev/null
@@ -1,7 +0,0 @@
-const config = {
- plugins: {
- "@tailwindcss/postcss": {},
- },
-};
-
-export default config;
diff --git a/apps/app/src/app/api/claude/test/route.ts b/apps/app/src/app/api/claude/test/route.ts
deleted file mode 100644
index 95dab4ba..00000000
--- a/apps/app/src/app/api/claude/test/route.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-
-interface AnthropicResponse {
- content?: Array<{ type: string; text?: string }>;
- model?: string;
- error?: { message?: string };
-}
-
-export async function POST(request: NextRequest) {
- try {
- const { apiKey } = await request.json();
-
- // Use provided API key or fall back to environment variable
- const effectiveApiKey = apiKey || process.env.ANTHROPIC_API_KEY;
-
- if (!effectiveApiKey) {
- return NextResponse.json(
- { success: false, error: "No API key provided or configured in environment" },
- { status: 400 }
- );
- }
-
- // Send a simple test prompt to the Anthropic API
- const response = await fetch("https://api.anthropic.com/v1/messages", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "x-api-key": effectiveApiKey,
- "anthropic-version": "2023-06-01",
- },
- body: JSON.stringify({
- model: "claude-sonnet-4-20250514",
- max_tokens: 100,
- messages: [
- {
- role: "user",
- content: "Respond with exactly: 'Claude API connection successful!' and nothing else.",
- },
- ],
- }),
- });
-
- if (!response.ok) {
- const errorData = (await response.json()) as AnthropicResponse;
- const errorMessage = errorData.error?.message || `HTTP ${response.status}`;
-
- if (response.status === 401) {
- return NextResponse.json(
- { success: false, error: "Invalid API key. Please check your Anthropic API key." },
- { status: 401 }
- );
- }
-
- if (response.status === 429) {
- return NextResponse.json(
- { success: false, error: "Rate limit exceeded. Please try again later." },
- { status: 429 }
- );
- }
-
- return NextResponse.json(
- { success: false, error: `API error: ${errorMessage}` },
- { status: response.status }
- );
- }
-
- const data = (await response.json()) as AnthropicResponse;
-
- // Check if we got a valid response
- if (data.content && data.content.length > 0) {
- const textContent = data.content.find((block) => block.type === "text");
- if (textContent && textContent.type === "text" && textContent.text) {
- return NextResponse.json({
- success: true,
- message: `Connection successful! Response: "${textContent.text}"`,
- model: data.model,
- });
- }
- }
-
- return NextResponse.json({
- success: true,
- message: "Connection successful! Claude responded.",
- model: data.model,
- });
- } catch (error: unknown) {
- console.error("Claude API test error:", error);
-
- const errorMessage =
- error instanceof Error ? error.message : "Failed to connect to Claude API";
-
- return NextResponse.json(
- { success: false, error: errorMessage },
- { status: 500 }
- );
- }
-}
diff --git a/apps/app/src/app/api/gemini/test/route.ts b/apps/app/src/app/api/gemini/test/route.ts
deleted file mode 100644
index a4830c84..00000000
--- a/apps/app/src/app/api/gemini/test/route.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-
-interface GeminiContent {
- parts: Array<{
- text?: string;
- inlineData?: {
- mimeType: string;
- data: string;
- };
- }>;
- role?: string;
-}
-
-interface GeminiRequest {
- contents: GeminiContent[];
- generationConfig?: {
- maxOutputTokens?: number;
- temperature?: number;
- };
-}
-
-interface GeminiResponse {
- candidates?: Array<{
- content: {
- parts: Array<{
- text: string;
- }>;
- role: string;
- };
- finishReason: string;
- safetyRatings?: Array<{
- category: string;
- probability: string;
- }>;
- }>;
- promptFeedback?: {
- safetyRatings?: Array<{
- category: string;
- probability: string;
- }>;
- };
- error?: {
- code: number;
- message: string;
- status: string;
- };
-}
-
-export async function POST(request: NextRequest) {
- try {
- const { apiKey, imageData, mimeType, prompt } = await request.json();
-
- // Use provided API key or fall back to environment variable
- const effectiveApiKey = apiKey || process.env.GOOGLE_API_KEY;
-
- if (!effectiveApiKey) {
- return NextResponse.json(
- { success: false, error: "No API key provided or configured in environment" },
- { status: 400 }
- );
- }
-
- // Build the request body
- const requestBody: GeminiRequest = {
- contents: [
- {
- parts: [],
- },
- ],
- generationConfig: {
- maxOutputTokens: 150,
- temperature: 0.4,
- },
- };
-
- // Add image if provided
- if (imageData && mimeType) {
- requestBody.contents[0].parts.push({
- inlineData: {
- mimeType: mimeType,
- data: imageData,
- },
- });
- }
-
- // Add text prompt
- const textPrompt = prompt || (imageData
- ? "Describe what you see in this image briefly."
- : "Respond with exactly: 'Gemini SDK connection successful!' and nothing else.");
-
- requestBody.contents[0].parts.push({
- text: textPrompt,
- });
-
- // Call Gemini API - using gemini-1.5-flash as it supports both text and vision
- const model = imageData ? "gemini-1.5-flash" : "gemini-1.5-flash";
- const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${effectiveApiKey}`;
-
- const response = await fetch(geminiUrl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(requestBody),
- });
-
- const data: GeminiResponse = await response.json();
-
- // Check for API errors
- if (data.error) {
- const errorMessage = data.error.message || "Unknown Gemini API error";
- const statusCode = data.error.code || 500;
-
- if (statusCode === 400 && errorMessage.includes("API key")) {
- return NextResponse.json(
- { success: false, error: "Invalid API key. Please check your Google API key." },
- { status: 401 }
- );
- }
-
- if (statusCode === 429) {
- return NextResponse.json(
- { success: false, error: "Rate limit exceeded. Please try again later." },
- { status: 429 }
- );
- }
-
- return NextResponse.json(
- { success: false, error: `API error: ${errorMessage}` },
- { status: statusCode }
- );
- }
-
- // Check for valid response
- if (!response.ok) {
- return NextResponse.json(
- { success: false, error: `HTTP error: ${response.status} ${response.statusText}` },
- { status: response.status }
- );
- }
-
- // Extract response text
- if (data.candidates && data.candidates.length > 0 && data.candidates[0].content?.parts?.length > 0) {
- const responseText = data.candidates[0].content.parts
- .filter((part) => part.text)
- .map((part) => part.text)
- .join("");
-
- return NextResponse.json({
- success: true,
- message: `Connection successful! Response: "${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}"`,
- model: model,
- hasImage: !!imageData,
- });
- }
-
- // Handle blocked responses
- if (data.promptFeedback?.safetyRatings) {
- return NextResponse.json({
- success: true,
- message: "Connection successful! Gemini responded (response may have been filtered).",
- model: model,
- hasImage: !!imageData,
- });
- }
-
- return NextResponse.json({
- success: true,
- message: "Connection successful! Gemini responded.",
- model: model,
- hasImage: !!imageData,
- });
- } catch (error: unknown) {
- console.error("Gemini API test error:", error);
-
- if (error instanceof TypeError && error.message.includes("fetch")) {
- return NextResponse.json(
- { success: false, error: "Network error. Unable to reach Gemini API." },
- { status: 503 }
- );
- }
-
- const errorMessage =
- error instanceof Error ? error.message : "Failed to connect to Gemini API";
-
- return NextResponse.json(
- { success: false, error: errorMessage },
- { status: 500 }
- );
- }
-}
diff --git a/apps/app/src/app/favicon.ico b/apps/app/src/app/favicon.ico
deleted file mode 100644
index 718d6fea..00000000
Binary files a/apps/app/src/app/favicon.ico and /dev/null differ
diff --git a/apps/app/src/app/layout.tsx b/apps/app/src/app/layout.tsx
deleted file mode 100644
index 2d7df503..00000000
--- a/apps/app/src/app/layout.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { Metadata } from "next";
-import { GeistSans } from "geist/font/sans";
-import { GeistMono } from "geist/font/mono";
-import { Toaster } from "sonner";
-import "./globals.css";
-export const metadata: Metadata = {
- title: "Automaker - Autonomous AI Development Studio",
- description: "Build software autonomously with intelligent orchestration",
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
-
-
- {children}
-
-
-
- );
-}
diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx
deleted file mode 100644
index 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 (