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 (