refactor: move from next js to vite and tanstack router

This commit is contained in:
Kacper
2025-12-17 20:11:16 +01:00
parent 9954feafd8
commit 5136c32b68
263 changed files with 11148 additions and 10276 deletions

View File

@@ -1,5 +0,0 @@
module.exports = {
rules: {
"@typescript-eslint/no-require-imports": "off",
},
};

View File

@@ -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)");

View File

@@ -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;

View File

@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -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 (
<html lang="en" suppressHydrationWarning>
<body
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
>
{children}
<Toaster richColors position="bottom-right" />
</body>
</html>
);
}

View File

@@ -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 <WelcomeView />;
case "setup":
return <SetupView />;
case "board":
return <BoardView />;
case "spec":
return <SpecView />;
case "agent":
return <AgentView />;
case "settings":
return <SettingsView />;
case "interview":
return <InterviewView />;
case "context":
return <ContextView />;
case "profiles":
return <ProfilesView />;
case "running-agents":
return <RunningAgentsView />;
case "terminal":
return <TerminalView />;
case "wiki":
return <WikiView />;
default:
return <WelcomeView />;
}
};
// Setup view is full-screen without sidebar
if (currentView === "setup") {
return (
<main className="h-screen overflow-hidden" data-testid="app-container">
<SetupView />
</main>
);
}
return (
<main className="flex h-screen overflow-hidden" data-testid="app-container">
<Sidebar />
<div
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
style={{ marginRight: streamerPanelOpen ? "250px" : "0" }}
>
{renderView()}
</div>
{/* Hidden streamer panel - opens with "\" key, pushes content */}
<div
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
streamerPanelOpen ? "translate-x-0" : "translate-x-full"
}`}
/>
</main>
);
}
export default function Home() {
return (
<FileBrowserProvider>
<HomeContent />
</FileBrowserProvider>
);
}

View File

@@ -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"]
}

View File

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

36
apps/ui/eslint.config.mjs Normal file
View File

@@ -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;

31
apps/ui/index.html Normal file
View File

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en" suppressHydrationWarning>
<head>
<meta charset="UTF-8" />
<title>Automaker - Autonomous AI Development Studio</title>
<meta name="description" content="Build software autonomously with AI agents" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script>
// Prevent dark mode flash
(function() {
try {
const stored = localStorage.getItem('automaker-storage');
if (stored) {
const data = JSON.parse(stored);
const theme = data.state?.theme;
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else if (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
}
} catch (e) {}
})();
</script>
</head>
<body class="antialiased">
<div id="app"></div>
<script type="module" src="/src/renderer.tsx"></script>
</body>
</html>

View File

@@ -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/**/*"
],

View File

@@ -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",
},
},
],

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

7
apps/ui/src/App.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./utils/router";
import "./styles/global.css";
export default function App() {
return <RouterProvider router={router} />;
}

View File

@@ -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}`

View File

@@ -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",

View File

@@ -1,4 +1,3 @@
"use client";
import { Sparkles, Clock } from "lucide-react";
import {

View File

@@ -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 (
<button
key={item.id}
onClick={() => setCurrentView(item.id as any)}
onClick={() => navigate({ to: `/${item.id}` as const })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
@@ -1951,7 +1951,7 @@ export function Sidebar() {
{!hideWiki && (
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("wiki")}
onClick={() => navigate({ to: "/wiki" })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
@@ -2014,7 +2014,7 @@ export function Sidebar() {
{!hideRunningAgents && (
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("running-agents")}
onClick={() => navigate({ to: "/running-agents" })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",
@@ -2112,7 +2112,7 @@ export function Sidebar() {
{/* Settings Link */}
<div className="p-2">
<button
onClick={() => setCurrentView("settings")}
onClick={() => navigate({ to: "/settings" })}
className={cn(
"group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag",
"transition-all duration-200 ease-out",

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { ChevronDown } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { Check, ChevronsUpDown, LucideIcon } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { GitBranch } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { Autocomplete } from "@/components/ui/autocomplete";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import { Clock } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { Sparkles, X } from "lucide-react";

View File

@@ -1,4 +1,3 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";
@@ -85,7 +84,7 @@ export function DescriptionImageDropZone({
// Construct server URL for loading saved images
const getImageServerUrl = useCallback((imagePath: string): string => {
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
const serverUrl = import.meta.env.VITE_SERVER_URL || "http://localhost:3008";
const projectPath = currentProject?.path || "";
return `${serverUrl}/api/fs/image?path=${encodeURIComponent(imagePath)}&projectPath=${encodeURIComponent(projectPath)}`;
}, [currentProject?.path]);

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"

View File

@@ -1,4 +1,3 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect, useMemo, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { useEffect, useCallback, useRef } from "react";

View File

@@ -1,4 +1,3 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS, parseShortcut, formatShortcut } from "@/store/app-store";

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useMemo, useEffect, useRef } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import ReactMarkdown from "react-markdown";
import { cn } from "@/lib/utils";

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";

View File

@@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

View File

@@ -1,4 +1,3 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"

View File

@@ -1,4 +1,3 @@
"use client";
import CodeMirror from "@uiw/react-codemirror";
import { xml } from "@codemirror/lang-xml";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useCallback } from "react";
import { useAppStore } from "@/store/app-store";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { useAppStore, type AgentModel } from "@/store/app-store";

View File

@@ -1,4 +1,3 @@
"use client";
import { useCallback, useState } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import { useEffect, useState, useCallback, useMemo } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";

View File

@@ -1,4 +1,3 @@
"use client";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";

View File

@@ -1,4 +1,3 @@
"use client";
import { useRef, useEffect } from "react";
import { Input } from "@/components/ui/input";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect, useMemo, memo } from "react";
import { useSortable } from "@dnd-kit/sortable";

View File

@@ -1,4 +1,3 @@
"use client";
import { memo } from "react";
import { useDroppable } from "@dnd-kit/core";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import {
@@ -50,6 +49,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useNavigate } from "@tanstack/react-router";
interface AddFeatureDialogProps {
open: boolean;
@@ -87,6 +87,7 @@ export function AddFeatureDialog({
showProfilesOnly,
aiProfiles,
}: AddFeatureDialogProps) {
const navigate = useNavigate();
const [newFeature, setNewFeature] = useState({
category: "",
description: "",
@@ -424,7 +425,7 @@ export function AddFeatureDialog({
showManageLink
onManageLinkClick={() => {
onOpenChange(false);
useAppStore.getState().setCurrentView("profiles");
navigate({ to: "/profiles" });
}}
/>

View File

@@ -1,4 +1,3 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import { useState, useEffect } from "react";
import {

View File

@@ -1,4 +1,3 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import {

Some files were not shown because too many files have changed in this diff Show More