feat(issues): rewrite investigation IPC handlers for full lifecycle (F3)
Replace the old single-handler investigation flow with the complete 7-channel IPC handler system. Adds start/cancel investigation with subprocess management, create-task from report, dismiss, post to GitHub, and get/save investigation settings. Legacy handler retained for backwards compatibility. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cda24a3e71
commit
899ebf6848
@@ -1,21 +1,104 @@
|
||||
/**
|
||||
* GitHub issue investigation IPC handlers
|
||||
*
|
||||
* Handles the full investigation lifecycle:
|
||||
* - Start investigation (spawn Python orchestrator subprocess)
|
||||
* - Cancel investigation (kill running subprocess)
|
||||
* - Create task from investigation report
|
||||
* - Dismiss issue
|
||||
* - Post investigation results to GitHub
|
||||
* - Get/save investigation settings
|
||||
*
|
||||
* Also retains the legacy GITHUB_INVESTIGATE_ISSUE handler for backwards
|
||||
* compatibility with the old one-shot investigation flow.
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import { IPC_CHANNELS } from '../../../shared/constants';
|
||||
import type { GitHubInvestigationResult, GitHubInvestigationStatus } from '../../../shared/types';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { IPC_CHANNELS, MODEL_ID_MAP, DEFAULT_FEATURE_MODELS, DEFAULT_FEATURE_THINKING } from '../../../shared/constants';
|
||||
import type {
|
||||
GitHubInvestigationResult,
|
||||
GitHubInvestigationStatus,
|
||||
InvestigationProgress,
|
||||
InvestigationResult,
|
||||
InvestigationSettings,
|
||||
InvestigationDismissReason,
|
||||
} from '../../../shared/types';
|
||||
import type { AuthFailureInfo } from '../../../shared/types/terminal';
|
||||
import type { AppSettings } from '../../../shared/types';
|
||||
import { projectStore } from '../../project-store';
|
||||
import { writeJsonWithRetry } from '../../utils/atomic-file';
|
||||
import { readSettingsFile } from '../../settings-utils';
|
||||
import { AgentManager } from '../../agent';
|
||||
import { getGitHubConfig, githubFetch } from './utils';
|
||||
import type { GitHubAPIComment } from './types';
|
||||
import { createSpecForIssue, buildIssueContext, buildInvestigationTask } from './spec-utils';
|
||||
import { createContextLogger } from './utils/logger';
|
||||
import { withProjectOrNull } from './utils/project-middleware';
|
||||
import { createIPCCommunicators } from './utils/ipc-communicator';
|
||||
import { getRunnerEnv } from './utils/runner-env';
|
||||
import {
|
||||
runPythonSubprocess,
|
||||
getPythonPath,
|
||||
getRunnerPath,
|
||||
validateGitHubModule,
|
||||
buildRunnerArgs,
|
||||
parseJSONFromOutput,
|
||||
} from './utils/subprocess-runner';
|
||||
import { killProcessGracefully } from '../../platform';
|
||||
|
||||
const { debug: debugLog } = createContextLogger('Investigation');
|
||||
|
||||
// Track active investigation subprocesses, keyed by `${projectId}:${issueNumber}`
|
||||
const activeInvestigations = new Map<string, ChildProcess>();
|
||||
|
||||
/**
|
||||
* Send investigation progress update to renderer
|
||||
* Get GitHub Issues model and thinking settings from app settings
|
||||
*/
|
||||
function sendProgress(
|
||||
function getGitHubIssuesSettings(): { model: string; thinkingLevel: string } {
|
||||
const rawSettings = readSettingsFile() as Partial<AppSettings> | undefined;
|
||||
const featureModels = rawSettings?.featureModels ?? DEFAULT_FEATURE_MODELS;
|
||||
const featureThinking = rawSettings?.featureThinking ?? DEFAULT_FEATURE_THINKING;
|
||||
const modelShort = featureModels.githubIssues ?? DEFAULT_FEATURE_MODELS.githubIssues;
|
||||
const thinkingLevel = featureThinking.githubIssues ?? DEFAULT_FEATURE_THINKING.githubIssues;
|
||||
const model = MODEL_ID_MAP[modelShort] ?? MODEL_ID_MAP['opus'];
|
||||
return { model, thinkingLevel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GitHub config directory for a project
|
||||
*/
|
||||
function getGitHubDir(projectPath: string): string {
|
||||
return path.join(projectPath, '.auto-claude', 'github');
|
||||
}
|
||||
|
||||
/**
|
||||
* Default investigation settings
|
||||
*/
|
||||
function createDefaultSettings(): InvestigationSettings {
|
||||
return {
|
||||
autoCreateTasks: false,
|
||||
autoStartTasks: false,
|
||||
pipelineMode: 'full',
|
||||
autoPostToGitHub: false,
|
||||
autoCloseIssues: false,
|
||||
maxParallelInvestigations: 3,
|
||||
labelIncludeFilter: [],
|
||||
labelExcludeFilter: [],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Legacy handler (old one-shot investigation)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Send investigation progress update to renderer (legacy)
|
||||
*/
|
||||
function sendLegacyProgress(
|
||||
mainWindow: BrowserWindow,
|
||||
projectId: string,
|
||||
status: GitHubInvestigationStatus
|
||||
@@ -28,9 +111,9 @@ function sendProgress(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send investigation error to renderer
|
||||
* Send investigation error to renderer (legacy)
|
||||
*/
|
||||
function sendError(
|
||||
function sendLegacyError(
|
||||
mainWindow: BrowserWindow,
|
||||
projectId: string,
|
||||
error: string
|
||||
@@ -43,9 +126,9 @@ function sendError(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send investigation completion to renderer
|
||||
* Send investigation completion to renderer (legacy)
|
||||
*/
|
||||
function sendComplete(
|
||||
function sendLegacyComplete(
|
||||
mainWindow: BrowserWindow,
|
||||
projectId: string,
|
||||
result: GitHubInvestigationResult
|
||||
@@ -58,9 +141,9 @@ function sendComplete(
|
||||
}
|
||||
|
||||
/**
|
||||
* Investigate a GitHub issue and create a task
|
||||
* Legacy: Investigate a GitHub issue and create a task (old one-shot flow)
|
||||
*/
|
||||
export function registerInvestigateIssue(
|
||||
function registerLegacyInvestigateIssue(
|
||||
_agentManager: AgentManager,
|
||||
getMainWindow: () => BrowserWindow | null
|
||||
): void {
|
||||
@@ -72,26 +155,24 @@ export function registerInvestigateIssue(
|
||||
|
||||
const project = projectStore.getProject(projectId);
|
||||
if (!project) {
|
||||
sendError(mainWindow, projectId, 'Project not found');
|
||||
sendLegacyError(mainWindow, projectId, 'Project not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getGitHubConfig(project);
|
||||
if (!config) {
|
||||
sendError(mainWindow, projectId, 'No GitHub token or repository configured');
|
||||
sendLegacyError(mainWindow, projectId, 'No GitHub token or repository configured');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Phase 1: Fetching issue details
|
||||
sendProgress(mainWindow, projectId, {
|
||||
sendLegacyProgress(mainWindow, projectId, {
|
||||
phase: 'fetching',
|
||||
issueNumber,
|
||||
progress: 10,
|
||||
message: 'Fetching issue details...'
|
||||
});
|
||||
|
||||
// Fetch the issue
|
||||
const issue = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}/issues/${issueNumber}`
|
||||
@@ -103,19 +184,15 @@ export function registerInvestigateIssue(
|
||||
html_url: string;
|
||||
};
|
||||
|
||||
// Fetch issue comments for more context
|
||||
const allComments = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}/issues/${issueNumber}/comments`
|
||||
) as GitHubAPIComment[];
|
||||
|
||||
// Filter comments based on selection (if provided)
|
||||
// Use Array.isArray to handle empty array case (all comments deselected)
|
||||
const comments = Array.isArray(selectedCommentIds)
|
||||
? allComments.filter(c => selectedCommentIds.includes(c.id))
|
||||
: allComments;
|
||||
|
||||
// Build context for the AI investigation
|
||||
const labels = issue.labels.map(l => l.name);
|
||||
const issueContext = buildIssueContext(
|
||||
issue.number,
|
||||
@@ -126,22 +203,19 @@ export function registerInvestigateIssue(
|
||||
comments
|
||||
);
|
||||
|
||||
// Phase 2: Analyzing issue
|
||||
sendProgress(mainWindow, projectId, {
|
||||
sendLegacyProgress(mainWindow, projectId, {
|
||||
phase: 'analyzing',
|
||||
issueNumber,
|
||||
progress: 30,
|
||||
message: 'AI is analyzing the issue...'
|
||||
});
|
||||
|
||||
// Build task description
|
||||
const taskDescription = buildInvestigationTask(
|
||||
issue.number,
|
||||
issue.title,
|
||||
issueContext
|
||||
);
|
||||
|
||||
// Create spec directory and files (with coordinated numbering)
|
||||
const specData = await createSpecForIssue(
|
||||
project,
|
||||
issue.number,
|
||||
@@ -149,22 +223,16 @@ export function registerInvestigateIssue(
|
||||
taskDescription,
|
||||
issue.html_url,
|
||||
labels,
|
||||
project.settings?.mainBranch // Pass project's configured main branch
|
||||
project.settings?.mainBranch
|
||||
);
|
||||
|
||||
// NOTE: We intentionally do NOT call agentManager.startSpecCreation() here
|
||||
// This allows the task to stay in "backlog" status until the user manually starts it
|
||||
// Previously, calling startSpecCreation would auto-start the task immediately
|
||||
|
||||
// Phase 3: Creating task
|
||||
sendProgress(mainWindow, projectId, {
|
||||
sendLegacyProgress(mainWindow, projectId, {
|
||||
phase: 'creating_task',
|
||||
issueNumber,
|
||||
progress: 70,
|
||||
message: 'Creating task from investigation...'
|
||||
});
|
||||
|
||||
// Build investigation result
|
||||
const investigationResult: GitHubInvestigationResult = {
|
||||
success: true,
|
||||
issueNumber,
|
||||
@@ -182,18 +250,17 @@ export function registerInvestigateIssue(
|
||||
taskId: specData.specId
|
||||
};
|
||||
|
||||
// Phase 4: Complete
|
||||
sendProgress(mainWindow, projectId, {
|
||||
sendLegacyProgress(mainWindow, projectId, {
|
||||
phase: 'complete',
|
||||
issueNumber,
|
||||
progress: 100,
|
||||
message: 'Investigation complete!'
|
||||
});
|
||||
|
||||
sendComplete(mainWindow, projectId, investigationResult);
|
||||
sendLegacyComplete(mainWindow, projectId, investigationResult);
|
||||
|
||||
} catch (error) {
|
||||
sendError(
|
||||
sendLegacyError(
|
||||
mainWindow,
|
||||
projectId,
|
||||
error instanceof Error ? error.message : 'Failed to investigate issue'
|
||||
@@ -203,6 +270,10 @@ export function registerInvestigateIssue(
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// New investigation system handlers
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Register all investigation-related handlers
|
||||
*/
|
||||
@@ -210,5 +281,402 @@ export function registerInvestigationHandlers(
|
||||
agentManager: AgentManager,
|
||||
getMainWindow: () => BrowserWindow | null
|
||||
): void {
|
||||
registerInvestigateIssue(agentManager, getMainWindow);
|
||||
debugLog('Registering Investigation handlers');
|
||||
|
||||
// Keep legacy handler for backwards compatibility
|
||||
registerLegacyInvestigateIssue(agentManager, getMainWindow);
|
||||
|
||||
// ============================================
|
||||
// 1. Start investigation
|
||||
// ============================================
|
||||
ipcMain.on(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_START,
|
||||
async (_, projectId: string, issueNumber: number) => {
|
||||
debugLog('startInvestigation handler called', { projectId, issueNumber });
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) return;
|
||||
|
||||
const { sendProgress, sendError, sendComplete } = createIPCCommunicators<
|
||||
InvestigationProgress,
|
||||
InvestigationResult
|
||||
>(
|
||||
mainWindow,
|
||||
{
|
||||
progress: IPC_CHANNELS.GITHUB_INVESTIGATION_PROGRESS,
|
||||
error: IPC_CHANNELS.GITHUB_INVESTIGATION_ERROR,
|
||||
complete: IPC_CHANNELS.GITHUB_INVESTIGATION_COMPLETE,
|
||||
},
|
||||
projectId,
|
||||
);
|
||||
|
||||
if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
|
||||
sendError('Invalid issue number');
|
||||
return;
|
||||
}
|
||||
|
||||
const processKey = `${projectId}:${issueNumber}`;
|
||||
|
||||
// Cancel any existing investigation for this issue
|
||||
const existingProcess = activeInvestigations.get(processKey);
|
||||
if (existingProcess && !existingProcess.killed) {
|
||||
killProcessGracefully(existingProcess);
|
||||
activeInvestigations.delete(processKey);
|
||||
}
|
||||
|
||||
try {
|
||||
await withProjectOrNull(projectId, async (project) => {
|
||||
const validation = await validateGitHubModule(project);
|
||||
if (!validation.valid) {
|
||||
sendError(validation.error ?? 'GitHub module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const backendPath = validation.backendPath ?? '';
|
||||
const { model, thinkingLevel } = getGitHubIssuesSettings();
|
||||
|
||||
const args = buildRunnerArgs(
|
||||
getRunnerPath(backendPath),
|
||||
project.path,
|
||||
'investigate',
|
||||
[String(issueNumber)],
|
||||
{ model, thinkingLevel },
|
||||
);
|
||||
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
sendProgress({
|
||||
issueNumber,
|
||||
phase: 'starting',
|
||||
progress: 5,
|
||||
message: 'Starting investigation...',
|
||||
agentStatuses: [
|
||||
{ agentType: 'root_cause', status: 'pending', progress: 0 },
|
||||
{ agentType: 'impact', status: 'pending', progress: 0 },
|
||||
{ agentType: 'fix_advisor', status: 'pending', progress: 0 },
|
||||
{ agentType: 'reproducer', status: 'pending', progress: 0 },
|
||||
],
|
||||
startedAt,
|
||||
});
|
||||
|
||||
const subprocessEnv = await getRunnerEnv();
|
||||
const { process: childProcess, promise } = runPythonSubprocess<InvestigationResult>({
|
||||
pythonPath: getPythonPath(backendPath),
|
||||
args,
|
||||
cwd: backendPath,
|
||||
env: subprocessEnv,
|
||||
onProgress: (percent, message, data) => {
|
||||
// Parse agent status updates from progress data if available
|
||||
const progressUpdate: InvestigationProgress = {
|
||||
issueNumber,
|
||||
phase: percent < 90 ? 'investigating' : 'finalizing',
|
||||
progress: percent,
|
||||
message,
|
||||
agentStatuses: (data as InvestigationProgress | undefined)?.agentStatuses ?? [],
|
||||
startedAt,
|
||||
};
|
||||
sendProgress(progressUpdate);
|
||||
},
|
||||
onComplete: (stdout) => parseJSONFromOutput<InvestigationResult>(stdout),
|
||||
onStdout: (line) => debugLog('STDOUT:', line),
|
||||
onStderr: (line) => debugLog('STDERR:', line),
|
||||
onAuthFailure: (authFailureInfo: AuthFailureInfo) => {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.CLAUDE_AUTH_FAILURE, authFailureInfo);
|
||||
},
|
||||
});
|
||||
|
||||
activeInvestigations.set(processKey, childProcess);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await promise;
|
||||
} finally {
|
||||
activeInvestigations.delete(processKey);
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
sendError(result.error ?? 'Investigation failed');
|
||||
return;
|
||||
}
|
||||
|
||||
const investigationResult = result.data as InvestigationResult;
|
||||
sendComplete(investigationResult);
|
||||
});
|
||||
} catch (error) {
|
||||
sendError(error instanceof Error ? error.message : 'Failed to start investigation');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 2. Cancel investigation
|
||||
// ============================================
|
||||
ipcMain.on(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_CANCEL,
|
||||
(_, projectId: string, issueNumber: number) => {
|
||||
debugLog('cancelInvestigation handler called', { projectId, issueNumber });
|
||||
const processKey = `${projectId}:${issueNumber}`;
|
||||
const proc = activeInvestigations.get(processKey);
|
||||
|
||||
if (proc && !proc.killed) {
|
||||
killProcessGracefully(proc);
|
||||
debugLog('Investigation process killed', { processKey });
|
||||
}
|
||||
|
||||
activeInvestigations.delete(processKey);
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 3. Create task from investigation
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_CREATE_TASK,
|
||||
async (_, projectId: string, issueNumber: number) => {
|
||||
debugLog('createTaskFromInvestigation handler called', { projectId, issueNumber });
|
||||
|
||||
try {
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
// Read investigation report from persistence
|
||||
const reportPath = path.join(
|
||||
getGitHubDir(project.path),
|
||||
'investigations',
|
||||
`${issueNumber}`,
|
||||
'report.json',
|
||||
);
|
||||
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
return { success: false, error: 'Investigation report not found. Run investigation first.' };
|
||||
}
|
||||
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
||||
|
||||
// Build task description from investigation report
|
||||
const summary = reportData.summary || `Investigation of issue #${issueNumber}`;
|
||||
const fixAdvice = reportData.fixAdvice;
|
||||
const taskDescription = [
|
||||
`# GitHub Issue #${issueNumber}`,
|
||||
'',
|
||||
`## Summary`,
|
||||
summary,
|
||||
'',
|
||||
fixAdvice?.suggestedApproaches?.length
|
||||
? [
|
||||
'## Suggested Approach',
|
||||
fixAdvice.suggestedApproaches[fixAdvice.recommendedApproach || 0]?.description || '',
|
||||
'',
|
||||
'### Files to Modify',
|
||||
...(fixAdvice.suggestedApproaches[fixAdvice.recommendedApproach || 0]?.filesToModify?.map(
|
||||
(f: string) => `- ${f}`,
|
||||
) || []),
|
||||
].join('\n')
|
||||
: '',
|
||||
].join('\n');
|
||||
|
||||
const config = getGitHubConfig(project);
|
||||
const githubUrl = config
|
||||
? `https://github.com/${config.repo}/issues/${issueNumber}`
|
||||
: '';
|
||||
|
||||
const labels = reportData.suggestedLabels
|
||||
?.filter((l: { accepted?: boolean }) => l.accepted !== false)
|
||||
.map((l: { name: string }) => l.name) ?? [];
|
||||
|
||||
const specData = await createSpecForIssue(
|
||||
project,
|
||||
issueNumber,
|
||||
summary,
|
||||
taskDescription,
|
||||
githubUrl,
|
||||
labels,
|
||||
project.settings?.mainBranch,
|
||||
);
|
||||
|
||||
return { success: true, data: { specId: specData.specId } };
|
||||
});
|
||||
|
||||
return result ?? { success: false, error: 'Project not found' };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create task',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 4. Dismiss issue
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_DISMISS,
|
||||
async (_, projectId: string, issueNumber: number, reason: InvestigationDismissReason) => {
|
||||
debugLog('dismissIssue handler called', { projectId, issueNumber, reason });
|
||||
|
||||
try {
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const githubDir = getGitHubDir(project.path);
|
||||
const dismissDir = path.join(githubDir, 'investigations', 'dismissed');
|
||||
fs.mkdirSync(dismissDir, { recursive: true });
|
||||
|
||||
const dismissPath = path.join(dismissDir, `${issueNumber}.json`);
|
||||
await writeJsonWithRetry(dismissPath, {
|
||||
issueNumber,
|
||||
reason,
|
||||
dismissedAt: new Date().toISOString(),
|
||||
}, { indent: 2 });
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
return result ?? { success: false, error: 'Project not found' };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to dismiss issue',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 5. Post investigation results to GitHub
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_POST_GITHUB,
|
||||
async (_, projectId: string, issueNumber: number) => {
|
||||
debugLog('postInvestigationToGitHub handler called', { projectId, issueNumber });
|
||||
|
||||
try {
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const validation = await validateGitHubModule(project);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error ?? 'GitHub module not available' };
|
||||
}
|
||||
|
||||
const backendPath = validation.backendPath ?? '';
|
||||
const { model, thinkingLevel } = getGitHubIssuesSettings();
|
||||
|
||||
const args = buildRunnerArgs(
|
||||
getRunnerPath(backendPath),
|
||||
project.path,
|
||||
'post-investigation',
|
||||
[String(issueNumber)],
|
||||
{ model, thinkingLevel },
|
||||
);
|
||||
|
||||
const subprocessEnv = await getRunnerEnv();
|
||||
const { promise } = runPythonSubprocess<{ commentId: number }>({
|
||||
pythonPath: getPythonPath(backendPath),
|
||||
args,
|
||||
cwd: backendPath,
|
||||
env: subprocessEnv,
|
||||
onComplete: (stdout) => parseJSONFromOutput<{ commentId: number }>(stdout),
|
||||
onStdout: (line) => debugLog('STDOUT:', line),
|
||||
onStderr: (line) => debugLog('STDERR:', line),
|
||||
});
|
||||
|
||||
const subResult = await promise;
|
||||
|
||||
if (!subResult.success) {
|
||||
return { success: false, error: subResult.error ?? 'Failed to post to GitHub' };
|
||||
}
|
||||
|
||||
const postResult = subResult.data as { commentId: number };
|
||||
return { success: true, data: { commentId: postResult.commentId } };
|
||||
});
|
||||
|
||||
return result ?? { success: false, error: 'Project not found' };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to post to GitHub',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 6. Get investigation settings
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_GET_SETTINGS,
|
||||
async (_, projectId: string) => {
|
||||
debugLog('getInvestigationSettings handler called', { projectId });
|
||||
|
||||
try {
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const configPath = path.join(getGitHubDir(project.path), 'config.json');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
if (data.investigation_settings) {
|
||||
return {
|
||||
success: true,
|
||||
data: data.investigation_settings as InvestigationSettings,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or is corrupted, return defaults
|
||||
}
|
||||
|
||||
return { success: true, data: createDefaultSettings() };
|
||||
});
|
||||
|
||||
return result ?? { success: true, data: createDefaultSettings() };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get settings',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 7. Save investigation settings
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_INVESTIGATION_SAVE_SETTINGS,
|
||||
async (_, projectId: string, settings: Partial<InvestigationSettings>) => {
|
||||
debugLog('saveInvestigationSettings handler called', { projectId });
|
||||
|
||||
try {
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const githubDir = getGitHubDir(project.path);
|
||||
fs.mkdirSync(githubDir, { recursive: true });
|
||||
|
||||
const configPath = path.join(githubDir, 'config.json');
|
||||
let existingConfig: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
} catch {
|
||||
// Use empty config if file doesn't exist
|
||||
}
|
||||
|
||||
// Merge with existing settings (partial update)
|
||||
const existingSettings = (existingConfig.investigation_settings as InvestigationSettings) ?? createDefaultSettings();
|
||||
const updatedConfig = {
|
||||
...existingConfig,
|
||||
investigation_settings: {
|
||||
...existingSettings,
|
||||
...settings,
|
||||
},
|
||||
};
|
||||
|
||||
await writeJsonWithRetry(configPath, updatedConfig, { indent: 2 });
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
return result ?? { success: false, error: 'Project not found' };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to save settings',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
debugLog('Investigation handlers registered');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user