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.
@@ -1,23 +1,154 @@
1
- import React from 'react';
2
- import { getInk, getInkSelectInput } from '../inkModules';
1
+ import React, { useCallback, useState } from 'react';
2
+ import { getInk, getInkSelectInput, getInkTextInput } from '../inkModules';
3
3
  import { useLayout } from '../Layout';
4
+ import type { ReviewMode } from '../../reviewPipeline';
4
5
 
5
6
  interface ModeSelectionProps {
6
- onSelect: (mode: string) => void;
7
+ onSelect: (mode: ReviewMode, extras?: { targetUrl?: string }) => void;
8
+ debugEnabled: boolean;
9
+ onToggleDebug: () => void;
7
10
  }
8
11
 
9
- const OPTIONS = [
12
+ interface ModeOption {
13
+ label: string;
14
+ value: ReviewMode;
15
+ description: string;
16
+ requiresUrl?: boolean;
17
+ }
18
+
19
+ const OPTIONS: ModeOption[] = [
10
20
  {
11
21
  label: 'Review uncommitted changes',
12
22
  value: 'uncommitted',
13
23
  description: 'Scan staged and unstaged changes only',
14
24
  },
25
+ {
26
+ label: 'Pre-production review',
27
+ value: 'preprod',
28
+ description: 'Scan the entire workspace and trigger website checks',
29
+ requiresUrl: true,
30
+ },
15
31
  ];
16
32
 
17
- export function ModeSelection({ onSelect }: ModeSelectionProps) {
18
- const { Box, Text } = getInk();
33
+ export function ModeSelection({ onSelect, debugEnabled, onToggleDebug }: ModeSelectionProps) {
34
+ const { Box, Text, useInput } = getInk();
19
35
  const SelectInput = getInkSelectInput();
36
+ const TextInput = getInkTextInput();
20
37
  const { frameWidth } = useLayout();
38
+ const [phase, setPhase] = useState<'list' | 'preprodUrl'>('list');
39
+ const [pendingMode, setPendingMode] = useState<ReviewMode>('preprod');
40
+ const [highlighted, setHighlighted] = useState<ReviewMode>('uncommitted');
41
+ const [urlValue, setUrlValue] = useState('');
42
+ const [urlError, setUrlError] = useState<string | null>(null);
43
+
44
+ const resetUrlState = useCallback(() => {
45
+ setUrlValue('');
46
+ setUrlError(null);
47
+ }, []);
48
+
49
+ const handleModeSelect = useCallback(
50
+ (mode: ReviewMode) => {
51
+ const option = OPTIONS.find((opt) => opt.value === mode);
52
+ if (!option) {
53
+ return;
54
+ }
55
+
56
+ if (option.requiresUrl) {
57
+ setPendingMode(option.value);
58
+ resetUrlState();
59
+ setPhase('preprodUrl');
60
+ return;
61
+ }
62
+
63
+ onSelect(option.value);
64
+ },
65
+ [onSelect, resetUrlState],
66
+ );
67
+
68
+ const submitUrl = useCallback(() => {
69
+ if (phase !== 'preprodUrl') {
70
+ return;
71
+ }
72
+
73
+ const trimmed = urlValue.trim();
74
+ if (!trimmed) {
75
+ setUrlError('URL is required');
76
+ return;
77
+ }
78
+
79
+ try {
80
+ const parsed = new URL(trimmed);
81
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
82
+ throw new Error('invalid');
83
+ }
84
+ setUrlError(null);
85
+ onSelect(pendingMode, { targetUrl: parsed.toString() });
86
+ } catch {
87
+ setUrlError('Enter a valid URL starting with http:// or https://');
88
+ }
89
+ }, [onSelect, pendingMode, phase, urlValue]);
90
+
91
+ useInput((input, key) => {
92
+ const normalizedInput = input.toLowerCase();
93
+ if (normalizedInput === 'd' && !key.ctrl && !key.meta) {
94
+ onToggleDebug();
95
+ return;
96
+ }
97
+
98
+ if (phase !== 'preprodUrl') {
99
+ return;
100
+ }
101
+
102
+ if (key.escape) {
103
+ setPhase('list');
104
+ resetUrlState();
105
+ return;
106
+ }
107
+
108
+ if (key.return) {
109
+ submitUrl();
110
+ }
111
+ });
112
+
113
+ if (phase === 'preprodUrl') {
114
+ return (
115
+ <Box
116
+ width={frameWidth}
117
+ flexDirection="column"
118
+ borderStyle="round"
119
+ borderColor="cyan"
120
+ paddingX={2}
121
+ paddingY={1}
122
+ gap={1}
123
+ >
124
+ <Text color="cyan" bold>
125
+ Pre-production review
126
+ </Text>
127
+ <Text dimColor>Enter the full website URL. We will trigger ai.enki.si and scan every file here.</Text>
128
+ <Box flexDirection="column">
129
+ <Text dimColor>Target URL</Text>
130
+ <TextInput
131
+ value={urlValue}
132
+ onChange={(value) => {
133
+ setUrlValue(value);
134
+ if (urlError) {
135
+ setUrlError(null);
136
+ }
137
+ }}
138
+ placeholder="https://example.com"
139
+ focus
140
+ />
141
+ </Box>
142
+ {urlError && (
143
+ <Text color="red">{urlError}</Text>
144
+ )}
145
+ <Box justifyContent="space-between">
146
+ <Text dimColor>Press Enter to start · Esc to go back</Text>
147
+ <Text dimColor>Debug: {debugEnabled ? 'On' : 'Off'} (press D)</Text>
148
+ </Box>
149
+ </Box>
150
+ );
151
+ }
21
152
 
22
153
  return (
23
154
  <Box
@@ -29,23 +160,25 @@ export function ModeSelection({ onSelect }: ModeSelectionProps) {
29
160
  paddingY={1}
30
161
  gap={1}
31
162
  >
32
- <Text color="green" bold>
33
- Choose review mode
34
- </Text>
163
+ <Box justifyContent="space-between" alignItems="center">
164
+ <Text color="green" bold>
165
+ Choose review mode
166
+ </Text>
167
+ <Text dimColor>Debug: {debugEnabled ? 'On' : 'Off'} (press D)</Text>
168
+ </Box>
35
169
  <Text dimColor>Use arrow keys, press Enter to start.</Text>
36
170
  <SelectInput
37
171
  items={OPTIONS.map((option) => ({
38
172
  label: option.label,
39
173
  value: option.value,
40
174
  }))}
41
- onSelect={(item) => onSelect(String(item.value))}
175
+ onHighlight={(item) => setHighlighted(item.value as ReviewMode)}
176
+ onSelect={(item) => handleModeSelect(item.value as ReviewMode)}
42
177
  />
43
178
  <Box flexDirection="column" marginTop={1}>
44
- {OPTIONS.map((option) => (
45
- <Text key={option.value} dimColor>
46
- · {option.description}
47
- </Text>
48
- ))}
179
+ <Text dimColor>
180
+ · {OPTIONS.find((option) => option.value === highlighted)?.description}
181
+ </Text>
49
182
  </Box>
50
183
  </Box>
51
184
  );
@@ -6,9 +6,18 @@ import { getInk, getInkSpinner } from '../inkModules';
6
6
  interface ProgressScreenProps {
7
7
  stage: ReviewStage;
8
8
  filesFound?: number;
9
+ currentFile?: string;
10
+ currentIndex?: number;
11
+ totalFiles?: number;
12
+ uncommittedFiles?: number;
13
+ contextFiles?: number;
9
14
  }
10
15
 
11
16
  const STAGE_TEXT: Record<ReviewStage, { title: string; detail: string }> = {
17
+ preparing: {
18
+ title: 'Preparing run',
19
+ detail: 'Triggering website checks and configuring the reviewer',
20
+ },
12
21
  collecting: {
13
22
  title: 'Collecting files',
14
23
  detail: 'Inspecting your working tree',
@@ -23,14 +32,45 @@ const STAGE_TEXT: Record<ReviewStage, { title: string; detail: string }> = {
23
32
  },
24
33
  };
25
34
 
26
- export function ProgressScreen({ stage, filesFound }: ProgressScreenProps) {
35
+ export function ProgressScreen({
36
+ stage,
37
+ filesFound,
38
+ currentFile,
39
+ currentIndex,
40
+ totalFiles,
41
+ uncommittedFiles,
42
+ contextFiles,
43
+ }: ProgressScreenProps) {
27
44
  const { Box, Text } = getInk();
28
45
  const Spinner = getInkSpinner();
29
46
  const { frameWidth } = useLayout();
30
47
 
31
48
  const meta = useMemo(() => STAGE_TEXT[stage], [stage]);
49
+ const base = typeof uncommittedFiles === 'number' ? uncommittedFiles : undefined;
50
+ const context = typeof contextFiles === 'number' ? contextFiles : undefined;
51
+ const totalBreakdown =
52
+ base !== undefined || context !== undefined
53
+ ? `Queued: ${base ?? '?'} uncommitted${context ? ` + ${context} context` : ''}`
54
+ : '';
32
55
  const fileStatus =
33
- typeof filesFound === 'number' && filesFound >= 0 ? `${filesFound} files queued` : 'Preparing file list…';
56
+ stage === 'collecting' && typeof filesFound === 'number' && filesFound >= 0
57
+ ? `${filesFound} files queued`
58
+ : stage === 'collecting'
59
+ ? 'Preparing file list…'
60
+ : '';
61
+
62
+ const showReviewingProgress =
63
+ stage === 'reviewing' &&
64
+ typeof currentIndex === 'number' &&
65
+ typeof totalFiles === 'number' &&
66
+ totalFiles > 0 &&
67
+ Boolean(currentFile);
68
+
69
+ const truncatedPath = useMemo(() => {
70
+ if (!currentFile) return '';
71
+ const availableWidth = Math.max(10, frameWidth - 10);
72
+ return truncateMiddle(currentFile, availableWidth);
73
+ }, [currentFile, frameWidth]);
34
74
 
35
75
  return (
36
76
  <Box
@@ -48,8 +88,23 @@ export function ProgressScreen({ stage, filesFound }: ProgressScreenProps) {
48
88
  </Text>
49
89
  </Box>
50
90
  <Text dimColor>{meta.detail}</Text>
51
- <Text dimColor>{fileStatus}</Text>
91
+ {fileStatus && <Text dimColor>{fileStatus}</Text>}
92
+ {totalBreakdown && <Text dimColor>{totalBreakdown}</Text>}
93
+ {showReviewingProgress && (
94
+ <Text dimColor>{`Reviewing (${currentIndex}/${totalFiles}): ${truncatedPath}`}</Text>
95
+ )}
52
96
  </Box>
53
97
  );
54
98
  }
55
99
 
100
+ function truncateMiddle(value: string, maxLength: number): string {
101
+ if (value.length <= maxLength) {
102
+ return value;
103
+ }
104
+
105
+ const ellipsis = '…';
106
+ const keep = Math.max(1, Math.floor((maxLength - ellipsis.length) / 2));
107
+ const start = value.slice(0, keep);
108
+ const end = value.slice(value.length - keep);
109
+ return `${start}${ellipsis}${end}`;
110
+ }
@@ -5,9 +5,10 @@ import { getInk } from '../inkModules';
5
5
 
6
6
  interface ResultsScreenProps {
7
7
  result: ReviewResult;
8
+ debugLogPath?: string | null;
8
9
  }
9
10
 
10
- export function ResultsScreen({ result }: ResultsScreenProps) {
11
+ export function ResultsScreen({ result, debugLogPath }: ResultsScreenProps) {
11
12
  const { Box, Text } = getInk();
12
13
  const { frameWidth } = useLayout();
13
14
 
@@ -21,6 +22,14 @@ export function ResultsScreen({ result }: ResultsScreenProps) {
21
22
  ];
22
23
  }, [result.totals]);
23
24
 
25
+ const scopeLine = useMemo(() => {
26
+ const breakdown = result.fileBreakdown;
27
+ if (!breakdown) return null;
28
+ return `Scope: ${breakdown.uncommitted} uncommitted + ${breakdown.context} context files`;
29
+ }, [result.fileBreakdown]);
30
+
31
+ const summaryText = result.reportSummary?.trim() || 'Summary unavailable. Open the saved report for details.';
32
+
24
33
  return (
25
34
  <Box
26
35
  width={frameWidth}
@@ -35,15 +44,26 @@ export function ResultsScreen({ result }: ResultsScreenProps) {
35
44
  Review complete for {result.repo}
36
45
  </Text>
37
46
  <Text dimColor>Duration: {result.duration}s — Files reviewed: {result.filesReviewed}</Text>
47
+ {scopeLine && <Text dimColor>{scopeLine}</Text>}
48
+
49
+ <Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} paddingY={0}>
50
+ <Text color="cyan" bold>
51
+ LLM summary
52
+ </Text>
53
+ <Text>{summaryText}</Text>
54
+ </Box>
38
55
 
39
56
  <Box flexDirection="column" marginTop={1}>
40
- {severityRows.map((row) => (
41
- <Box key={row.label}>
42
- <Text>
43
- {row.label}: {row.value}
44
- </Text>
45
- </Box>
46
- ))}
57
+ <Text bold>Severity totals</Text>
58
+ <Box flexDirection="row" flexWrap="wrap">
59
+ {severityRows.map((row) => (
60
+ <Box key={row.label} width={24}>
61
+ <Text>
62
+ {row.label}: {row.value}
63
+ </Text>
64
+ </Box>
65
+ ))}
66
+ </Box>
47
67
  </Box>
48
68
 
49
69
  {result.findings.length === 0 ? (
@@ -52,8 +72,9 @@ export function ResultsScreen({ result }: ResultsScreenProps) {
52
72
  <Box flexDirection="column" marginTop={1}>
53
73
  <Text bold>Sample findings</Text>
54
74
  {result.findings.slice(0, 5).map((finding, index) => (
55
- <Text key={`${finding.filePath}-${index}`} dimColor>
56
- · {finding.filePath ?? finding.file ?? 'unknown'} {finding.priority ? `(${finding.priority})` : ''}
75
+ <Text key={`${finding.filePath ?? finding.file ?? 'unknown'}-${index}`} dimColor>
76
+ · {finding.filePath ?? finding.file ?? 'unknown'}{' '}
77
+ {finding.priority ? `(${finding.priority})` : ''}
57
78
  </Text>
58
79
  ))}
59
80
  {result.findings.length > 5 && (
@@ -68,6 +89,12 @@ export function ResultsScreen({ result }: ResultsScreenProps) {
68
89
  </Box>
69
90
  )}
70
91
 
92
+ {debugLogPath && (
93
+ <Box marginTop={1}>
94
+ <Text dimColor>Debug logs saved to: {debugLogPath}</Text>
95
+ </Box>
96
+ )}
97
+
71
98
  <Box marginTop={1}>
72
99
  <Text dimColor>Press Enter to return to mode selection.</Text>
73
100
  </Box>
@@ -97,6 +97,10 @@ export interface ReviewOptions {
97
97
  uiLanguage?: string;
98
98
  /** Target file or directory (used internally) */
99
99
  target?: string;
100
+ /** Explicit list of files to review (bypasses discovery) */
101
+ targetFileList?: string[];
102
+ /** Runtime context information for prompt enrichment */
103
+ runContext?: RunContext;
100
104
  /** Schema instructions for structured output */
101
105
  schemaInstructions?: string;
102
106
  /** Examples for prompts */
@@ -277,6 +281,20 @@ export interface ReviewResult {
277
281
  files?: FileInfo[];
278
282
  }
279
283
 
284
+ export interface RunContext {
285
+ repoName: string;
286
+ reviewMode?: string;
287
+ totalFiles?: number;
288
+ groupIndex?: number;
289
+ groupCount?: number;
290
+ groupLabel?: string;
291
+ groupFiles?: string[];
292
+ docFiles?: string[];
293
+ uncommittedFiles?: number;
294
+ contextFiles?: number;
295
+ contextDocs?: Array<{ path: string; snippet: string }>;
296
+ }
297
+
280
298
  /**
281
299
  * Cost information for a single pass in a multi-pass review
282
300
  */
@@ -25,6 +25,9 @@ const LOG_LEVEL_MAP: Record<string, LogLevel> = {
25
25
  none: LogLevel.NONE,
26
26
  };
27
27
 
28
+ import { createWriteStream, mkdirSync, type WriteStream } from 'node:fs';
29
+ import { dirname } from 'node:path';
30
+
28
31
  // ANSI color codes for terminal output
29
32
  const COLORS = {
30
33
  reset: '\x1b[0m',
@@ -39,6 +42,8 @@ const COLORS = {
39
42
 
40
43
  // Track if we're initializing to avoid circular dependencies
41
44
  const isInitializing = false;
45
+ let logFileStream: WriteStream | null = null;
46
+ let suppressConsoleOutput = false;
42
47
 
43
48
  // Get the current log level from environment variables
44
49
  function getCurrentLogLevel(): LogLevel {
@@ -131,19 +136,54 @@ export function getLogLevel(): LogLevel {
131
136
  return currentLogLevel;
132
137
  }
133
138
 
134
- /**
135
- * Format a log message with timestamp and level
136
- * @param level The log level
137
- * @param message The message to log
138
- * @returns The formatted message
139
- */
140
- function formatLogMessage(level: string, message: string): string {
141
- const timestamp = new Date().toISOString();
139
+ function formatConsoleMessage(level: string, message: string, timestamp: string): string {
142
140
  const levelUpper = level.toUpperCase().padEnd(5);
143
-
144
141
  return `${COLORS.time}[${timestamp}]${COLORS.reset} ${COLORS[level as keyof typeof COLORS]}${levelUpper}${COLORS.reset} ${message}`;
145
142
  }
146
143
 
144
+ function formatFileMessage(level: string, message: string, timestamp: string, args: any[]): string {
145
+ const levelUpper = level.toUpperCase().padEnd(5);
146
+ const extras = args.length ? ` ${args.map((arg) => serializeLogArg(arg)).join(' ')}` : '';
147
+ return `[${timestamp}] ${levelUpper} ${message}${extras}`;
148
+ }
149
+
150
+ function serializeLogArg(arg: unknown): string {
151
+ if (typeof arg === 'string') {
152
+ return arg;
153
+ }
154
+ if (arg instanceof Error) {
155
+ return arg.stack || arg.message;
156
+ }
157
+ try {
158
+ return JSON.stringify(arg);
159
+ } catch {
160
+ return String(arg);
161
+ }
162
+ }
163
+
164
+ export function enableFileLogging(filePath: string, options?: { suppressConsole?: boolean }): void {
165
+ try {
166
+ mkdirSync(dirname(filePath), { recursive: true });
167
+ if (logFileStream) {
168
+ logFileStream.end();
169
+ }
170
+ logFileStream = createWriteStream(filePath, { flags: 'a' });
171
+ suppressConsoleOutput = options?.suppressConsole ?? true;
172
+ } catch (error) {
173
+ console.error('Failed to enable file logging:', error);
174
+ logFileStream = null;
175
+ suppressConsoleOutput = false;
176
+ }
177
+ }
178
+
179
+ export function disableFileLogging(): void {
180
+ if (logFileStream) {
181
+ logFileStream.end();
182
+ logFileStream = null;
183
+ }
184
+ suppressConsoleOutput = false;
185
+ }
186
+
147
187
  /**
148
188
  * Log a message if the current log level allows it
149
189
  * @param level The log level
@@ -151,9 +191,16 @@ function formatLogMessage(level: string, message: string): string {
151
191
  * @param args Additional arguments to log
152
192
  */
153
193
  function log(level: LogLevel, levelName: string, message: string, ...args: any[]): void {
154
- // Only log if the current log level is less than or equal to the specified level
155
- if (level >= currentLogLevel) {
156
- const formattedMessage = formatLogMessage(levelName, message);
194
+ const timestamp = new Date().toISOString();
195
+
196
+ if (logFileStream) {
197
+ logFileStream.write(`${formatFileMessage(levelName, message, timestamp, args)}\n`);
198
+ }
199
+
200
+ const shouldLogToConsole = !suppressConsoleOutput && level >= currentLogLevel;
201
+
202
+ if (shouldLogToConsole) {
203
+ const formattedMessage = formatConsoleMessage(levelName, message, timestamp);
157
204
 
158
205
  switch (level) {
159
206
  case LogLevel.DEBUG:
@@ -168,9 +215,10 @@ function log(level: LogLevel, levelName: string, message: string, ...args: any[]
168
215
  case LogLevel.ERROR:
169
216
  console.error(formattedMessage, ...args);
170
217
  break;
218
+ default:
219
+ break;
171
220
  }
172
- } else if (level === LogLevel.DEBUG && process.argv.includes('--trace-logger')) {
173
- // Only show debug suppression messages when explicitly requested
221
+ } else if (!suppressConsoleOutput && level === LogLevel.DEBUG && process.argv.includes('--trace-logger')) {
174
222
  console.error(
175
223
  `Suppressing DEBUG log because currentLogLevel=${currentLogLevel}, message was: ${message}`,
176
224
  );
@@ -237,4 +285,6 @@ export default {
237
285
  getLogLevel,
238
286
  createLogger,
239
287
  LogLevel,
288
+ enableFileLogging,
289
+ disableFileLogging,
240
290
  };