diff --git a/apps/app/electron/main.js b/apps/app/electron/main.js
index 80271332..70e2511c 100644
--- a/apps/app/electron/main.js
+++ b/apps/app/electron/main.js
@@ -1,22 +1,10 @@
const path = require("path");
-
-// Load environment variables from .env file
-require("dotenv").config({ path: path.join(__dirname, "../.env") });
-
-const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
-const fs = require("fs/promises");
-const agentService = require("./agent-service");
-const autoModeService = require("./auto-mode-service");
-const worktreeManager = require("./services/worktree-manager");
-const featureSuggestionsService = require("./services/feature-suggestions-service");
-const specRegenerationService = require("./services/spec-regeneration-service");
+const { app, BrowserWindow, shell } = require("electron");
let mainWindow = null;
// Get icon path - works in both dev and production
function getIconPath() {
- // In dev: __dirname is electron/, so ../public/logo.png
- // In production: public folder is included in the app bundle
return app.isPackaged
? path.join(process.resourcesPath, "app", "public", "logo.png")
: path.join(__dirname, "../public/logo.png");
@@ -50,33 +38,23 @@ function createWindow() {
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
}
+ // Handle external links - open in default browser
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ shell.openExternal(url);
+ return { action: "deny" };
+ });
+
mainWindow.on("closed", () => {
mainWindow = null;
});
}
-app.whenReady().then(async () => {
+app.whenReady().then(() => {
// Set app icon (dock icon on macOS)
if (process.platform === "darwin" && app.dock) {
app.dock.setIcon(getIconPath());
}
- // Initialize agent service
- const appDataPath = app.getPath("userData");
- await agentService.initialize(appDataPath);
-
- // Pre-load allowed paths from agent history to prevent breaking "Recent Projects"
- try {
- const sessions = await agentService.listSessions({ includeArchived: true });
- sessions.forEach((session) => {
- if (session.projectPath) {
- addAllowedPath(session.projectPath);
- }
- });
- } catch (error) {
- console.error("Failed to load sessions for security whitelist:", error);
- }
-
createWindow();
app.on("activate", () => {
@@ -91,1764 +69,3 @@ app.on("window-all-closed", () => {
app.quit();
}
});
-
-// Track allowed paths for file operations (security)
-const allowedPaths = new Set();
-
-/**
- * Add a path to the allowed list
- */
-function addAllowedPath(pathToAdd) {
- if (!pathToAdd) return;
- allowedPaths.add(path.resolve(pathToAdd));
-}
-
-/**
- * Check if a file path is allowed (must be within an allowed directory)
- */
-function isPathAllowed(filePath) {
- const resolvedPath = path.resolve(filePath);
-
- // Allow access to app data directory (for logs, temp images etc)
- const appDataPath = app.getPath("userData");
- if (resolvedPath.startsWith(appDataPath)) return true;
-
- // Check against all allowed project paths
- for (const allowedPath of allowedPaths) {
- // Check if path starts with allowed directory
- // Ensure we don't match "/foo/bar" against "/foo/b"
- if (
- resolvedPath === allowedPath ||
- resolvedPath.startsWith(allowedPath + path.sep)
- ) {
- return true;
- }
- }
-
- return false;
-}
-
-// IPC Handlers
-
-// Dialog handlers
-ipcMain.handle("dialog:openDirectory", async () => {
- const result = await dialog.showOpenDialog(mainWindow, {
- properties: ["openDirectory", "createDirectory"],
- });
-
- if (!result.canceled && result.filePaths.length > 0) {
- result.filePaths.forEach((p) => addAllowedPath(p));
- }
-
- return result;
-});
-
-ipcMain.handle("dialog:openFile", async (_, options = {}) => {
- const result = await dialog.showOpenDialog(mainWindow, {
- properties: ["openFile"],
- ...options,
- });
-
- if (!result.canceled && result.filePaths.length > 0) {
- // Allow reading the specific file selected
- result.filePaths.forEach((p) => addAllowedPath(p));
- }
-
- return result;
-});
-
-// File system handlers
-ipcMain.handle("fs:readFile", async (_, filePath) => {
- try {
- // Security check
- if (!isPathAllowed(filePath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const content = await fs.readFile(filePath, "utf-8");
- return { success: true, content };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-ipcMain.handle("fs:writeFile", async (_, filePath, content) => {
- try {
- // Security check
- if (!isPathAllowed(filePath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- await fs.writeFile(filePath, content, "utf-8");
- return { success: true };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-ipcMain.handle("fs:mkdir", async (_, dirPath) => {
- try {
- // Security check
- if (!isPathAllowed(dirPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- await fs.mkdir(dirPath, { recursive: true });
- return { success: true };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-ipcMain.handle("fs:readdir", async (_, dirPath) => {
- try {
- // Security check
- if (!isPathAllowed(dirPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
- const result = entries.map((entry) => ({
- name: entry.name,
- isDirectory: entry.isDirectory(),
- isFile: entry.isFile(),
- }));
- return { success: true, entries: result };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-ipcMain.handle("fs:exists", async (_, filePath) => {
- try {
- // Exists check is generally safe, but we can restrict it too for strict privacy
- if (!isPathAllowed(filePath)) {
- return false;
- }
-
- await fs.access(filePath);
- return true;
- } catch {
- return false;
- }
-});
-
-ipcMain.handle("fs:stat", async (_, filePath) => {
- try {
- // Security check
- if (!isPathAllowed(filePath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const stats = await fs.stat(filePath);
- return {
- success: true,
- stats: {
- isDirectory: stats.isDirectory(),
- isFile: stats.isFile(),
- size: stats.size,
- mtime: stats.mtime,
- },
- };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-ipcMain.handle("fs:deleteFile", async (_, filePath) => {
- try {
- // Security check
- if (!isPathAllowed(filePath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- await fs.unlink(filePath);
- return { success: true };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-ipcMain.handle("fs:trashItem", async (_, targetPath) => {
- try {
- // Security check
- if (!isPathAllowed(targetPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- await shell.trashItem(targetPath);
- return { success: true };
- } catch (error) {
- return { success: false, error: error.message };
- }
-});
-
-// App data path
-ipcMain.handle("app:getPath", (_, name) => {
- return app.getPath(name);
-});
-
-// Save image to .automaker/images directory
-ipcMain.handle(
- "app:saveImageToTemp",
- async (_, { data, filename, mimeType, projectPath }) => {
- try {
- // Use .automaker/images directory instead of /tmp
- // If projectPath is provided, use it; otherwise fall back to app data directory
- let imagesDir;
- if (projectPath) {
- imagesDir = path.join(projectPath, ".automaker", "images");
- } else {
- // Fallback for cases where project isn't loaded yet
- const appDataPath = app.getPath("userData");
- imagesDir = path.join(appDataPath, "images");
- }
-
- await fs.mkdir(imagesDir, { recursive: true });
-
- // Generate unique filename with unique ID
- const uniqueId = `${Date.now()}-${Math.random()
- .toString(36)
- .substring(2, 11)}`;
- const safeName = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
- const imageFilePath = path.join(imagesDir, `${uniqueId}_${safeName}`);
-
- // Remove data URL prefix if present (data:image/png;base64,...)
- const base64Data = data.includes(",") ? data.split(",")[1] : data;
-
- // Write image to file
- await fs.writeFile(imageFilePath, base64Data, "base64");
-
- return { success: true, path: imageFilePath };
- } catch (error) {
- console.error("[IPC] Failed to save image:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-// IPC ping for testing communication
-ipcMain.handle("ping", () => {
- return "pong";
-});
-
-// Open external link in default browser
-ipcMain.handle("shell:openExternal", async (_, url) => {
- try {
- await shell.openExternal(url);
- return { success: true };
- } catch (error) {
- console.error("[IPC] shell:openExternal error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// Agent IPC Handlers
-// ============================================================================
-
-/**
- * Start or resume a conversation session
- */
-ipcMain.handle("agent:start", async (_, { sessionId, workingDirectory }) => {
- try {
- return await agentService.startConversation({
- sessionId,
- workingDirectory,
- });
- } catch (error) {
- console.error("[IPC] agent:start error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Send a message to the agent - returns immediately, streams via events
- */
-ipcMain.handle(
- "agent:send",
- async (event, { sessionId, message, workingDirectory, imagePaths }) => {
- try {
- // Create a function to send updates to the renderer
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("agent:stream", {
- sessionId,
- ...data,
- });
- }
- };
-
- // Start processing (runs in background)
- agentService
- .sendMessage({
- sessionId,
- message,
- workingDirectory,
- imagePaths,
- sendToRenderer,
- })
- .catch((error) => {
- console.error("[IPC] agent:send background error:", error);
- sendToRenderer({
- type: "error",
- error: error.message,
- });
- });
-
- // Return immediately
- return { success: true };
- } catch (error) {
- console.error("[IPC] agent:send error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Get conversation history
- */
-ipcMain.handle("agent:getHistory", (_, { sessionId }) => {
- try {
- return agentService.getHistory(sessionId);
- } catch (error) {
- console.error("[IPC] agent:getHistory error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Stop current agent execution
- */
-ipcMain.handle("agent:stop", async (_, { sessionId }) => {
- try {
- return await agentService.stopExecution(sessionId);
- } catch (error) {
- console.error("[IPC] agent:stop error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Clear conversation history
- */
-ipcMain.handle("agent:clear", async (_, { sessionId }) => {
- try {
- return await agentService.clearSession(sessionId);
- } catch (error) {
- console.error("[IPC] agent:clear error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// Session Management IPC Handlers
-// ============================================================================
-
-/**
- * List all sessions
- */
-ipcMain.handle("sessions:list", async (_, { includeArchived }) => {
- try {
- const sessions = await agentService.listSessions({ includeArchived });
- return { success: true, sessions };
- } catch (error) {
- console.error("[IPC] sessions:list error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Create a new session
- */
-ipcMain.handle(
- "sessions:create",
- async (_, { name, projectPath, workingDirectory }) => {
- try {
- // Add project path to allowed paths
- addAllowedPath(projectPath);
- if (workingDirectory) addAllowedPath(workingDirectory);
-
- return await agentService.createSession({
- name,
- projectPath,
- workingDirectory,
- });
- } catch (error) {
- console.error("[IPC] sessions:create error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Update session metadata
- */
-ipcMain.handle("sessions:update", async (_, { sessionId, name, tags }) => {
- try {
- return await agentService.updateSession({ sessionId, name, tags });
- } catch (error) {
- console.error("[IPC] sessions:update error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Archive a session
- */
-ipcMain.handle("sessions:archive", async (_, { sessionId }) => {
- try {
- return await agentService.archiveSession(sessionId);
- } catch (error) {
- console.error("[IPC] sessions:archive error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Unarchive a session
- */
-ipcMain.handle("sessions:unarchive", async (_, { sessionId }) => {
- try {
- return await agentService.unarchiveSession(sessionId);
- } catch (error) {
- console.error("[IPC] sessions:unarchive error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Delete a session permanently
- */
-ipcMain.handle("sessions:delete", async (_, { sessionId }) => {
- try {
- return await agentService.deleteSession(sessionId);
- } catch (error) {
- console.error("[IPC] sessions:delete error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// Auto Mode IPC Handlers
-// ============================================================================
-
-/**
- * Start auto mode - autonomous feature implementation
- */
-ipcMain.handle(
- "auto-mode:start",
- async (_, { projectPath, maxConcurrency }) => {
- try {
- // Add project path to allowed paths
- addAllowedPath(projectPath);
-
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.start({
- projectPath,
- sendToRenderer,
- maxConcurrency,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:start error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Stop auto mode for a specific project
- */
-ipcMain.handle("auto-mode:stop", async (_, { projectPath }) => {
- try {
- return await autoModeService.stop({ projectPath });
- } catch (error) {
- console.error("[IPC] auto-mode:stop error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get auto mode status (optionally for a specific project)
- */
-ipcMain.handle("auto-mode:status", (_, { projectPath } = {}) => {
- try {
- return { success: true, ...autoModeService.getStatus({ projectPath }) };
- } catch (error) {
- console.error("[IPC] auto-mode:status error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Run a specific feature
- */
-ipcMain.handle(
- "auto-mode:run-feature",
- async (_, { projectPath, featureId, useWorktrees = false }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.runFeature({
- projectPath,
- featureId,
- sendToRenderer,
- useWorktrees,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:run-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Verify a specific feature by running its tests
- */
-ipcMain.handle(
- "auto-mode:verify-feature",
- async (_, { projectPath, featureId }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.verifyFeature({
- projectPath,
- featureId,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:verify-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Resume a specific feature with previous context
- */
-ipcMain.handle(
- "auto-mode:resume-feature",
- async (_, { projectPath, featureId }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.resumeFeature({
- projectPath,
- featureId,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:resume-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Check if a context file exists for a feature
- */
-ipcMain.handle(
- "auto-mode:context-exists",
- async (_, { projectPath, featureId }) => {
- try {
- const contextPath = path.join(
- projectPath,
- ".automaker",
- "context",
- `${featureId}.md`
- );
- try {
- await fs.access(contextPath);
- return { success: true, exists: true };
- } catch {
- return { success: true, exists: false };
- }
- } catch (error) {
- console.error("[IPC] auto-mode:context-exists error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Analyze a new project - kicks off an agent to analyze the codebase
- * and update the app_spec.txt with tech stack and implemented features
- */
-ipcMain.handle("auto-mode:analyze-project", async (_, { projectPath }) => {
- try {
- // Add project path to allowed paths
- addAllowedPath(projectPath);
-
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.analyzeProject({
- projectPath,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:analyze-project error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Stop a specific feature
- */
-ipcMain.handle("auto-mode:stop-feature", async (_, { featureId }) => {
- try {
- return await autoModeService.stopFeature({ featureId });
- } catch (error) {
- console.error("[IPC] auto-mode:stop-feature error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Follow-up on a feature with additional prompt
- */
-ipcMain.handle(
- "auto-mode:follow-up-feature",
- async (_, { projectPath, featureId, prompt, imagePaths }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.followUpFeature({
- projectPath,
- featureId,
- prompt,
- imagePaths,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:follow-up-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Commit changes for a feature (no further work, just commit)
- */
-ipcMain.handle(
- "auto-mode:commit-feature",
- async (_, { projectPath, featureId }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.commitFeature({
- projectPath,
- featureId,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] auto-mode:commit-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-// ============================================================================
-// Claude CLI Detection IPC Handlers
-// ============================================================================
-
-/**
- * Check Claude Code CLI installation status
- */
-ipcMain.handle("claude:check-cli", async () => {
- try {
- const claudeCliDetector = require("./services/claude-cli-detector");
- const path = require("path");
- const credentialsPath = path.join(
- app.getPath("userData"),
- "credentials.json"
- );
- const fullStatus = claudeCliDetector.getFullStatus(credentialsPath);
-
- // Return in format expected by settings view (status: "installed" | "not_installed")
- return {
- success: true,
- status: fullStatus.installed ? "installed" : "not_installed",
- method: fullStatus.auth?.method || null,
- version: fullStatus.version || null,
- path: fullStatus.path || null,
- authenticated: fullStatus.auth?.authenticated || false,
- recommendation: fullStatus.installed
- ? null
- : "Install Claude Code CLI for optimal performance with ultrathink.",
- installCommands: fullStatus.installed
- ? null
- : claudeCliDetector.getInstallCommands(),
- };
- } catch (error) {
- console.error("[IPC] claude:check-cli error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// Codex CLI Detection IPC Handlers
-// ============================================================================
-
-/**
- * Check Codex CLI installation status
- */
-ipcMain.handle("codex:check-cli", async () => {
- try {
- const codexCliDetector = require("./services/codex-cli-detector");
- const info = codexCliDetector.getInstallationInfo();
- return { success: true, ...info };
- } catch (error) {
- console.error("[IPC] codex:check-cli error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get all available models from all providers
- */
-ipcMain.handle("model:get-available", async () => {
- try {
- const { ModelProviderFactory } = require("./services/model-provider");
- const models = ModelProviderFactory.getAllModels();
- return { success: true, models };
- } catch (error) {
- console.error("[IPC] model:get-available error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Check all provider installation status
- */
-ipcMain.handle("model:check-providers", async () => {
- try {
- const { ModelProviderFactory } = require("./services/model-provider");
- const status = await ModelProviderFactory.checkAllProviders();
- return { success: true, providers: status };
- } catch (error) {
- console.error("[IPC] model:check-providers error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// MCP Server IPC Handlers
-// ============================================================================
-
-/**
- * Handle MCP server callback for updating feature status
- * This can be called by the MCP server script via HTTP or other communication mechanism
- * Note: The MCP server script runs as a separate process, so it can't directly use Electron IPC.
- * For now, the MCP server calls featureLoader.updateFeatureStatus directly.
- * This handler is here for future extensibility (e.g., HTTP endpoint bridge).
- */
-ipcMain.handle(
- "mcp:update-feature-status",
- async (_, { featureId, status, projectPath, summary }) => {
- try {
- const featureLoader = require("./services/feature-loader");
- await featureLoader.updateFeatureStatus(featureId, status, projectPath, {
- summary,
- });
-
- // Notify renderer if window is available
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("mcp:feature-status-updated", {
- featureId,
- status,
- projectPath,
- summary,
- });
- }
-
- return { success: true };
- } catch (error) {
- console.error("[IPC] mcp:update-feature-status error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-// ============================================================================
-// Feature Suggestions IPC Handlers
-// ============================================================================
-
-// Track running suggestions analysis
-let suggestionsExecution = null;
-
-/**
- * Generate feature suggestions by analyzing the project
- * @param {string} projectPath - The path to the project
- * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
- */
-ipcMain.handle(
- "suggestions:generate",
- async (_, { projectPath, suggestionType = "features" }) => {
- try {
- // Check if already running
- if (suggestionsExecution && suggestionsExecution.isActive()) {
- return {
- success: false,
- error: "Suggestions generation is already running",
- };
- }
-
- // Create execution context
- suggestionsExecution = {
- abortController: null,
- query: null,
- isActive: () => suggestionsExecution !== null,
- };
-
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("suggestions:event", data);
- }
- };
-
- // Start generating suggestions (runs in background)
- featureSuggestionsService
- .generateSuggestions(
- projectPath,
- sendToRenderer,
- suggestionsExecution,
- suggestionType
- )
- .catch((error) => {
- console.error("[IPC] suggestions:generate background error:", error);
- sendToRenderer({
- type: "suggestions_error",
- error: error.message,
- });
- })
- .finally(() => {
- suggestionsExecution = null;
- });
-
- // Return immediately
- return { success: true };
- } catch (error) {
- console.error("[IPC] suggestions:generate error:", error);
- suggestionsExecution = null;
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Stop the current suggestions generation
- */
-ipcMain.handle("suggestions:stop", async () => {
- try {
- if (suggestionsExecution && suggestionsExecution.abortController) {
- suggestionsExecution.abortController.abort();
- }
- suggestionsExecution = null;
- return { success: true };
- } catch (error) {
- console.error("[IPC] suggestions:stop error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get suggestions generation status
- */
-ipcMain.handle("suggestions:status", () => {
- return {
- success: true,
- isRunning: suggestionsExecution !== null && suggestionsExecution.isActive(),
- };
-});
-
-// ============================================================================
-// OpenAI API Handlers
-// ============================================================================
-
-/**
- * Test OpenAI API connection
- */
-ipcMain.handle("openai:test-connection", async (_, { apiKey }) => {
- try {
- // Simple test using fetch to OpenAI API
- const response = await fetch("https://api.openai.com/v1/models", {
- method: "GET",
- headers: {
- Authorization: `Bearer ${apiKey || process.env.OPENAI_API_KEY}`,
- "Content-Type": "application/json",
- },
- });
-
- if (response.ok) {
- const data = await response.json();
- return {
- success: true,
- message: `Connected successfully. Found ${
- data.data?.length || 0
- } models.`,
- };
- } else {
- const error = await response.json();
- return {
- success: false,
- error: error.error?.message || "Failed to connect to OpenAI API",
- };
- }
- } catch (error) {
- console.error("[IPC] openai:test-connection error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// Worktree Management IPC Handlers
-// ============================================================================
-
-/**
- * Revert feature changes by removing the worktree
- * This effectively discards all changes made by the agent
- */
-ipcMain.handle(
- "worktree:revert-feature",
- async (_, { projectPath, featureId }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.revertFeature({
- projectPath,
- featureId,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] worktree:revert-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-// ============================================================================
-// Spec Regeneration IPC Handlers
-// ============================================================================
-
-// Track running spec regeneration
-let specRegenerationExecution = null;
-
-/**
- * Regenerate the app spec based on project definition
- */
-ipcMain.handle(
- "spec-regeneration:generate",
- async (_, { projectPath, projectDefinition }) => {
- try {
- // Add project path to allowed paths
- addAllowedPath(projectPath);
-
- // Check if already running
- if (specRegenerationExecution && specRegenerationExecution.isActive()) {
- return {
- success: false,
- error: "Spec regeneration is already running",
- };
- }
-
- // Create execution context
- specRegenerationExecution = {
- abortController: null,
- query: null,
- isActive: () => specRegenerationExecution !== null,
- };
-
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("spec-regeneration:event", data);
- }
- };
-
- // Start regenerating spec (runs in background)
- specRegenerationService
- .regenerateSpec(
- projectPath,
- projectDefinition,
- sendToRenderer,
- specRegenerationExecution
- )
- .catch((error) => {
- console.error(
- "[IPC] spec-regeneration:generate background error:",
- error
- );
- sendToRenderer({
- type: "spec_regeneration_error",
- error: error.message,
- });
- })
- .finally(() => {
- specRegenerationExecution = null;
- });
-
- // Return immediately
- return { success: true };
- } catch (error) {
- console.error("[IPC] spec-regeneration:generate error:", error);
- specRegenerationExecution = null;
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Stop the current spec regeneration
- */
-ipcMain.handle("spec-regeneration:stop", async () => {
- try {
- if (
- specRegenerationExecution &&
- specRegenerationExecution.abortController
- ) {
- specRegenerationExecution.abortController.abort();
- }
- specRegenerationExecution = null;
- return { success: true };
- } catch (error) {
- console.error("[IPC] spec-regeneration:stop error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get spec regeneration status
- */
-ipcMain.handle("spec-regeneration:status", () => {
- return {
- success: true,
- isRunning:
- specRegenerationExecution !== null &&
- specRegenerationExecution.isActive(),
- currentPhase: specRegenerationService.getCurrentPhase(),
- };
-});
-
-/**
- * Create initial app spec for a new project
- */
-ipcMain.handle(
- "spec-regeneration:create",
- async (_, { projectPath, projectOverview, generateFeatures = true }) => {
- try {
- // Add project path to allowed paths
- addAllowedPath(projectPath);
-
- // Check if already running
- if (specRegenerationExecution && specRegenerationExecution.isActive()) {
- return { success: false, error: "Spec creation is already running" };
- }
-
- // Create execution context
- specRegenerationExecution = {
- abortController: null,
- query: null,
- isActive: () => specRegenerationExecution !== null,
- };
-
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("spec-regeneration:event", data);
- }
- };
-
- // Start creating spec (runs in background)
- specRegenerationService
- .createInitialSpec(
- projectPath,
- projectOverview,
- sendToRenderer,
- specRegenerationExecution,
- generateFeatures
- )
- .catch((error) => {
- console.error(
- "[IPC] spec-regeneration:create background error:",
- error
- );
- sendToRenderer({
- type: "spec_regeneration_error",
- error: error.message,
- });
- })
- .finally(() => {
- specRegenerationExecution = null;
- });
-
- // Return immediately
- return { success: true };
- } catch (error) {
- console.error("[IPC] spec-regeneration:create error:", error);
- specRegenerationExecution = null;
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Generate features from existing app_spec.txt
- * This allows users to generate features retroactively without regenerating the spec
- */
-ipcMain.handle(
- "spec-regeneration:generate-features",
- async (_, { projectPath }) => {
- try {
- // Add project path to allowed paths
- addAllowedPath(projectPath);
-
- // Check if already running
- if (specRegenerationExecution && specRegenerationExecution.isActive()) {
- return {
- success: false,
- error: "Spec regeneration is already running",
- };
- }
-
- // Create execution context
- specRegenerationExecution = {
- abortController: null,
- query: null,
- isActive: () => specRegenerationExecution !== null,
- };
-
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("spec-regeneration:event", data);
- }
- };
-
- // Start generating features (runs in background)
- specRegenerationService
- .generateFeaturesOnly(
- projectPath,
- sendToRenderer,
- specRegenerationExecution
- )
- .catch((error) => {
- console.error(
- "[IPC] spec-regeneration:generate-features background error:",
- error
- );
- sendToRenderer({
- type: "spec_regeneration_error",
- error: error.message,
- });
- })
- .finally(() => {
- specRegenerationExecution = null;
- });
-
- // Return immediately
- return { success: true };
- } catch (error) {
- console.error("[IPC] spec-regeneration:generate-features error:", error);
- specRegenerationExecution = null;
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Merge feature worktree changes back to main branch
- */
-ipcMain.handle(
- "worktree:merge-feature",
- async (_, { projectPath, featureId, options }) => {
- try {
- const sendToRenderer = (data) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("auto-mode:event", data);
- }
- };
-
- return await autoModeService.mergeFeature({
- projectPath,
- featureId,
- options,
- sendToRenderer,
- });
- } catch (error) {
- console.error("[IPC] worktree:merge-feature error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-/**
- * Get worktree info for a feature
- */
-ipcMain.handle("worktree:get-info", async (_, { projectPath, featureId }) => {
- try {
- return await autoModeService.getWorktreeInfo({ projectPath, featureId });
- } catch (error) {
- console.error("[IPC] worktree:get-info error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get worktree status (changed files, commits)
- */
-ipcMain.handle("worktree:get-status", async (_, { projectPath, featureId }) => {
- try {
- return await autoModeService.getWorktreeStatus({ projectPath, featureId });
- } catch (error) {
- console.error("[IPC] worktree:get-status error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * List all feature worktrees
- */
-ipcMain.handle("worktree:list", async (_, { projectPath }) => {
- try {
- return await autoModeService.listWorktrees({ projectPath });
- } catch (error) {
- console.error("[IPC] worktree:list error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get file diffs for a worktree
- */
-ipcMain.handle("worktree:get-diffs", async (_, { projectPath, featureId }) => {
- try {
- return await autoModeService.getFileDiffs({ projectPath, featureId });
- } catch (error) {
- console.error("[IPC] worktree:get-diffs error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get diff for a specific file in a worktree
- */
-ipcMain.handle(
- "worktree:get-file-diff",
- async (_, { projectPath, featureId, filePath }) => {
- try {
- return await autoModeService.getFileDiff({
- projectPath,
- featureId,
- filePath,
- });
- } catch (error) {
- console.error("[IPC] worktree:get-file-diff error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Get file diffs for the main project (non-worktree)
- */
-ipcMain.handle("git:get-diffs", async (_, { projectPath }) => {
- try {
- return await worktreeManager.getFileDiffs(projectPath);
- } catch (error) {
- console.error("[IPC] git:get-diffs error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get diff for a specific file in the main project (non-worktree)
- */
-ipcMain.handle("git:get-file-diff", async (_, { projectPath, filePath }) => {
- try {
- return await worktreeManager.getFileDiff(projectPath, filePath);
- } catch (error) {
- console.error("[IPC] git:get-file-diff error:", error);
- return { success: false, error: error.message };
- }
-});
-
-// ============================================================================
-// Setup & CLI Management IPC Handlers
-// ============================================================================
-
-/**
- * Get comprehensive Claude CLI status including auth
- */
-ipcMain.handle("setup:claude-status", async () => {
- try {
- const claudeCliDetector = require("./services/claude-cli-detector");
- const credentialsPath = path.join(
- app.getPath("userData"),
- "credentials.json"
- );
- const result = claudeCliDetector.getFullStatus(credentialsPath);
- return result;
- } catch (error) {
- console.error("[IPC] setup:claude-status error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get comprehensive Codex CLI status including auth
- */
-ipcMain.handle("setup:codex-status", async () => {
- try {
- const codexCliDetector = require("./services/codex-cli-detector");
- const info = codexCliDetector.getFullStatus();
- return { success: true, ...info };
- } catch (error) {
- console.error("[IPC] setup:codex-status error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Install Claude CLI
- */
-ipcMain.handle("setup:install-claude", async (event) => {
- try {
- const claudeCliDetector = require("./services/claude-cli-detector");
-
- const sendProgress = (progress) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("setup:install-progress", {
- cli: "claude",
- ...progress,
- });
- }
- };
-
- const result = await claudeCliDetector.installCli(sendProgress);
- return { success: true, ...result };
- } catch (error) {
- console.error("[IPC] setup:install-claude error:", error);
- return { success: false, error: error.message || error.error };
- }
-});
-
-/**
- * Install Codex CLI
- */
-ipcMain.handle("setup:install-codex", async (event) => {
- try {
- const codexCliDetector = require("./services/codex-cli-detector");
-
- const sendProgress = (progress) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("setup:install-progress", {
- cli: "codex",
- ...progress,
- });
- }
- };
-
- const result = await codexCliDetector.installCli(sendProgress);
- return { success: true, ...result };
- } catch (error) {
- console.error("[IPC] setup:install-codex error:", error);
- return { success: false, error: error.message || error.error };
- }
-});
-
-/**
- * Authenticate Claude CLI (manual auth required)
- */
-ipcMain.handle("setup:auth-claude", async (event) => {
- try {
- const claudeCliDetector = require("./services/claude-cli-detector");
-
- const sendProgress = (progress) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("setup:auth-progress", {
- cli: "claude",
- ...progress,
- });
- }
- };
-
- const result = await claudeCliDetector.runSetupToken(sendProgress);
- return { success: true, ...result };
- } catch (error) {
- console.error("[IPC] setup:auth-claude error:", error);
- return { success: false, error: error.message || error.error };
- }
-});
-
-/**
- * Authenticate Codex CLI with optional API key
- */
-ipcMain.handle("setup:auth-codex", async (event, { apiKey }) => {
- try {
- const codexCliDetector = require("./services/codex-cli-detector");
-
- const sendProgress = (progress) => {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("setup:auth-progress", {
- cli: "codex",
- ...progress,
- });
- }
- };
-
- const result = await codexCliDetector.authenticate(apiKey, sendProgress);
- return { success: true, ...result };
- } catch (error) {
- console.error("[IPC] setup:auth-codex error:", error);
- return { success: false, error: error.message || error.error };
- }
-});
-
-/**
- * Store API key or OAuth token securely (using app's userData)
- * @param {string} provider - Provider name (anthropic, openai, google, anthropic_oauth_token)
- * @param {string} apiKey - The API key or OAuth token to store
- */
-ipcMain.handle("setup:store-api-key", async (_, { provider, apiKey }) => {
- try {
- const configPath = path.join(app.getPath("userData"), "credentials.json");
- let credentials = {};
-
- // Read existing credentials
- try {
- const content = await fs.readFile(configPath, "utf-8");
- credentials = JSON.parse(content);
- } catch (e) {
- // File doesn't exist, start fresh
- }
-
- // Store the new key/token
- credentials[provider] = apiKey;
-
- // Write back
- await fs.writeFile(
- configPath,
- JSON.stringify(credentials, null, 2),
- "utf-8"
- );
-
- return { success: true };
- } catch (error) {
- console.error("[IPC] setup:store-api-key error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get stored API keys and tokens
- */
-ipcMain.handle("setup:get-api-keys", async () => {
- try {
- const configPath = path.join(app.getPath("userData"), "credentials.json");
-
- try {
- const content = await fs.readFile(configPath, "utf-8");
- const credentials = JSON.parse(content);
-
- // Return which keys/tokens exist (not the actual values for security)
- return {
- success: true,
- hasAnthropicKey: !!credentials.anthropic,
- hasAnthropicOAuthToken: !!credentials.anthropic_oauth_token,
- hasOpenAIKey: !!credentials.openai,
- hasGoogleKey: !!credentials.google,
- };
- } catch (e) {
- return {
- success: true,
- hasAnthropicKey: false,
- hasAnthropicOAuthToken: false,
- hasOpenAIKey: false,
- hasGoogleKey: false,
- };
- }
- } catch (error) {
- console.error("[IPC] setup:get-api-keys error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Configure Codex MCP server for a project
- */
-ipcMain.handle("setup:configure-codex-mcp", async (_, { projectPath }) => {
- try {
- const codexConfigManager = require("./services/codex-config-manager");
- const mcpServerPath = path.join(
- __dirname,
- "services",
- "mcp-server-factory.js"
- );
-
- const configPath = await codexConfigManager.configureMcpServer(
- projectPath,
- mcpServerPath
- );
-
- return { success: true, configPath };
- } catch (error) {
- console.error("[IPC] setup:configure-codex-mcp error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get platform information
- */
-ipcMain.handle("setup:get-platform", async () => {
- const os = require("os");
- return {
- success: true,
- platform: process.platform,
- arch: process.arch,
- homeDir: os.homedir(),
- isWindows: process.platform === "win32",
- isMac: process.platform === "darwin",
- isLinux: process.platform === "linux",
- };
-});
-
-// ============================================================================
-// Features IPC Handlers
-// ============================================================================
-
-/**
- * Get all features for a project
- */
-ipcMain.handle("features:getAll", async (_, { projectPath }) => {
- try {
- // Security check
- if (!isPathAllowed(projectPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const featureLoader = require("./services/feature-loader");
- const features = await featureLoader.getAll(projectPath);
- return { success: true, features };
- } catch (error) {
- console.error("[IPC] features:getAll error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get a single feature by ID
- */
-ipcMain.handle("features:get", async (_, { projectPath, featureId }) => {
- try {
- // Security check
- if (!isPathAllowed(projectPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const featureLoader = require("./services/feature-loader");
- const feature = await featureLoader.get(projectPath, featureId);
- if (!feature) {
- return { success: false, error: "Feature not found" };
- }
- return { success: true, feature };
- } catch (error) {
- console.error("[IPC] features:get error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Create a new feature
- */
-ipcMain.handle("features:create", async (_, { projectPath, feature }) => {
- try {
- // Security check
- if (!isPathAllowed(projectPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const featureLoader = require("./services/feature-loader");
- const createdFeature = await featureLoader.create(projectPath, feature);
- return { success: true, feature: createdFeature };
- } catch (error) {
- console.error("[IPC] features:create error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Update a feature (partial updates supported)
- */
-ipcMain.handle(
- "features:update",
- async (_, { projectPath, featureId, updates }) => {
- try {
- // Security check
- if (!isPathAllowed(projectPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const featureLoader = require("./services/feature-loader");
- const updatedFeature = await featureLoader.update(
- projectPath,
- featureId,
- updates
- );
- return { success: true, feature: updatedFeature };
- } catch (error) {
- console.error("[IPC] features:update error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-/**
- * Delete a feature and its folder
- */
-ipcMain.handle("features:delete", async (_, { projectPath, featureId }) => {
- try {
- // Security check
- if (!isPathAllowed(projectPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const featureLoader = require("./services/feature-loader");
- await featureLoader.delete(projectPath, featureId);
- return { success: true };
- } catch (error) {
- console.error("[IPC] features:delete error:", error);
- return { success: false, error: error.message };
- }
-});
-
-/**
- * Get agent output for a feature
- */
-ipcMain.handle(
- "features:getAgentOutput",
- async (_, { projectPath, featureId }) => {
- try {
- // Security check
- if (!isPathAllowed(projectPath)) {
- return {
- success: false,
- error: "Access denied: Path is outside allowed project directories",
- };
- }
-
- const featureLoader = require("./services/feature-loader");
- const content = await featureLoader.getAgentOutput(
- projectPath,
- featureId
- );
- return { success: true, content };
- } catch (error) {
- console.error("[IPC] features:getAgentOutput error:", error);
- return { success: false, error: error.message };
- }
- }
-);
-
-// ============================================================================
-// Running Agents IPC Handlers
-// ============================================================================
-
-/**
- * Get all currently running agents across all projects
- */
-ipcMain.handle("running-agents:getAll", () => {
- try {
- const status = autoModeService.getStatus();
- const allStatuses = autoModeService.getAllProjectStatuses();
-
- // Build a list of running agents with their details
- const runningAgents = [];
-
- for (const [projectPath, projectStatus] of Object.entries(allStatuses)) {
- for (const featureId of projectStatus.runningFeatures) {
- runningAgents.push({
- featureId,
- projectPath,
- projectName: projectPath.split(/[/\\]/).pop() || projectPath,
- isAutoMode: projectStatus.isRunning,
- });
- }
- }
-
- return {
- success: true,
- runningAgents,
- totalCount: status.runningCount,
- autoLoopRunning: status.autoLoopRunning,
- };
- } catch (error) {
- console.error("[IPC] running-agents:getAll error:", error);
- return { success: false, error: error.message };
- }
-});
diff --git a/apps/app/electron/preload.js b/apps/app/electron/preload.js
index 85a31baa..4d802527 100644
--- a/apps/app/electron/preload.js
+++ b/apps/app/electron/preload.js
@@ -1,404 +1,10 @@
-const { contextBridge, ipcRenderer } = require("electron");
+const { contextBridge } = require("electron");
-// Expose protected methods that allow the renderer process to use
-// the ipcRenderer without exposing the entire object
-contextBridge.exposeInMainWorld("electronAPI", {
- // IPC test
- ping: () => ipcRenderer.invoke("ping"),
-
- // Shell APIs
- openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
-
- // Dialog APIs
- openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
- openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
-
- // File system APIs
- readFile: (filePath) => ipcRenderer.invoke("fs:readFile", filePath),
- writeFile: (filePath, content) =>
- ipcRenderer.invoke("fs:writeFile", filePath, content),
- mkdir: (dirPath) => ipcRenderer.invoke("fs:mkdir", dirPath),
- readdir: (dirPath) => ipcRenderer.invoke("fs:readdir", dirPath),
- exists: (filePath) => ipcRenderer.invoke("fs:exists", filePath),
- stat: (filePath) => ipcRenderer.invoke("fs:stat", filePath),
- deleteFile: (filePath) => ipcRenderer.invoke("fs:deleteFile", filePath),
- trashItem: (filePath) => ipcRenderer.invoke("fs:trashItem", filePath),
-
- // App APIs
- getPath: (name) => ipcRenderer.invoke("app:getPath", name),
- saveImageToTemp: (data, filename, mimeType, projectPath) =>
- ipcRenderer.invoke("app:saveImageToTemp", {
- data,
- filename,
- mimeType,
- projectPath,
- }),
-
- // Agent APIs
- agent: {
- // Start or resume a conversation
- start: (sessionId, workingDirectory) =>
- ipcRenderer.invoke("agent:start", { sessionId, workingDirectory }),
-
- // Send a message to the agent
- send: (sessionId, message, workingDirectory, imagePaths) =>
- ipcRenderer.invoke("agent:send", {
- sessionId,
- message,
- workingDirectory,
- imagePaths,
- }),
-
- // Get conversation history
- getHistory: (sessionId) =>
- ipcRenderer.invoke("agent:getHistory", { sessionId }),
-
- // Stop current execution
- stop: (sessionId) => ipcRenderer.invoke("agent:stop", { sessionId }),
-
- // Clear conversation
- clear: (sessionId) => ipcRenderer.invoke("agent:clear", { sessionId }),
-
- // Subscribe to streaming events
- onStream: (callback) => {
- const subscription = (_, data) => callback(data);
- ipcRenderer.on("agent:stream", subscription);
- // Return unsubscribe function
- return () => ipcRenderer.removeListener("agent:stream", subscription);
- },
- },
-
- // Session Management APIs
- sessions: {
- // List all sessions
- list: (includeArchived) =>
- ipcRenderer.invoke("sessions:list", { includeArchived }),
-
- // Create a new session
- create: (name, projectPath, workingDirectory) =>
- ipcRenderer.invoke("sessions:create", {
- name,
- projectPath,
- workingDirectory,
- }),
-
- // Update session metadata
- update: (sessionId, name, tags) =>
- ipcRenderer.invoke("sessions:update", { sessionId, name, tags }),
-
- // Archive a session
- archive: (sessionId) =>
- ipcRenderer.invoke("sessions:archive", { sessionId }),
-
- // Unarchive a session
- unarchive: (sessionId) =>
- ipcRenderer.invoke("sessions:unarchive", { sessionId }),
-
- // Delete a session permanently
- delete: (sessionId) => ipcRenderer.invoke("sessions:delete", { sessionId }),
- },
-
- // Auto Mode API
- autoMode: {
- // Start auto mode for a specific project
- start: (projectPath, maxConcurrency) =>
- ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }),
-
- // Stop auto mode for a specific project
- stop: (projectPath) => ipcRenderer.invoke("auto-mode:stop", { projectPath }),
-
- // Get auto mode status (optionally for a specific project)
- status: (projectPath) => ipcRenderer.invoke("auto-mode:status", { projectPath }),
-
- // Run a specific feature
- runFeature: (projectPath, featureId, useWorktrees) =>
- ipcRenderer.invoke("auto-mode:run-feature", {
- projectPath,
- featureId,
- useWorktrees,
- }),
-
- // Verify a specific feature by running its tests
- verifyFeature: (projectPath, featureId) =>
- ipcRenderer.invoke("auto-mode:verify-feature", {
- projectPath,
- featureId,
- }),
-
- // Resume a specific feature with previous context
- resumeFeature: (projectPath, featureId) =>
- ipcRenderer.invoke("auto-mode:resume-feature", {
- projectPath,
- featureId,
- }),
-
- // Check if context file exists for a feature
- contextExists: (projectPath, featureId) =>
- ipcRenderer.invoke("auto-mode:context-exists", {
- projectPath,
- featureId,
- }),
-
- // Analyze a new project - kicks off an agent to analyze codebase
- analyzeProject: (projectPath) =>
- ipcRenderer.invoke("auto-mode:analyze-project", { projectPath }),
-
- // Stop a specific feature
- stopFeature: (featureId) =>
- ipcRenderer.invoke("auto-mode:stop-feature", { featureId }),
-
- // Follow-up on a feature with additional prompt
- followUpFeature: (projectPath, featureId, prompt, imagePaths) =>
- ipcRenderer.invoke("auto-mode:follow-up-feature", {
- projectPath,
- featureId,
- prompt,
- imagePaths,
- }),
-
- // Commit changes for a feature
- commitFeature: (projectPath, featureId) =>
- ipcRenderer.invoke("auto-mode:commit-feature", {
- projectPath,
- featureId,
- }),
-
- // Listen for auto mode events
- onEvent: (callback) => {
- const subscription = (_, data) => callback(data);
- ipcRenderer.on("auto-mode:event", subscription);
-
- // Return unsubscribe function
- return () => {
- ipcRenderer.removeListener("auto-mode:event", subscription);
- };
- },
- },
-
- // Claude CLI Detection API
- checkClaudeCli: () => ipcRenderer.invoke("claude:check-cli"),
-
- // Codex CLI Detection API
- checkCodexCli: () => ipcRenderer.invoke("codex:check-cli"),
-
- // Model Management APIs
- model: {
- // Get all available models from all providers
- getAvailable: () => ipcRenderer.invoke("model:get-available"),
-
- // Check all provider installation status
- checkProviders: () => ipcRenderer.invoke("model:check-providers"),
- },
-
- // OpenAI API
- testOpenAIConnection: (apiKey) =>
- ipcRenderer.invoke("openai:test-connection", { apiKey }),
-
- // Worktree Management APIs
- worktree: {
- // Revert feature changes by removing the worktree
- revertFeature: (projectPath, featureId) =>
- ipcRenderer.invoke("worktree:revert-feature", { projectPath, featureId }),
-
- // Merge feature worktree changes back to main branch
- mergeFeature: (projectPath, featureId, options) =>
- ipcRenderer.invoke("worktree:merge-feature", {
- projectPath,
- featureId,
- options,
- }),
-
- // Get worktree info for a feature
- getInfo: (projectPath, featureId) =>
- ipcRenderer.invoke("worktree:get-info", { projectPath, featureId }),
-
- // Get worktree status (changed files, commits)
- getStatus: (projectPath, featureId) =>
- ipcRenderer.invoke("worktree:get-status", { projectPath, featureId }),
-
- // List all feature worktrees
- list: (projectPath) => ipcRenderer.invoke("worktree:list", { projectPath }),
-
- // Get file diffs for a feature worktree
- getDiffs: (projectPath, featureId) =>
- ipcRenderer.invoke("worktree:get-diffs", { projectPath, featureId }),
-
- // Get diff for a specific file in a worktree
- getFileDiff: (projectPath, featureId, filePath) =>
- ipcRenderer.invoke("worktree:get-file-diff", {
- projectPath,
- featureId,
- filePath,
- }),
- },
-
- // Git Operations APIs (for non-worktree operations)
- git: {
- // Get file diffs for the main project
- getDiffs: (projectPath) =>
- ipcRenderer.invoke("git:get-diffs", { projectPath }),
-
- // Get diff for a specific file in the main project
- getFileDiff: (projectPath, filePath) =>
- ipcRenderer.invoke("git:get-file-diff", { projectPath, filePath }),
- },
-
- // Feature Suggestions API
- suggestions: {
- // Generate feature suggestions
- // suggestionType can be: "features", "refactoring", "security", "performance"
- generate: (projectPath, suggestionType = "features") =>
- ipcRenderer.invoke("suggestions:generate", { projectPath, suggestionType }),
-
- // Stop generating suggestions
- stop: () => ipcRenderer.invoke("suggestions:stop"),
-
- // Get suggestions status
- status: () => ipcRenderer.invoke("suggestions:status"),
-
- // Listen for suggestions events
- onEvent: (callback) => {
- const subscription = (_, data) => callback(data);
- ipcRenderer.on("suggestions:event", subscription);
-
- // Return unsubscribe function
- return () => {
- ipcRenderer.removeListener("suggestions:event", subscription);
- };
- },
- },
-
- // Spec Regeneration API
- specRegeneration: {
- // Create initial app spec for a new project
- create: (projectPath, projectOverview, generateFeatures = true) =>
- ipcRenderer.invoke("spec-regeneration:create", {
- projectPath,
- projectOverview,
- generateFeatures,
- }),
-
- // Regenerate the app spec
- generate: (projectPath, projectDefinition) =>
- ipcRenderer.invoke("spec-regeneration:generate", {
- projectPath,
- projectDefinition,
- }),
-
- // Generate features from existing app_spec.txt
- generateFeatures: (projectPath) =>
- ipcRenderer.invoke("spec-regeneration:generate-features", {
- projectPath,
- }),
-
- // Stop regenerating spec
- stop: () => ipcRenderer.invoke("spec-regeneration:stop"),
-
- // Get regeneration status
- status: () => ipcRenderer.invoke("spec-regeneration:status"),
-
- // Listen for regeneration events
- onEvent: (callback) => {
- const subscription = (_, data) => callback(data);
- ipcRenderer.on("spec-regeneration:event", subscription);
-
- // Return unsubscribe function
- return () => {
- ipcRenderer.removeListener("spec-regeneration:event", subscription);
- };
- },
- },
-
- // Setup & CLI Management API
- setup: {
- // Get comprehensive Claude CLI status
- getClaudeStatus: () => ipcRenderer.invoke("setup:claude-status"),
-
- // Get comprehensive Codex CLI status
- getCodexStatus: () => ipcRenderer.invoke("setup:codex-status"),
-
- // Install Claude CLI
- installClaude: () => ipcRenderer.invoke("setup:install-claude"),
-
- // Install Codex CLI
- installCodex: () => ipcRenderer.invoke("setup:install-codex"),
-
- // Authenticate Claude CLI
- authClaude: () => ipcRenderer.invoke("setup:auth-claude"),
-
- // Authenticate Codex CLI with optional API key
- authCodex: (apiKey) => ipcRenderer.invoke("setup:auth-codex", { apiKey }),
-
- // Store API key securely
- storeApiKey: (provider, apiKey) =>
- ipcRenderer.invoke("setup:store-api-key", { provider, apiKey }),
-
- // Get stored API keys status
- getApiKeys: () => ipcRenderer.invoke("setup:get-api-keys"),
-
- // Configure Codex MCP server for a project
- configureCodexMcp: (projectPath) =>
- ipcRenderer.invoke("setup:configure-codex-mcp", { projectPath }),
-
- // Get platform information
- getPlatform: () => ipcRenderer.invoke("setup:get-platform"),
-
- // Listen for installation progress
- onInstallProgress: (callback) => {
- const subscription = (_, data) => callback(data);
- ipcRenderer.on("setup:install-progress", subscription);
- return () => {
- ipcRenderer.removeListener("setup:install-progress", subscription);
- };
- },
-
- // Listen for auth progress
- onAuthProgress: (callback) => {
- const subscription = (_, data) => callback(data);
- ipcRenderer.on("setup:auth-progress", subscription);
- return () => {
- ipcRenderer.removeListener("setup:auth-progress", subscription);
- };
- },
- },
-
- // Features API
- features: {
- // Get all features for a project
- getAll: (projectPath) =>
- ipcRenderer.invoke("features:getAll", { projectPath }),
-
- // Get a single feature by ID
- get: (projectPath, featureId) =>
- ipcRenderer.invoke("features:get", { projectPath, featureId }),
-
- // Create a new feature
- create: (projectPath, feature) =>
- ipcRenderer.invoke("features:create", { projectPath, feature }),
-
- // Update a feature (partial updates supported)
- update: (projectPath, featureId, updates) =>
- ipcRenderer.invoke("features:update", {
- projectPath,
- featureId,
- updates,
- }),
-
- // Delete a feature and its folder
- delete: (projectPath, featureId) =>
- ipcRenderer.invoke("features:delete", { projectPath, featureId }),
-
- // Get agent output for a feature
- getAgentOutput: (projectPath, featureId) =>
- ipcRenderer.invoke("features:getAgentOutput", { projectPath, featureId }),
- },
-
- // Running Agents API
- runningAgents: {
- // Get all running agents across all projects
- getAll: () => ipcRenderer.invoke("running-agents:getAll"),
- },
-});
-
-// Also expose a flag to detect if we're in Electron
+// Only expose a flag to detect Electron environment
+// All API calls go through HTTP to the backend server
contextBridge.exposeInMainWorld("isElectron", true);
+
+// Expose platform info for UI purposes
+contextBridge.exposeInMainWorld("electronPlatform", process.platform);
+
+console.log("[Preload] Electron flag exposed (HTTP-only mode)");
diff --git a/apps/app/src/app/globals.css b/apps/app/src/app/globals.css
index 36444bdb..2f7dc659 100644
--- a/apps/app/src/app/globals.css
+++ b/apps/app/src/app/globals.css
@@ -143,6 +143,80 @@
--running-indicator-text: oklch(0.6 0.22 265);
}
+/* Apply dark mode immediately based on system preference (before JS runs) */
+@media (prefers-color-scheme: dark) {
+ :root {
+ /* Deep dark backgrounds - zinc-950 family */
+ --background: oklch(0.04 0 0); /* zinc-950 */
+ --background-50: oklch(0.04 0 0 / 0.5); /* zinc-950/50 */
+ --background-80: oklch(0.04 0 0 / 0.8); /* zinc-950/80 */
+
+ /* Text colors following hierarchy */
+ --foreground: oklch(1 0 0); /* text-white */
+ --foreground-secondary: oklch(0.588 0 0); /* text-zinc-400 */
+ --foreground-muted: oklch(0.525 0 0); /* text-zinc-500 */
+
+ /* Card and popover backgrounds */
+ --card: oklch(0.14 0 0);
+ --card-foreground: oklch(1 0 0);
+ --popover: oklch(0.10 0 0);
+ --popover-foreground: oklch(1 0 0);
+
+ /* Brand colors - purple/violet theme */
+ --primary: oklch(0.55 0.25 265);
+ --primary-foreground: oklch(1 0 0);
+ --brand-400: oklch(0.6 0.22 265);
+ --brand-500: oklch(0.55 0.25 265);
+ --brand-600: oklch(0.5 0.28 270);
+
+ /* Glass morphism borders and accents */
+ --secondary: oklch(1 0 0 / 0.05);
+ --secondary-foreground: oklch(1 0 0);
+ --muted: oklch(0.176 0 0);
+ --muted-foreground: oklch(0.588 0 0);
+ --accent: oklch(1 0 0 / 0.1);
+ --accent-foreground: oklch(1 0 0);
+
+ /* Borders with transparency for glass effect */
+ --border: oklch(0.176 0 0);
+ --border-glass: oklch(1 0 0 / 0.1);
+ --destructive: oklch(0.6 0.25 25);
+ --input: oklch(0.04 0 0 / 0.8);
+ --ring: oklch(0.55 0.25 265);
+
+ /* Chart colors with brand theme */
+ --chart-1: oklch(0.55 0.25 265);
+ --chart-2: oklch(0.65 0.2 160);
+ --chart-3: oklch(0.75 0.2 70);
+ --chart-4: oklch(0.6 0.25 300);
+ --chart-5: oklch(0.6 0.25 20);
+
+ /* Sidebar with glass morphism */
+ --sidebar: oklch(0.04 0 0 / 0.5);
+ --sidebar-foreground: oklch(1 0 0);
+ --sidebar-primary: oklch(0.55 0.25 265);
+ --sidebar-primary-foreground: oklch(1 0 0);
+ --sidebar-accent: oklch(1 0 0 / 0.05);
+ --sidebar-accent-foreground: oklch(1 0 0);
+ --sidebar-border: oklch(1 0 0 / 0.1);
+ --sidebar-ring: oklch(0.55 0.25 265);
+
+ /* Action button colors */
+ --action-view: oklch(0.6 0.25 265);
+ --action-view-hover: oklch(0.55 0.27 270);
+ --action-followup: oklch(0.6 0.2 230);
+ --action-followup-hover: oklch(0.55 0.22 230);
+ --action-commit: oklch(0.55 0.2 140);
+ --action-commit-hover: oklch(0.5 0.22 140);
+ --action-verify: oklch(0.55 0.2 140);
+ --action-verify-hover: oklch(0.5 0.22 140);
+
+ /* Running indicator - Purple */
+ --running-indicator: oklch(0.6 0.25 265);
+ --running-indicator-text: oklch(0.65 0.22 265);
+ }
+}
+
.light {
/* Explicit light mode - same as root but ensures it overrides any dark defaults */
--background: oklch(1 0 0); /* White */
@@ -211,10 +285,10 @@
--foreground-secondary: oklch(0.588 0 0); /* text-zinc-400 */
--foreground-muted: oklch(0.525 0 0); /* text-zinc-500 */
- /* Glass morphism effects */
- --card: oklch(0.04 0 0 / 0.5); /* zinc-950/50 with transparency */
+ /* Card and popover backgrounds */
+ --card: oklch(0.14 0 0); /* slightly lighter than background for contrast */
--card-foreground: oklch(1 0 0);
- --popover: oklch(0.04 0 0 / 0.8); /* zinc-950/80 for popover */
+ --popover: oklch(0.10 0 0); /* slightly lighter than background */
--popover-foreground: oklch(1 0 0);
/* Brand colors - purple/violet theme */
diff --git a/apps/app/src/app/layout.tsx b/apps/app/src/app/layout.tsx
index 2d7df503..b303aeec 100644
--- a/apps/app/src/app/layout.tsx
+++ b/apps/app/src/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Toaster } from "sonner";
+import { CoursePromoBadge } from "@/components/ui/course-promo-badge";
import "./globals.css";
export const metadata: Metadata = {
title: "Automaker - Autonomous AI Development Studio",
@@ -20,6 +21,7 @@ export default function RootLayout({
>
{children}
+