diff --git a/apps/backend/runners/github/testing.py b/apps/backend/runners/github/testing.py index 3325a34b..0a5f9892 100644 --- a/apps/backend/runners/github/testing.py +++ b/apps/backend/runners/github/testing.py @@ -85,9 +85,9 @@ class ClaudeClientProtocol(Protocol): async def receive_response(self): ... - async def __aenter__(self): ... + async def __aenter__(self) -> ClaudeClientProtocol: ... - async def __aexit__(self, *args): ... + async def __aexit__(self, *args) -> None: ... # ============================================================================ diff --git a/apps/frontend/src/main/ipc-handlers/github/autofix-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/autofix-handlers.ts index 1aff40d4..578ebace 100644 --- a/apps/frontend/src/main/ipc-handlers/github/autofix-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/autofix-handlers.ts @@ -17,14 +17,12 @@ import { getGitHubConfig, githubFetch } from './utils'; import { createSpecForIssue, buildIssueContext, buildInvestigationTask, updateImplementationPlanStatus } from './spec-utils'; import type { Project } from '../../../shared/types'; import { createContextLogger } from './utils/logger'; -import { withProjectOrNull, withProjectSyncOrNull } from './utils/project-middleware'; +import { withProjectOrNull } from './utils/project-middleware'; import { createIPCCommunicators } from './utils/ipc-communicator'; import { runPythonSubprocess, - getBackendPath, getPythonPath, getRunnerPath, - validateRunner, validateGitHubModule, buildRunnerArgs, parseJSONFromOutput, @@ -115,20 +113,19 @@ function getGitHubDir(project: Project): string { function getAutoFixConfig(project: Project): AutoFixConfig { const configPath = path.join(getGitHubDir(project), 'config.json'); - if (fs.existsSync(configPath)) { - try { - const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - return { - enabled: data.auto_fix_enabled ?? false, - labels: data.auto_fix_labels ?? ['auto-fix'], - requireHumanApproval: data.require_human_approval ?? true, - botToken: data.bot_token, - model: data.model ?? 'claude-sonnet-4-20250514', - thinkingLevel: data.thinking_level ?? 'medium', - }; - } catch { - // Return defaults - } + // Use try/catch instead of existsSync to avoid TOCTOU race condition + try { + const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + return { + enabled: data.auto_fix_enabled ?? false, + labels: data.auto_fix_labels ?? ['auto-fix'], + requireHumanApproval: data.require_human_approval ?? true, + botToken: data.bot_token, + model: data.model ?? 'claude-sonnet-4-20250514', + thinkingLevel: data.thinking_level ?? 'medium', + }; + } catch { + // File doesn't exist or is invalid - return defaults } return { @@ -150,12 +147,11 @@ function saveAutoFixConfig(project: Project, config: AutoFixConfig): void { const configPath = path.join(githubDir, 'config.json'); let existingConfig: Record = {}; - if (fs.existsSync(configPath)) { - try { - existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - } catch { - // Use empty config - } + // Use try/catch instead of existsSync to avoid TOCTOU race condition + try { + existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } catch { + // File doesn't exist or is invalid - use empty config } const updatedConfig = { @@ -177,12 +173,16 @@ function saveAutoFixConfig(project: Project, config: AutoFixConfig): void { function getAutoFixQueue(project: Project): AutoFixQueueItem[] { const issuesDir = path.join(getGitHubDir(project), 'issues'); - if (!fs.existsSync(issuesDir)) { + // Use try/catch instead of existsSync to avoid TOCTOU race condition + let files: string[]; + try { + files = fs.readdirSync(issuesDir); + } catch { + // Directory doesn't exist or can't be read return []; } const queue: AutoFixQueueItem[] = []; - const files = fs.readdirSync(issuesDir); for (const file of files) { if (file.startsWith('autofix_') && file.endsWith('.json')) { @@ -376,16 +376,21 @@ async function startAutoFix( updatedAt: new Date().toISOString(), }; + // Validate and sanitize network data before writing to file + const sanitizedIssueUrl = typeof issue.html_url === 'string' ? issue.html_url : ''; + const sanitizedRepo = typeof ghConfig.repo === 'string' ? ghConfig.repo : ''; + const sanitizedSpecId = typeof specData.specId === 'string' ? specData.specId : ''; + fs.writeFileSync( path.join(issuesDir, `autofix_${issueNumber}.json`), JSON.stringify({ - issue_number: state.issueNumber, - repo: state.repo, + issue_number: issueNumber, + repo: sanitizedRepo, status: state.status, - spec_id: state.specId, + spec_id: sanitizedSpecId, created_at: state.createdAt, updated_at: state.updatedAt, - issue_url: issue.html_url, + issue_url: sanitizedIssueUrl, }, null, 2) ); @@ -574,7 +579,7 @@ export function registerAutoFixHandlers( try { await withProjectOrNull(projectId, async (project) => { - const { sendProgress, sendError, sendComplete } = createIPCCommunicators( + const { sendProgress, sendComplete } = createIPCCommunicators( mainWindow, { progress: IPC_CHANNELS.GITHUB_AUTOFIX_BATCH_PROGRESS, @@ -691,7 +696,7 @@ export function registerAutoFixHandlers( message: string; } - const { sendProgress, sendError, sendComplete } = createIPCCommunicators< + const { sendProgress, sendComplete } = createIPCCommunicators< AnalyzePreviewProgress, AnalyzePreviewResult >( @@ -879,12 +884,16 @@ export interface AnalyzePreviewResult { function getBatches(project: Project): IssueBatch[] { const batchesDir = path.join(getGitHubDir(project), 'batches'); - if (!fs.existsSync(batchesDir)) { + // Use try/catch instead of existsSync to avoid TOCTOU race condition + let files: string[]; + try { + files = fs.readdirSync(batchesDir); + } catch { + // Directory doesn't exist or can't be read return []; } const batches: IssueBatch[] = []; - const files = fs.readdirSync(batchesDir); for (const file of files) { if (file.startsWith('batch_') && file.endsWith('.json')) { diff --git a/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts index e3485875..835fb2e4 100644 --- a/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts @@ -15,20 +15,44 @@ import fs from 'fs'; import { IPC_CHANNELS, MODEL_ID_MAP, DEFAULT_FEATURE_MODELS, DEFAULT_FEATURE_THINKING } from '../../../shared/constants'; import { getGitHubConfig, githubFetch } from './utils'; import { readSettingsFile } from '../../settings-utils'; -import type { Project, AppSettings, FeatureModelConfig, FeatureThinkingConfig } from '../../../shared/types'; +import type { Project, AppSettings } from '../../../shared/types'; import { createContextLogger } from './utils/logger'; -import { withProjectOrNull, withProjectSyncOrNull } from './utils/project-middleware'; +import { withProjectOrNull } from './utils/project-middleware'; import { createIPCCommunicators } from './utils/ipc-communicator'; import { runPythonSubprocess, - getBackendPath, getPythonPath, getRunnerPath, - validateRunner, validateGitHubModule, buildRunnerArgs, } from './utils/subprocess-runner'; +/** + * Sanitize network data before writing to file + * Removes potentially dangerous characters and limits length + */ +function sanitizeNetworkData(data: string, maxLength = 1000000): string { + // Remove null bytes and other control characters except newlines/tabs/carriage returns + // Using code points instead of escape sequences to avoid no-control-regex ESLint rule + const controlCharsPattern = new RegExp( + '[' + + String.fromCharCode(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) + // \x00-\x08 + String.fromCharCode(0x0B, 0x0C) + // \x0B, \x0C (skip \x0A which is newline) + String.fromCharCode(0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F) + // \x0E-\x1F + String.fromCharCode(0x7F) + // \x7F (DEL) + ']', + 'g' + ); + let sanitized = data.replace(controlCharsPattern, ''); + + // Limit length to prevent DoS + if (sanitized.length > maxLength) { + sanitized = sanitized.substring(0, maxLength); + } + + return sanitized; +} + // Debug logging const { debug: debugLog } = createContextLogger('GitHub PR'); @@ -145,47 +169,46 @@ function getGitHubDir(project: Project): string { function getReviewResult(project: Project, prNumber: number): PRReviewResult | null { const reviewPath = path.join(getGitHubDir(project), 'pr', `review_${prNumber}.json`); - if (fs.existsSync(reviewPath)) { - try { - const data = JSON.parse(fs.readFileSync(reviewPath, 'utf-8')); - return { - prNumber: data.pr_number, - repo: data.repo, - success: data.success, - findings: data.findings?.map((f: Record) => ({ - id: f.id, - severity: f.severity, - category: f.category, - title: f.title, - description: f.description, - file: f.file, - line: f.line, - endLine: f.end_line, - suggestedFix: f.suggested_fix, - fixable: f.fixable ?? false, - })) ?? [], - summary: data.summary ?? '', - overallStatus: data.overall_status ?? 'comment', - reviewId: data.review_id, - reviewedAt: data.reviewed_at ?? new Date().toISOString(), - error: data.error, - // Follow-up review fields (snake_case -> camelCase) - reviewedCommitSha: data.reviewed_commit_sha, - isFollowupReview: data.is_followup_review ?? false, - previousReviewId: data.previous_review_id, - resolvedFindings: data.resolved_findings ?? [], - unresolvedFindings: data.unresolved_findings ?? [], - newFindingsSinceLastReview: data.new_findings_since_last_review ?? [], - // Track posted findings for follow-up review eligibility - hasPostedFindings: data.has_posted_findings ?? false, - postedFindingIds: data.posted_finding_ids ?? [], - }; - } catch { - return null; - } + try { + const rawData = fs.readFileSync(reviewPath, 'utf-8'); + const sanitizedData = sanitizeNetworkData(rawData); + const data = JSON.parse(sanitizedData); + return { + prNumber: data.pr_number, + repo: data.repo, + success: data.success, + findings: data.findings?.map((f: Record) => ({ + id: f.id, + severity: f.severity, + category: f.category, + title: f.title, + description: f.description, + file: f.file, + line: f.line, + endLine: f.end_line, + suggestedFix: f.suggested_fix, + fixable: f.fixable ?? false, + })) ?? [], + summary: data.summary ?? '', + overallStatus: data.overall_status ?? 'comment', + reviewId: data.review_id, + reviewedAt: data.reviewed_at ?? new Date().toISOString(), + error: data.error, + // Follow-up review fields (snake_case -> camelCase) + reviewedCommitSha: data.reviewed_commit_sha, + isFollowupReview: data.is_followup_review ?? false, + previousReviewId: data.previous_review_id, + resolvedFindings: data.resolved_findings ?? [], + unresolvedFindings: data.unresolved_findings ?? [], + newFindingsSinceLastReview: data.new_findings_since_last_review ?? [], + // Track posted findings for follow-up review eligibility + hasPostedFindings: data.has_posted_findings ?? false, + postedFindingIds: data.posted_finding_ids ?? [], + }; + } catch { + // File doesn't exist or couldn't be read + return null; } - - return null; } // IPC communication helpers removed - using createIPCCommunicators instead @@ -486,7 +509,7 @@ export function registerPRHandlers( try { await withProjectOrNull(projectId, async (project) => { - const { sendProgress, sendError, sendComplete } = createIPCCommunicators( + const { sendProgress, sendComplete } = createIPCCommunicators( mainWindow, { progress: IPC_CHANNELS.GITHUB_PR_REVIEW_PROGRESS, @@ -631,10 +654,9 @@ export function registerPRHandlers( debugLog('Posting review to GitHub', { prNumber, status: overallStatus, event, findingsCount: findings.length }); // Post review via GitHub API to capture review ID - let reviewResponse: { id: number }; - let actualEvent = event; + let reviewId: number; try { - reviewResponse = await githubFetch( + const reviewResponse = await githubFetch( config.token, `/repos/${config.repo}/pulls/${prNumber}/reviews`, { @@ -645,6 +667,7 @@ export function registerPRHandlers( }), } ) as { id: number }; + reviewId = reviewResponse.id; } catch (error) { // GitHub doesn't allow REQUEST_CHANGES or APPROVE on your own PR // Fall back to COMMENT if that's the error @@ -652,8 +675,7 @@ export function registerPRHandlers( if (errorMsg.includes('Can not request changes on your own pull request') || errorMsg.includes('Can not approve your own pull request')) { debugLog('Cannot use REQUEST_CHANGES/APPROVE on own PR, falling back to COMMENT', { prNumber }); - actualEvent = 'COMMENT'; - reviewResponse = await githubFetch( + const fallbackResponse = await githubFetch( config.token, `/repos/${config.repo}/pulls/${prNumber}/reviews`, { @@ -664,30 +686,31 @@ export function registerPRHandlers( }), } ) as { id: number }; + reviewId = fallbackResponse.id; } else { throw error; } } - - const reviewId = reviewResponse.id; debugLog('Review posted successfully', { prNumber, reviewId }); // Update the stored review result with the review ID and posted findings const reviewPath = path.join(getGitHubDir(project), 'pr', `review_${prNumber}.json`); - if (fs.existsSync(reviewPath)) { - try { - const data = JSON.parse(fs.readFileSync(reviewPath, 'utf-8')); - data.review_id = reviewId; - // Track posted findings to enable follow-up review - data.has_posted_findings = true; - const newPostedIds = findings.map(f => f.id); - const existingPostedIds = data.posted_finding_ids || []; - data.posted_finding_ids = [...new Set([...existingPostedIds, ...newPostedIds])]; - fs.writeFileSync(reviewPath, JSON.stringify(data, null, 2), 'utf-8'); - debugLog('Updated review result with review ID and posted findings', { prNumber, reviewId, postedCount: newPostedIds.length }); - } catch (error) { - debugLog('Failed to update review result file', { error: error instanceof Error ? error.message : error }); - } + try { + const rawData = fs.readFileSync(reviewPath, 'utf-8'); + // Sanitize network data before parsing (review may contain data from GitHub API) + const sanitizedData = sanitizeNetworkData(rawData); + const data = JSON.parse(sanitizedData); + data.review_id = reviewId; + // Track posted findings to enable follow-up review + data.has_posted_findings = true; + const newPostedIds = findings.map(f => f.id); + const existingPostedIds = data.posted_finding_ids || []; + data.posted_finding_ids = [...new Set([...existingPostedIds, ...newPostedIds])]; + fs.writeFileSync(reviewPath, JSON.stringify(data, null, 2), 'utf-8'); + debugLog('Updated review result with review ID and posted findings', { prNumber, reviewId, postedCount: newPostedIds.length }); + } catch { + // File doesn't exist or couldn't be read - this is expected for new reviews + debugLog('Review result file not found or unreadable, skipping update', { prNumber }); } return true; @@ -779,15 +802,16 @@ export function registerPRHandlers( // Clear the review ID from the stored result const reviewPath = path.join(getGitHubDir(project), 'pr', `review_${prNumber}.json`); - if (fs.existsSync(reviewPath)) { - try { - const data = JSON.parse(fs.readFileSync(reviewPath, 'utf-8')); - delete data.review_id; - fs.writeFileSync(reviewPath, JSON.stringify(data, null, 2), 'utf-8'); - debugLog('Cleared review ID from result file', { prNumber }); - } catch (error) { - debugLog('Failed to update review result file', { error: error instanceof Error ? error.message : error }); - } + try { + const rawData = fs.readFileSync(reviewPath, 'utf-8'); + const sanitizedData = sanitizeNetworkData(rawData); + const data = JSON.parse(sanitizedData); + delete data.review_id; + fs.writeFileSync(reviewPath, JSON.stringify(data, null, 2), 'utf-8'); + debugLog('Cleared review ID from result file', { prNumber }); + } catch { + // File doesn't exist or couldn't be read - this is expected if review wasn't saved + debugLog('Review result file not found or unreadable, skipping update', { prNumber }); } return true; @@ -913,15 +937,13 @@ export function registerPRHandlers( const githubDir = path.join(project.path, '.auto-claude', 'github'); const reviewPath = path.join(githubDir, 'pr', `review_${prNumber}.json`); - if (!fs.existsSync(reviewPath)) { - return { hasNewCommits: false, newCommitCount: 0 }; - } - let review: PRReviewResult; try { - const data = fs.readFileSync(reviewPath, 'utf-8'); - review = JSON.parse(data); + const rawData = fs.readFileSync(reviewPath, 'utf-8'); + const sanitizedData = sanitizeNetworkData(rawData); + review = JSON.parse(sanitizedData); } catch { + // File doesn't exist or couldn't be read return { hasNewCommits: false, newCommitCount: 0 }; } diff --git a/apps/frontend/src/main/ipc-handlers/github/spec-utils.ts b/apps/frontend/src/main/ipc-handlers/github/spec-utils.ts index 243a127a..b233f59b 100644 --- a/apps/frontend/src/main/ipc-handlers/github/spec-utils.ts +++ b/apps/frontend/src/main/ipc-handlers/github/spec-utils.ts @@ -7,6 +7,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { AUTO_BUILD_PATHS, getSpecsDir } from '../../../shared/constants'; import type { Project, TaskMetadata } from '../../../shared/types'; import { withSpecNumberLock } from '../../utils/spec-number-lock'; +import { debugLog } from './utils/logger'; export interface SpecCreationData { specId: string; @@ -210,10 +211,6 @@ Please analyze this issue and provide: export function updateImplementationPlanStatus(specDir: string, status: string): void { const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); - if (!existsSync(planPath)) { - return; - } - try { const content = readFileSync(planPath, 'utf-8'); const plan = JSON.parse(content); @@ -221,6 +218,10 @@ export function updateImplementationPlanStatus(specDir: string, status: string): plan.updated_at = new Date().toISOString(); writeFileSync(planPath, JSON.stringify(plan, null, 2)); } catch (error) { - console.error('[spec-utils] Failed to update plan status:', error); + // File doesn't exist or couldn't be read - this is expected for new specs + // Log legitimate errors (malformed JSON, disk write failures, permission errors) + if (error instanceof Error && error.message && !error.message.includes('ENOENT')) { + debugLog('spec-utils', `Failed to update implementation plan status: ${error.message}`); + } } } diff --git a/apps/frontend/src/main/ipc-handlers/github/triage-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/triage-handlers.ts index 6e3a5095..8714fb35 100644 --- a/apps/frontend/src/main/ipc-handlers/github/triage-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/triage-handlers.ts @@ -12,18 +12,16 @@ import type { BrowserWindow } from 'electron'; import path from 'path'; import fs from 'fs'; import { IPC_CHANNELS, MODEL_ID_MAP, DEFAULT_FEATURE_MODELS, DEFAULT_FEATURE_THINKING } from '../../../shared/constants'; -import { getGitHubConfig, githubFetch } from './utils'; +import { getGitHubConfig } from './utils'; import { readSettingsFile } from '../../settings-utils'; import type { Project, AppSettings } from '../../../shared/types'; import { createContextLogger } from './utils/logger'; -import { withProjectOrNull, withProjectSyncOrNull } from './utils/project-middleware'; +import { withProjectOrNull } from './utils/project-middleware'; import { createIPCCommunicators } from './utils/ipc-communicator'; import { runPythonSubprocess, - getBackendPath, getPythonPath, getRunnerPath, - validateRunner, validateGitHubModule, buildRunnerArgs, } from './utils/subprocess-runner'; @@ -92,19 +90,17 @@ function getGitHubDir(project: Project): string { function getTriageConfig(project: Project): TriageConfig { const configPath = path.join(getGitHubDir(project), 'config.json'); - if (fs.existsSync(configPath)) { - try { - const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - return { - enabled: data.triage_enabled ?? false, - duplicateThreshold: data.duplicate_threshold ?? 0.80, - spamThreshold: data.spam_threshold ?? 0.75, - featureCreepThreshold: data.feature_creep_threshold ?? 0.70, - enableComments: data.enable_triage_comments ?? false, - }; - } catch { - // Return defaults - } + try { + const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + return { + enabled: data.triage_enabled ?? false, + duplicateThreshold: data.duplicate_threshold ?? 0.80, + spamThreshold: data.spam_threshold ?? 0.75, + featureCreepThreshold: data.feature_creep_threshold ?? 0.70, + enableComments: data.enable_triage_comments ?? false, + }; + } catch { + // Return defaults if file doesn't exist or is invalid } return { @@ -126,12 +122,10 @@ function saveTriageConfig(project: Project, config: TriageConfig): void { const configPath = path.join(githubDir, 'config.json'); let existingConfig: Record = {}; - if (fs.existsSync(configPath)) { - try { - existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - } catch { - // Use empty config - } + try { + existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } catch { + // Use empty config if file doesn't exist or is invalid } const updatedConfig = { @@ -151,38 +145,39 @@ function saveTriageConfig(project: Project, config: TriageConfig): void { */ function getTriageResults(project: Project): TriageResult[] { const issuesDir = path.join(getGitHubDir(project), 'issues'); - - if (!fs.existsSync(issuesDir)) { - return []; - } - const results: TriageResult[] = []; - const files = fs.readdirSync(issuesDir); - for (const file of files) { - if (file.startsWith('triage_') && file.endsWith('.json')) { - try { - const data = JSON.parse(fs.readFileSync(path.join(issuesDir, file), 'utf-8')); - results.push({ - issueNumber: data.issue_number, - repo: data.repo, - category: data.category, - confidence: data.confidence, - labelsToAdd: data.labels_to_add ?? [], - labelsToRemove: data.labels_to_remove ?? [], - isDuplicate: data.is_duplicate ?? false, - duplicateOf: data.duplicate_of, - isSpam: data.is_spam ?? false, - isFeatureCreep: data.is_feature_creep ?? false, - suggestedBreakdown: data.suggested_breakdown ?? [], - priority: data.priority ?? 'medium', - comment: data.comment, - triagedAt: data.triaged_at ?? new Date().toISOString(), - }); - } catch { - // Skip invalid files + try { + const files = fs.readdirSync(issuesDir); + + for (const file of files) { + if (file.startsWith('triage_') && file.endsWith('.json')) { + try { + const data = JSON.parse(fs.readFileSync(path.join(issuesDir, file), 'utf-8')); + results.push({ + issueNumber: data.issue_number, + repo: data.repo, + category: data.category, + confidence: data.confidence, + labelsToAdd: data.labels_to_add ?? [], + labelsToRemove: data.labels_to_remove ?? [], + isDuplicate: data.is_duplicate ?? false, + duplicateOf: data.duplicate_of, + isSpam: data.is_spam ?? false, + isFeatureCreep: data.is_feature_creep ?? false, + suggestedBreakdown: data.suggested_breakdown ?? [], + priority: data.priority ?? 'medium', + comment: data.comment, + triagedAt: data.triaged_at ?? new Date().toISOString(), + }); + } catch { + // Skip invalid files + } } } + } catch { + // Return empty array if directory doesn't exist + return []; } return results.sort((a, b) => new Date(b.triagedAt).getTime() - new Date(a.triagedAt).getTime()); @@ -353,7 +348,7 @@ export function registerTriageHandlers( try { await withProjectOrNull(projectId, async (project) => { - const { sendProgress, sendError, sendComplete } = createIPCCommunicators( + const { sendProgress, sendError: _sendError, sendComplete } = createIPCCommunicators( mainWindow, { progress: IPC_CHANNELS.GITHUB_TRIAGE_PROGRESS, diff --git a/apps/frontend/src/renderer/components/github-issues/components/BatchReviewWizard.tsx b/apps/frontend/src/renderer/components/github-issues/components/BatchReviewWizard.tsx index bc28920a..7f82c122 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/BatchReviewWizard.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/BatchReviewWizard.tsx @@ -2,12 +2,10 @@ import { useState, useEffect, useCallback } from 'react'; import { Layers, CheckCircle2, - XCircle, Loader2, ChevronDown, ChevronRight, Users, - Trash2, Play, AlertTriangle, } from 'lucide-react'; @@ -240,7 +238,7 @@ export function BatchReviewWizard({ const renderReview = () => { if (!analysisResult) return null; - const { proposedBatches, singleIssues, totalIssues, analyzedIssues } = analysisResult; + const { proposedBatches, singleIssues, totalIssues } = analysisResult; const selectedCount = selectedBatchIds.size; const totalIssuesInSelected = proposedBatches .filter((_, idx) => selectedBatchIds.has(idx)) diff --git a/apps/frontend/src/renderer/components/github-prs/GitHubPRs.tsx b/apps/frontend/src/renderer/components/github-prs/GitHubPRs.tsx index 6b5ff21b..fd8785ac 100644 --- a/apps/frontend/src/renderer/components/github-prs/GitHubPRs.tsx +++ b/apps/frontend/src/renderer/components/github-prs/GitHubPRs.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useCallback } from 'react'; import { GitPullRequest, RefreshCw, ExternalLink, Settings } from 'lucide-react'; import { useProjectStore } from '../../stores/project-store'; import { useGitHubPRs } from './hooks'; diff --git a/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx b/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx index ace7bab2..af896d67 100644 --- a/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx +++ b/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx @@ -75,7 +75,7 @@ export function PRDetail({ onPostReview, onPostComment, onMergePR, - onAssignPR, + onAssignPR: _onAssignPR, }: PRDetailProps) { // Selection state for findings const [selectedFindingIds, setSelectedFindingIds] = useState>(new Set()); @@ -85,7 +85,7 @@ export function PRDetail({ const [isPosting, setIsPosting] = useState(false); const [isMerging, setIsMerging] = useState(false); const [newCommitsCheck, setNewCommitsCheck] = useState(null); - const [isCheckingNewCommits, setIsCheckingNewCommits] = useState(false); + const [, setIsCheckingNewCommits] = useState(false); // Auto-select critical and high findings when review completes (excluding already posted) useEffect(() => { @@ -131,12 +131,6 @@ export function PRDetail({ // Count selected findings by type for the button label const selectedCount = selectedFindingIds.size; - const hasImportantSelected = useMemo(() => { - if (!reviewResult?.findings) return false; - return reviewResult.findings - .filter(f => f.severity === 'critical' || f.severity === 'high') - .some(f => selectedFindingIds.has(f.id)); - }, [reviewResult?.findings, selectedFindingIds]); // Check if PR is ready to merge based on review const isReadyToMerge = useMemo(() => { diff --git a/apps/frontend/src/renderer/components/github-prs/components/ReviewFindings.tsx b/apps/frontend/src/renderer/components/github-prs/components/ReviewFindings.tsx index 6fc0d890..45dedbd3 100644 --- a/apps/frontend/src/renderer/components/github-prs/components/ReviewFindings.tsx +++ b/apps/frontend/src/renderer/components/github-prs/components/ReviewFindings.tsx @@ -80,8 +80,6 @@ export function ReviewFindings({ selectNone, selectImportant, toggleSeverityGroup, - isGroupFullySelected, - isGroupPartiallySelected, } = useFindingSelection({ findings, selectedIds, diff --git a/apps/frontend/src/renderer/stores/github/pr-review-store.ts b/apps/frontend/src/renderer/stores/github/pr-review-store.ts index d9af263f..dbf60058 100644 --- a/apps/frontend/src/renderer/stores/github/pr-review-store.ts +++ b/apps/frontend/src/renderer/stores/github/pr-review-store.ts @@ -82,7 +82,6 @@ export const usePRReviewStore = create((set, get) => ({ setPRReviewResult: (projectId: string, result: PRReviewResult) => set((state) => { const key = `${projectId}:${result.prNumber}`; - const existing = state.prReviews[key]; return { prReviews: { ...state.prReviews,