Files
automaker/app/electron/main.js
Cody Seibert 72cc43d02f feat(feature-suggestions): implement feature suggestions and spec regeneration functionality
- Introduced a new `FeatureSuggestionsService` to analyze projects and generate feature suggestions based on the project structure and existing features.
- Added IPC handlers for generating and stopping feature suggestions, as well as checking their status.
- Implemented a `SpecRegenerationService` to create and regenerate application specifications based on user-defined project overviews and definitions.
- Enhanced the UI with a `FeatureSuggestionsDialog` for displaying generated suggestions and allowing users to import them into their project.
- Updated the sidebar and board view components to integrate feature suggestions and spec regeneration functionalities, improving project management capabilities.

These changes significantly enhance the application's ability to assist users in feature planning and specification management.
2025-12-10 08:51:33 -05:00

895 lines
24 KiB
JavaScript

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 featureSuggestionsService = require("./services/feature-suggestions-service");
const specRegenerationService = require("./services/spec-regeneration-service");
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");
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
minHeight: 700,
icon: getIconPath(),
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
titleBarStyle: "hiddenInset",
backgroundColor: "#0a0a0a",
});
// Load Next.js dev server in development or production build
const isDev = !app.isPackaged;
if (isDev) {
mainWindow.loadURL("http://localhost:3007");
// mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
}
mainWindow.on("closed", () => {
mainWindow = null;
});
}
app.whenReady().then(async () => {
// 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);
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
// IPC Handlers
// Dialog handlers
ipcMain.handle("dialog:openDirectory", async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory", "createDirectory"],
});
return result;
});
ipcMain.handle("dialog:openFile", async (_, options = {}) => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openFile"],
...options,
});
return result;
});
// File system handlers
ipcMain.handle("fs:readFile", async (_, filePath) => {
try {
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 {
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 {
await fs.mkdir(dirPath, { recursive: true });
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle("fs:readdir", async (_, dirPath) => {
try {
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 {
await fs.access(filePath);
return true;
} catch {
return false;
}
});
ipcMain.handle("fs:stat", async (_, filePath) => {
try {
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 {
await fs.unlink(filePath);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle("fs:trashItem", async (_, targetPath) => {
try {
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");
console.log("[IPC] Saved image to .automaker/images:", imageFilePath);
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";
});
// ============================================================================
// 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 {
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 {
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
*/
ipcMain.handle("auto-mode:stop", async () => {
try {
return await autoModeService.stop();
} catch (error) {
console.error("[IPC] auto-mode:stop error:", error);
return { success: false, error: error.message };
}
});
/**
* Get auto mode status
*/
ipcMain.handle("auto-mode:status", () => {
try {
return { success: true, ...autoModeService.getStatus() };
} 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 }) => {
try {
const sendToRenderer = (data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("auto-mode:event", data);
}
};
return await autoModeService.runFeature({
projectPath,
featureId,
sendToRenderer,
});
} 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 }) => {
console.log("[IPC] auto-mode:verify-feature called with:", {
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 }) => {
console.log("[IPC] auto-mode:resume-feature called with:", {
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 }) => {
console.log("[IPC] auto-mode:analyze-project called with:", { projectPath });
try {
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 }) => {
console.log("[IPC] auto-mode:stop-feature called with:", { 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 }) => {
console.log("[IPC] auto-mode:follow-up-feature called with:", {
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 }) => {
console.log("[IPC] auto-mode:commit-feature called with:", {
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 };
}
}
);
// ============================================================================
// Feature Suggestions IPC Handlers
// ============================================================================
// Track running suggestions analysis
let suggestionsExecution = null;
/**
* Generate feature suggestions by analyzing the project
*/
ipcMain.handle(
"suggestions:generate",
async (_, { projectPath }) => {
console.log("[IPC] suggestions:generate called with:", { projectPath });
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)
.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 () => {
console.log("[IPC] suggestions:stop called");
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(),
};
});
// ============================================================================
// 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 }) => {
console.log("[IPC] spec-regeneration:generate called with:", { projectPath });
try {
// 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 () => {
console.log("[IPC] spec-regeneration:stop called");
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(),
};
});
/**
* Create initial app spec for a new project
*/
ipcMain.handle(
"spec-regeneration:create",
async (_, { projectPath, projectOverview, generateFeatures = true }) => {
console.log("[IPC] spec-regeneration:create called with:", { projectPath, generateFeatures });
try {
// 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 };
}
}
);