cbd47f2c3a
* fix(insights): await async sendMessage to prevent race condition (#613) The IPC handler for INSIGHTS_SEND_MESSAGE was declared async but never awaited the sendMessage() call. This caused race conditions where async environment setup (getAPIProfileEnv) wouldn't complete before the Python process was spawned. On Windows especially, this led to "Process exited with code 1" errors because environment variables weren't set in time. The fix adds await to ensure all async operations complete before returning, and wraps in try/catch to prevent unhandled rejections. Fixes #613 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: send errors to UI in catch block Address Gemini Code Assist feedback - errors caught in the try/catch block are now also sent to the renderer process via IPC_CHANNELS.INSIGHTS_ERROR. This ensures all error types (not just executor errors) are reported to the UI. Co-authored-by: gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: add config.json to gitignore Prevents worktree configuration files from being accidentally committed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alex <63423455+AlexMadera@users.noreply.github.com>
316 lines
10 KiB
TypeScript
316 lines
10 KiB
TypeScript
import { ipcMain } from 'electron';
|
|
import type { BrowserWindow } from 'electron';
|
|
import path from 'path';
|
|
import { existsSync, readdirSync, mkdirSync, writeFileSync } from 'fs';
|
|
import { IPC_CHANNELS, getSpecsDir, AUTO_BUILD_PATHS } from '../../shared/constants';
|
|
import type { IPCResult, InsightsSession, InsightsSessionSummary, InsightsModelConfig, Task, TaskMetadata } from '../../shared/types';
|
|
import { projectStore } from '../project-store';
|
|
import { insightsService } from '../insights-service';
|
|
|
|
/**
|
|
* Register all insights-related IPC handlers
|
|
*/
|
|
export function registerInsightsHandlers(
|
|
getMainWindow: () => BrowserWindow | null
|
|
): void {
|
|
// ============================================
|
|
// Insights Operations
|
|
// ============================================
|
|
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_GET_SESSION,
|
|
async (_, projectId: string): Promise<IPCResult<InsightsSession | null>> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const session = insightsService.loadSession(projectId, project.path);
|
|
return { success: true, data: session };
|
|
}
|
|
);
|
|
|
|
ipcMain.on(
|
|
IPC_CHANNELS.INSIGHTS_SEND_MESSAGE,
|
|
async (_, projectId: string, message: string, modelConfig?: InsightsModelConfig) => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_ERROR, projectId, 'Project not found');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Await the async sendMessage to ensure proper error handling and
|
|
// that all async operations (like getProcessEnv) complete before
|
|
// the handler returns. This fixes race conditions on Windows where
|
|
// environment setup wouldn't complete before process spawn.
|
|
try {
|
|
await insightsService.sendMessage(projectId, project.path, message, modelConfig);
|
|
} catch (error) {
|
|
// Errors during sendMessage (executor errors) are already emitted via
|
|
// the 'error' event, but we catch here to prevent unhandled rejection
|
|
// and ensure all error types are reported to the UI
|
|
console.error('[Insights IPC] Error in sendMessage:', error);
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
mainWindow.webContents.send(
|
|
IPC_CHANNELS.INSIGHTS_ERROR,
|
|
projectId,
|
|
`Failed to send message: ${errorMessage}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_CLEAR_SESSION,
|
|
async (_, projectId: string): Promise<IPCResult> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
insightsService.clearSession(projectId, project.path);
|
|
return { success: true };
|
|
}
|
|
);
|
|
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_CREATE_TASK,
|
|
async (
|
|
_,
|
|
projectId: string,
|
|
title: string,
|
|
description: string,
|
|
metadata?: TaskMetadata
|
|
): Promise<IPCResult<Task>> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
if (!project.autoBuildPath) {
|
|
return { success: false, error: 'Auto Claude not initialized for this project' };
|
|
}
|
|
|
|
try {
|
|
// Generate a unique spec ID based on existing specs
|
|
// Get specs directory path
|
|
const specsBaseDir = getSpecsDir(project.autoBuildPath);
|
|
const specsDir = path.join(project.path, specsBaseDir);
|
|
|
|
// Find next available spec number
|
|
let specNumber = 1;
|
|
if (existsSync(specsDir)) {
|
|
const existingDirs = readdirSync(specsDir, { withFileTypes: true })
|
|
.filter(d => d.isDirectory())
|
|
.map(d => d.name);
|
|
|
|
const existingNumbers = existingDirs
|
|
.map(name => {
|
|
const match = name.match(/^(\d+)/);
|
|
return match ? parseInt(match[1], 10) : 0;
|
|
})
|
|
.filter(n => n > 0);
|
|
|
|
if (existingNumbers.length > 0) {
|
|
specNumber = Math.max(...existingNumbers) + 1;
|
|
}
|
|
}
|
|
|
|
// Create spec ID with zero-padded number and slugified title
|
|
const slugifiedTitle = title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
.substring(0, 50);
|
|
const specId = `${String(specNumber).padStart(3, '0')}-${slugifiedTitle}`;
|
|
|
|
// Create spec directory
|
|
const specDir = path.join(specsDir, specId);
|
|
mkdirSync(specDir, { recursive: true });
|
|
|
|
// Build metadata with source type
|
|
const taskMetadata: TaskMetadata = {
|
|
sourceType: 'insights',
|
|
...metadata
|
|
};
|
|
|
|
// Create initial implementation_plan.json
|
|
const now = new Date().toISOString();
|
|
const implementationPlan = {
|
|
feature: title,
|
|
description: description,
|
|
created_at: now,
|
|
updated_at: now,
|
|
status: 'pending',
|
|
phases: []
|
|
};
|
|
|
|
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
|
|
writeFileSync(planPath, JSON.stringify(implementationPlan, null, 2));
|
|
|
|
// Save task metadata
|
|
const metadataPath = path.join(specDir, 'task_metadata.json');
|
|
writeFileSync(metadataPath, JSON.stringify(taskMetadata, null, 2));
|
|
|
|
// Create the task object
|
|
const task: Task = {
|
|
id: specId,
|
|
specId: specId,
|
|
projectId,
|
|
title,
|
|
description,
|
|
status: 'backlog',
|
|
subtasks: [],
|
|
logs: [],
|
|
metadata: taskMetadata,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
return { success: true, data: task };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to create task'
|
|
};
|
|
}
|
|
}
|
|
);
|
|
|
|
// List all sessions for a project
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_LIST_SESSIONS,
|
|
async (_, projectId: string): Promise<IPCResult<InsightsSessionSummary[]>> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const sessions = insightsService.listSessions(project.path);
|
|
return { success: true, data: sessions };
|
|
}
|
|
);
|
|
|
|
// Create a new session
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_NEW_SESSION,
|
|
async (_, projectId: string): Promise<IPCResult<InsightsSession>> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const session = insightsService.createNewSession(projectId, project.path);
|
|
return { success: true, data: session };
|
|
}
|
|
);
|
|
|
|
// Switch to a different session
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_SWITCH_SESSION,
|
|
async (_, projectId: string, sessionId: string): Promise<IPCResult<InsightsSession | null>> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const session = insightsService.switchSession(projectId, project.path, sessionId);
|
|
return { success: true, data: session };
|
|
}
|
|
);
|
|
|
|
// Delete a session
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_DELETE_SESSION,
|
|
async (_, projectId: string, sessionId: string): Promise<IPCResult> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const success = insightsService.deleteSession(projectId, project.path, sessionId);
|
|
if (success) {
|
|
return { success: true };
|
|
}
|
|
return { success: false, error: 'Failed to delete session' };
|
|
}
|
|
);
|
|
|
|
// Rename a session
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_RENAME_SESSION,
|
|
async (_, projectId: string, sessionId: string, newTitle: string): Promise<IPCResult> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const success = insightsService.renameSession(project.path, sessionId, newTitle);
|
|
if (success) {
|
|
return { success: true };
|
|
}
|
|
return { success: false, error: 'Failed to rename session' };
|
|
}
|
|
);
|
|
|
|
// Update model configuration for a session
|
|
ipcMain.handle(
|
|
IPC_CHANNELS.INSIGHTS_UPDATE_MODEL_CONFIG,
|
|
async (_, projectId: string, sessionId: string, modelConfig: InsightsModelConfig): Promise<IPCResult> => {
|
|
const project = projectStore.getProject(projectId);
|
|
if (!project) {
|
|
return { success: false, error: 'Project not found' };
|
|
}
|
|
|
|
const success = insightsService.updateSessionModelConfig(project.path, sessionId, modelConfig);
|
|
if (success) {
|
|
return { success: true };
|
|
}
|
|
return { success: false, error: 'Failed to update model configuration' };
|
|
}
|
|
);
|
|
|
|
// ============================================
|
|
// Insights Event Forwarding (Service -> Renderer)
|
|
// ============================================
|
|
|
|
// Forward streaming chunks to renderer
|
|
insightsService.on('stream-chunk', (projectId: string, chunk: unknown) => {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_STREAM_CHUNK, projectId, chunk);
|
|
}
|
|
});
|
|
|
|
// Forward status updates to renderer
|
|
insightsService.on('status', (projectId: string, status: unknown) => {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_STATUS, projectId, status);
|
|
}
|
|
});
|
|
|
|
// Forward errors to renderer
|
|
insightsService.on('error', (projectId: string, error: string) => {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_ERROR, projectId, error);
|
|
}
|
|
});
|
|
|
|
// Forward SDK rate limit events to renderer
|
|
insightsService.on('sdk-rate-limit', (rateLimitInfo: unknown) => {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send(IPC_CHANNELS.CLAUDE_SDK_RATE_LIMIT, rateLimitInfo);
|
|
}
|
|
});
|
|
|
|
}
|