codereview-aia 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -1
- package/docs/opt-in-full-context.md +27 -0
- package/package.json +1 -1
- package/reports/cr-cr-aia-17-11-2025-20-13.md +354 -0
- package/src/clients/implementations/openRouterClient.ts +2 -0
- package/src/clients/openRouterClient.ts +8 -1
- package/src/clients/utils/promptFormatter.ts +97 -20
- package/src/core/handlers/FileProcessingHandler.ts +6 -0
- package/src/index.ts +0 -2
- package/src/runtime/cliEntry.ts +21 -3
- package/src/runtime/debug/logManager.ts +37 -0
- package/src/runtime/fileCollector.ts +335 -28
- package/src/runtime/preprod/webCheck.ts +104 -0
- package/src/runtime/reviewPipeline.ts +56 -11
- package/src/runtime/runAiCodeReview.ts +161 -6
- package/src/runtime/ui/RuntimeApp.tsx +127 -15
- package/src/runtime/ui/screens/ModeSelection.tsx +148 -15
- package/src/runtime/ui/screens/ProgressScreen.tsx +58 -3
- package/src/runtime/ui/screens/ResultsScreen.tsx +37 -10
- package/src/types/review.ts +18 -0
- package/src/utils/logger.ts +64 -14
|
@@ -2,7 +2,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
|
-
import { collectFiles } from './fileCollector';
|
|
5
|
+
import { collectFiles, type FileCollectionMode } from './fileCollector';
|
|
6
6
|
import { loadManifest } from './manifest';
|
|
7
7
|
import { runAiCodeReview } from './runAiCodeReview';
|
|
8
8
|
import { ensureProxyEnvironmentInitialized } from './proxyEnvironment';
|
|
@@ -10,14 +10,27 @@ import { mergeReports } from './reportMerge';
|
|
|
10
10
|
import { collectReportData } from './reporting/reportDataCollector';
|
|
11
11
|
import { buildMarkdownReport, injectSummary } from './reporting/markdownReportBuilder';
|
|
12
12
|
import { generateReportSummary } from './reporting/summaryGenerator';
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
import logger from '../utils/logger';
|
|
14
|
+
|
|
15
|
+
export type ReviewStage = 'preparing' | 'collecting' | 'reviewing' | 'merging';
|
|
16
|
+
export type ReviewMode = FileCollectionMode;
|
|
17
|
+
|
|
18
|
+
export interface StageUpdate {
|
|
19
|
+
filesFound?: number;
|
|
20
|
+
currentFile?: string;
|
|
21
|
+
fileIndex?: number;
|
|
22
|
+
totalFiles?: number;
|
|
23
|
+
uncommittedFiles?: number;
|
|
24
|
+
contextFiles?: number;
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
export interface ReviewOptions {
|
|
17
28
|
model: string;
|
|
18
29
|
outDir: string;
|
|
19
30
|
debug?: boolean;
|
|
20
|
-
|
|
31
|
+
mode?: ReviewMode;
|
|
32
|
+
preprodTargetUrl?: string;
|
|
33
|
+
onStage?: (stage: ReviewStage, info?: StageUpdate) => void;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export interface ReviewTotals {
|
|
@@ -33,21 +46,29 @@ export interface ReviewResult {
|
|
|
33
46
|
duration: number;
|
|
34
47
|
repo: string;
|
|
35
48
|
filesReviewed: number;
|
|
49
|
+
fileBreakdown?: { uncommitted: number; context: number };
|
|
50
|
+
reportSummary?: string;
|
|
36
51
|
reportPath?: string;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
export async function executeReview(options: ReviewOptions): Promise<ReviewResult> {
|
|
40
|
-
const { model, outDir, debug = false, onStage } = options;
|
|
55
|
+
const { model, outDir, debug = false, onStage, mode = 'uncommitted' } = options;
|
|
41
56
|
|
|
42
57
|
onStage?.('collecting');
|
|
43
|
-
|
|
44
|
-
const
|
|
58
|
+
logger.info('Collecting files for review', { mode, workspace: process.cwd() });
|
|
59
|
+
const { targets, context, stats } = await collectFiles({ mode });
|
|
60
|
+
const fileCount = targets.length;
|
|
45
61
|
|
|
46
62
|
if (fileCount === 0) {
|
|
47
63
|
throw new Error('No files to review');
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
onStage?.('collecting', {
|
|
66
|
+
onStage?.('collecting', {
|
|
67
|
+
filesFound: fileCount,
|
|
68
|
+
uncommittedFiles: stats.uncommitted,
|
|
69
|
+
contextFiles: stats.context,
|
|
70
|
+
});
|
|
71
|
+
logger.info('File collection complete', { fileCount });
|
|
51
72
|
|
|
52
73
|
const workspaceRoot = process.cwd();
|
|
53
74
|
const repoRoot = await resolveRepoRoot();
|
|
@@ -64,9 +85,15 @@ export async function executeReview(options: ReviewOptions): Promise<ReviewResul
|
|
|
64
85
|
|
|
65
86
|
const start = Date.now();
|
|
66
87
|
|
|
88
|
+
const repoName = basename(workspaceRoot) || 'workspace';
|
|
67
89
|
try {
|
|
68
|
-
onStage?.('reviewing', {
|
|
69
|
-
|
|
90
|
+
onStage?.('reviewing', {
|
|
91
|
+
filesFound: fileCount,
|
|
92
|
+
totalFiles: fileCount,
|
|
93
|
+
uncommittedFiles: stats.uncommitted,
|
|
94
|
+
contextFiles: stats.context,
|
|
95
|
+
});
|
|
96
|
+
const reports = await runAiCodeReview(targets, context, {
|
|
70
97
|
provider: 'openrouter',
|
|
71
98
|
type: 'comprehensive',
|
|
72
99
|
outDir: rawOutDir,
|
|
@@ -74,6 +101,22 @@ export async function executeReview(options: ReviewOptions): Promise<ReviewResul
|
|
|
74
101
|
model,
|
|
75
102
|
configPath: manifestPath,
|
|
76
103
|
debug,
|
|
104
|
+
runContext: {
|
|
105
|
+
repoName,
|
|
106
|
+
reviewMode: mode,
|
|
107
|
+
totalFiles: fileCount,
|
|
108
|
+
uncommittedFiles: stats.uncommitted,
|
|
109
|
+
contextFiles: stats.context,
|
|
110
|
+
},
|
|
111
|
+
onFileProgress: ({ file, index, total }) =>
|
|
112
|
+
onStage?.('reviewing', {
|
|
113
|
+
filesFound: fileCount,
|
|
114
|
+
currentFile: file,
|
|
115
|
+
fileIndex: index,
|
|
116
|
+
totalFiles: total,
|
|
117
|
+
uncommittedFiles: stats.uncommitted,
|
|
118
|
+
contextFiles: stats.context,
|
|
119
|
+
}),
|
|
77
120
|
});
|
|
78
121
|
|
|
79
122
|
onStage?.('merging');
|
|
@@ -82,7 +125,7 @@ export async function executeReview(options: ReviewOptions): Promise<ReviewResul
|
|
|
82
125
|
(merged?.totals as ReviewTotals) || ({ critical: 0, high: 0, medium: 0, low: 0 } as ReviewTotals);
|
|
83
126
|
const findings = (merged?.findings as any[]) || [];
|
|
84
127
|
|
|
85
|
-
const repo =
|
|
128
|
+
const repo = repoName;
|
|
86
129
|
const duration = Math.round((Date.now() - start) / 1000);
|
|
87
130
|
const reportTimestamp = new Date();
|
|
88
131
|
|
|
@@ -119,6 +162,8 @@ export async function executeReview(options: ReviewOptions): Promise<ReviewResul
|
|
|
119
162
|
duration,
|
|
120
163
|
repo,
|
|
121
164
|
filesReviewed: fileCount,
|
|
165
|
+
fileBreakdown: { uncommitted: stats.uncommitted, context: stats.context },
|
|
166
|
+
reportSummary: summary,
|
|
122
167
|
reportPath,
|
|
123
168
|
};
|
|
124
169
|
} finally {
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { join, relative, resolve } from 'node:path';
|
|
3
4
|
import { execa } from 'execa';
|
|
4
5
|
import type { OutputFormat } from '../types/common';
|
|
5
|
-
import type { ReviewOptions, ReviewType } from '../types/review';
|
|
6
|
+
import type { ReviewOptions, ReviewType, RunContext } from '../types/review';
|
|
6
7
|
import { orchestrateReview } from '../core/reviewOrchestrator';
|
|
7
8
|
import { RUNTIME_CONFIG } from './runtimeConfig';
|
|
8
9
|
|
|
10
|
+
interface FileProgressPayload {
|
|
11
|
+
file: string;
|
|
12
|
+
index: number;
|
|
13
|
+
total: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ReviewGroup {
|
|
17
|
+
label: string;
|
|
18
|
+
files: string[];
|
|
19
|
+
absoluteFiles: string[];
|
|
20
|
+
targetPath: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MAX_FILES_PER_GROUP = 5;
|
|
24
|
+
const MAX_CONTEXT_DOCS_PER_GROUP = 5;
|
|
25
|
+
const MAX_CONTEXT_DOC_CHARS = 2000;
|
|
26
|
+
|
|
27
|
+
interface ContextDocEntry {
|
|
28
|
+
path: string;
|
|
29
|
+
relativePath: string;
|
|
30
|
+
snippet: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
export interface AiReviewOptions {
|
|
10
34
|
provider: 'openrouter';
|
|
11
35
|
type: string;
|
|
@@ -14,10 +38,16 @@ export interface AiReviewOptions {
|
|
|
14
38
|
model?: string;
|
|
15
39
|
configPath?: string;
|
|
16
40
|
debug?: boolean;
|
|
41
|
+
onFileProgress?: (payload: FileProgressPayload) => void;
|
|
42
|
+
runContext?: RunContext;
|
|
17
43
|
}
|
|
18
44
|
|
|
19
|
-
export async function runAiCodeReview(
|
|
20
|
-
|
|
45
|
+
export async function runAiCodeReview(
|
|
46
|
+
targetFiles: string[],
|
|
47
|
+
contextFiles: string[],
|
|
48
|
+
opts: AiReviewOptions,
|
|
49
|
+
): Promise<any[]> {
|
|
50
|
+
if (targetFiles.length === 0) return [];
|
|
21
51
|
|
|
22
52
|
const workspaceRoot = process.cwd();
|
|
23
53
|
let repoRoot: string;
|
|
@@ -68,7 +98,7 @@ export async function runAiCodeReview(files: string[], opts: AiReviewOptions): P
|
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
const expandedFiles: string[] = [];
|
|
71
|
-
for (const file of
|
|
101
|
+
for (const file of targetFiles) {
|
|
72
102
|
const absPath = resolve(file);
|
|
73
103
|
try {
|
|
74
104
|
const stats = statSync(absPath);
|
|
@@ -144,10 +174,135 @@ export async function runAiCodeReview(files: string[], opts: AiReviewOptions): P
|
|
|
144
174
|
baseOptions.config = opts.configPath;
|
|
145
175
|
}
|
|
146
176
|
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
const groups = createReviewGroups(targets, workspaceRoot);
|
|
178
|
+
const contextDocEntries = prepareContextDocs(contextFiles, workspaceRoot);
|
|
179
|
+
const totalFiles = targets.length;
|
|
180
|
+
let processedCount = 0;
|
|
181
|
+
|
|
182
|
+
const groupCount = groups.length;
|
|
183
|
+
for (let groupIdx = 0; groupIdx < groups.length; groupIdx += 1) {
|
|
184
|
+
const group = groups[groupIdx];
|
|
185
|
+
const progressIndex = Math.min(processedCount + 1, totalFiles);
|
|
186
|
+
const displayName = formatGroupDisplay(group.files);
|
|
187
|
+
opts.onFileProgress?.({ file: displayName, index: progressIndex, total: totalFiles });
|
|
188
|
+
|
|
189
|
+
const selectedContextDocs = selectContextDocs(group.files, contextDocEntries);
|
|
190
|
+
const inheritedContext = opts.runContext;
|
|
191
|
+
const runContext = inheritedContext
|
|
192
|
+
? {
|
|
193
|
+
...inheritedContext,
|
|
194
|
+
groupIndex: groupIdx + 1,
|
|
195
|
+
groupCount,
|
|
196
|
+
groupLabel: group.label,
|
|
197
|
+
groupFiles: group.files,
|
|
198
|
+
docFiles: selectedContextDocs.map((doc) => doc.relativePath),
|
|
199
|
+
contextDocs: selectedContextDocs.map((doc) => ({
|
|
200
|
+
path: doc.relativePath,
|
|
201
|
+
snippet: doc.snippet,
|
|
202
|
+
})),
|
|
203
|
+
}
|
|
204
|
+
: undefined;
|
|
205
|
+
|
|
206
|
+
await orchestrateReview(group.targetPath, {
|
|
207
|
+
...baseOptions,
|
|
208
|
+
targetFileList: group.absoluteFiles,
|
|
209
|
+
runContext,
|
|
210
|
+
});
|
|
211
|
+
processedCount += group.files.length;
|
|
149
212
|
collectOutputs();
|
|
150
213
|
}
|
|
151
214
|
|
|
152
215
|
return outputs;
|
|
153
216
|
}
|
|
217
|
+
|
|
218
|
+
function createReviewGroups(targets: string[], workspaceRoot: string): ReviewGroup[] {
|
|
219
|
+
const directoryMap = new Map<string, string[]>();
|
|
220
|
+
for (const target of targets) {
|
|
221
|
+
const dir = path.posix.dirname(target) || '.';
|
|
222
|
+
if (!directoryMap.has(dir)) {
|
|
223
|
+
directoryMap.set(dir, []);
|
|
224
|
+
}
|
|
225
|
+
directoryMap.get(dir)!.push(target);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const groups: ReviewGroup[] = [];
|
|
229
|
+
for (const [dir, files] of directoryMap) {
|
|
230
|
+
for (let i = 0; i < files.length; i += MAX_FILES_PER_GROUP) {
|
|
231
|
+
const chunk = files.slice(i, i + MAX_FILES_PER_GROUP);
|
|
232
|
+
const absoluteFiles = chunk.map((file) => resolve(workspaceRoot, file));
|
|
233
|
+
const targetPath = dir === '.' ? chunk[0] ?? '.' : dir;
|
|
234
|
+
groups.push({
|
|
235
|
+
label: dir,
|
|
236
|
+
files: chunk,
|
|
237
|
+
absoluteFiles,
|
|
238
|
+
targetPath,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return groups;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatGroupDisplay(files: string[]): string {
|
|
247
|
+
if (files.length === 0) {
|
|
248
|
+
return 'pending files';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (files.length === 1) {
|
|
252
|
+
return files[0];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return `${files[0]} (+${files.length - 1} more)`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function prepareContextDocs(contextFiles: string[], workspaceRoot: string): ContextDocEntry[] {
|
|
259
|
+
const docs: ContextDocEntry[] = [];
|
|
260
|
+
const seen = new Set<string>();
|
|
261
|
+
|
|
262
|
+
for (const file of contextFiles) {
|
|
263
|
+
const absPath = resolve(file);
|
|
264
|
+
if (seen.has(absPath)) continue;
|
|
265
|
+
seen.add(absPath);
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const stats = statSync(absPath);
|
|
269
|
+
if (!stats.isFile()) continue;
|
|
270
|
+
const buffer = readFileSync(absPath);
|
|
271
|
+
if (buffer.length === 0 || buffer.includes(0)) continue;
|
|
272
|
+
const snippet = buffer.toString('utf-8').slice(0, MAX_CONTEXT_DOC_CHARS);
|
|
273
|
+
const relativePath = relative(workspaceRoot, absPath).replace(/\\/g, '/');
|
|
274
|
+
if (!relativePath || relativePath.startsWith('..')) continue;
|
|
275
|
+
docs.push({ path: absPath, relativePath, snippet });
|
|
276
|
+
} catch {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return docs;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function selectContextDocs(
|
|
285
|
+
groupFiles: string[],
|
|
286
|
+
docs: ContextDocEntry[],
|
|
287
|
+
limit = MAX_CONTEXT_DOCS_PER_GROUP,
|
|
288
|
+
): ContextDocEntry[] {
|
|
289
|
+
if (docs.length === 0 || limit <= 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const groupDirs = new Set(groupFiles.map((file) => path.posix.dirname(file)));
|
|
294
|
+
const prioritized: ContextDocEntry[] = [];
|
|
295
|
+
const remaining: ContextDocEntry[] = [];
|
|
296
|
+
|
|
297
|
+
for (const doc of docs) {
|
|
298
|
+
const docDir = path.posix.dirname(doc.relativePath);
|
|
299
|
+
if (groupDirs.has(docDir)) {
|
|
300
|
+
prioritized.push(doc);
|
|
301
|
+
} else {
|
|
302
|
+
remaining.push(doc);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const ordered = [...prioritized, ...remaining];
|
|
307
|
+
return ordered.slice(0, limit);
|
|
308
|
+
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { LayoutProvider, useLayout } from './Layout';
|
|
3
3
|
import { AuthScreen } from './screens/AuthScreen';
|
|
4
4
|
import { ModeSelection } from './screens/ModeSelection';
|
|
5
5
|
import { ProgressScreen } from './screens/ProgressScreen';
|
|
6
6
|
import { ResultsScreen } from './screens/ResultsScreen';
|
|
7
7
|
import { getInk, getInkSpinner } from './inkModules';
|
|
8
|
-
import type { ReviewResult, ReviewStage } from '../reviewPipeline';
|
|
8
|
+
import type { ReviewMode, ReviewResult, ReviewStage, StageUpdate } from '../reviewPipeline';
|
|
9
9
|
import { executeReview } from '../reviewPipeline';
|
|
10
10
|
import { loadSession, getSessionToken } from '../auth/session';
|
|
11
11
|
import type { SessionUser } from '../auth/types';
|
|
12
12
|
import { RUNTIME_CONFIG } from '../runtimeConfig';
|
|
13
13
|
import { MissingCrIgnoreError } from '../errors';
|
|
14
|
+
import { triggerManualWebCheck } from '../preprod/webCheck';
|
|
15
|
+
import { startDebugLogSession, type DebugLogSession } from '../debug/logManager';
|
|
16
|
+
import logger from '../../utils/logger';
|
|
14
17
|
|
|
15
18
|
type ScreenState = 'auth' | 'mode' | 'progress' | 'results';
|
|
16
19
|
|
|
@@ -21,9 +24,31 @@ interface RuntimeAppProps {
|
|
|
21
24
|
interface ProgressState {
|
|
22
25
|
stage: ReviewStage;
|
|
23
26
|
filesFound?: number;
|
|
27
|
+
currentFile?: string;
|
|
28
|
+
currentIndex?: number;
|
|
29
|
+
totalFiles?: number;
|
|
30
|
+
uncommittedFiles?: number;
|
|
31
|
+
contextFiles?: number;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
const defaultProgress: ProgressState = { stage: 'collecting' };
|
|
35
|
+
const GENERIC_ERROR_MESSAGE =
|
|
36
|
+
'Something went wrong. Press D to enable debug mode and rerun to capture logs.';
|
|
37
|
+
const PREPROD_ERROR_MESSAGE =
|
|
38
|
+
'Could not start the website check. Double-check the URL or try again later.';
|
|
39
|
+
|
|
40
|
+
function transformStageUpdate(stage: ReviewStage, info?: StageUpdate): ProgressState {
|
|
41
|
+
return {
|
|
42
|
+
stage,
|
|
43
|
+
filesFound: info?.filesFound,
|
|
44
|
+
currentFile: info?.currentFile,
|
|
45
|
+
currentIndex:
|
|
46
|
+
typeof info?.fileIndex === 'number' && info.fileIndex > 0 ? info.fileIndex : undefined,
|
|
47
|
+
totalFiles: info?.totalFiles,
|
|
48
|
+
uncommittedFiles: info?.uncommittedFiles,
|
|
49
|
+
contextFiles: info?.contextFiles,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
27
52
|
|
|
28
53
|
function AppBody({ debug = false }: RuntimeAppProps) {
|
|
29
54
|
const { Box, Text, useApp, useInput } = getInk();
|
|
@@ -33,8 +58,11 @@ function AppBody({ debug = false }: RuntimeAppProps) {
|
|
|
33
58
|
const [user, setUser] = useState<SessionUser | null>(null);
|
|
34
59
|
const [progress, setProgress] = useState<ProgressState>(defaultProgress);
|
|
35
60
|
const [results, setResults] = useState<ReviewResult | null>(null);
|
|
61
|
+
const [debugEnabled, setDebugEnabled] = useState(false);
|
|
62
|
+
const [lastDebugLogPath, setLastDebugLogPath] = useState<string | null>(null);
|
|
36
63
|
type ErrorInfo = { type: 'generic' | 'crignore'; message: string; hint?: string };
|
|
37
64
|
const [error, setError] = useState<ErrorInfo | null>(null);
|
|
65
|
+
const combinedDebugFlag = useMemo(() => debug || debugEnabled, [debug, debugEnabled]);
|
|
38
66
|
|
|
39
67
|
useEffect(() => {
|
|
40
68
|
const sessionUser = loadSession();
|
|
@@ -68,23 +96,86 @@ function AppBody({ debug = false }: RuntimeAppProps) {
|
|
|
68
96
|
setScreen('mode');
|
|
69
97
|
}, []);
|
|
70
98
|
|
|
71
|
-
const
|
|
99
|
+
const handleToggleDebug = useCallback(() => {
|
|
100
|
+
setDebugEnabled((prev) => {
|
|
101
|
+
if (prev) {
|
|
102
|
+
setLastDebugLogPath(null);
|
|
103
|
+
}
|
|
104
|
+
return !prev;
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const handleRunReview = useCallback(async (mode: ReviewMode, extras?: { targetUrl?: string }) => {
|
|
109
|
+
const trimmedUrl = extras?.targetUrl?.trim();
|
|
110
|
+
|
|
111
|
+
if (mode === 'preprod' && !trimmedUrl) {
|
|
112
|
+
setError({ type: 'generic', message: 'Pre-production mode requires a website URL.' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
setScreen('progress');
|
|
73
117
|
setResults(null);
|
|
74
118
|
setError(null);
|
|
75
|
-
setProgress(defaultProgress);
|
|
119
|
+
setProgress(mode === 'preprod' ? { stage: 'preparing' } : { ...defaultProgress });
|
|
120
|
+
setLastDebugLogPath(null);
|
|
121
|
+
|
|
122
|
+
let debugSession: DebugLogSession | null = null;
|
|
123
|
+
const finalizeDebugLogging = () => {
|
|
124
|
+
if (debugSession) {
|
|
125
|
+
debugSession.stop();
|
|
126
|
+
setLastDebugLogPath(debugSession.filePath);
|
|
127
|
+
} else {
|
|
128
|
+
setLastDebugLogPath(null);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (debugEnabled) {
|
|
133
|
+
try {
|
|
134
|
+
debugSession = startDebugLogSession(process.cwd());
|
|
135
|
+
} catch (logInitError) {
|
|
136
|
+
const message =
|
|
137
|
+
logInitError instanceof Error
|
|
138
|
+
? logInitError.message
|
|
139
|
+
: 'Unable to initialize debug logging.';
|
|
140
|
+
setError({ type: 'generic', message: `Debug logging disabled: ${message}` });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (mode === 'preprod' && trimmedUrl) {
|
|
145
|
+
try {
|
|
146
|
+
logger.info('Triggering manual website check', { url: trimmedUrl });
|
|
147
|
+
await triggerManualWebCheck(trimmedUrl);
|
|
148
|
+
} catch (webError) {
|
|
149
|
+
logger.error('Pre-production web check failed', webError);
|
|
150
|
+
setError({ type: 'generic', message: PREPROD_ERROR_MESSAGE });
|
|
151
|
+
setScreen('mode');
|
|
152
|
+
finalizeDebugLogging();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
76
156
|
|
|
77
157
|
try {
|
|
158
|
+
logger.info('Starting code review run', { mode, workspace: process.cwd(), preprodTarget: trimmedUrl });
|
|
159
|
+
setProgress({ ...defaultProgress });
|
|
78
160
|
const reviewResult = await executeReview({
|
|
79
161
|
model: process.env.AI_CODE_REVIEW_MODEL || RUNTIME_CONFIG.DEFAULT_MODEL,
|
|
80
162
|
outDir: './reports',
|
|
81
|
-
debug,
|
|
82
|
-
|
|
163
|
+
debug: combinedDebugFlag,
|
|
164
|
+
mode,
|
|
165
|
+
preprodTargetUrl: trimmedUrl,
|
|
166
|
+
onStage: (stage, info) =>
|
|
167
|
+
setProgress(transformStageUpdate(stage, info)),
|
|
83
168
|
});
|
|
84
169
|
|
|
170
|
+
logger.info('Code review run complete', {
|
|
171
|
+
mode,
|
|
172
|
+
durationSeconds: reviewResult.duration,
|
|
173
|
+
filesReviewed: reviewResult.filesReviewed,
|
|
174
|
+
});
|
|
85
175
|
setResults(reviewResult);
|
|
86
176
|
setScreen('results');
|
|
87
177
|
} catch (reviewError) {
|
|
178
|
+
logger.error('Code review run failed', reviewError);
|
|
88
179
|
if (reviewError instanceof MissingCrIgnoreError) {
|
|
89
180
|
setError({
|
|
90
181
|
type: 'crignore',
|
|
@@ -92,22 +183,38 @@ function AppBody({ debug = false }: RuntimeAppProps) {
|
|
|
92
183
|
hint: `Create ${reviewError.crIgnorePath} (can be empty, gitignore syntax) and rerun.`,
|
|
93
184
|
});
|
|
94
185
|
} else {
|
|
95
|
-
|
|
96
|
-
reviewError instanceof Error
|
|
97
|
-
? reviewError.message
|
|
98
|
-
: 'Failed to run review. Check proxy/auth configuration.';
|
|
99
|
-
setError({ type: 'generic', message });
|
|
186
|
+
setError({ type: 'generic', message: GENERIC_ERROR_MESSAGE });
|
|
100
187
|
}
|
|
101
188
|
setScreen('mode');
|
|
189
|
+
} finally {
|
|
190
|
+
finalizeDebugLogging();
|
|
102
191
|
}
|
|
103
|
-
}, [
|
|
192
|
+
}, [combinedDebugFlag, debugEnabled]);
|
|
104
193
|
|
|
105
194
|
return (
|
|
106
195
|
<Box width={layout.frameWidth} flexDirection="column" gap={1}>
|
|
107
196
|
{screen === 'auth' && <AuthScreen onAuth={handleAuth} />}
|
|
108
|
-
{screen === 'mode' && user &&
|
|
109
|
-
|
|
110
|
-
|
|
197
|
+
{screen === 'mode' && user && (
|
|
198
|
+
<ModeSelection
|
|
199
|
+
onSelect={(mode, payload) => handleRunReview(mode, payload)}
|
|
200
|
+
debugEnabled={debugEnabled}
|
|
201
|
+
onToggleDebug={handleToggleDebug}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
{screen === 'progress' && (
|
|
205
|
+
<ProgressScreen
|
|
206
|
+
stage={progress.stage}
|
|
207
|
+
filesFound={progress.filesFound}
|
|
208
|
+
currentFile={progress.currentFile}
|
|
209
|
+
currentIndex={progress.currentIndex}
|
|
210
|
+
totalFiles={progress.totalFiles}
|
|
211
|
+
uncommittedFiles={progress.uncommittedFiles}
|
|
212
|
+
contextFiles={progress.contextFiles}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
{screen === 'results' && results && (
|
|
216
|
+
<ResultsScreen result={results} debugLogPath={lastDebugLogPath} />
|
|
217
|
+
)}
|
|
111
218
|
{error && (
|
|
112
219
|
<Box
|
|
113
220
|
borderStyle="round"
|
|
@@ -121,6 +228,11 @@ function AppBody({ debug = false }: RuntimeAppProps) {
|
|
|
121
228
|
</Text>
|
|
122
229
|
</Box>
|
|
123
230
|
)}
|
|
231
|
+
{lastDebugLogPath && screen !== 'results' && (
|
|
232
|
+
<Box>
|
|
233
|
+
<Text dimColor>Debug logs saved to: {lastDebugLogPath}</Text>
|
|
234
|
+
</Box>
|
|
235
|
+
)}
|
|
124
236
|
<Box justifyContent="space-between">
|
|
125
237
|
<Text dimColor>{user ? `Logged in as ${user.username}` : 'Authenticate to start reviewing'}</Text>
|
|
126
238
|
<Text dimColor>Ctrl+C to exit</Text>
|