scai 0.1.74 → 0.1.76

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,318 +1,375 @@
1
1
  import readline from 'readline';
2
- import { fetchOpenPullRequests, fetchPullRequestDiff, getGitHubUsername, submitReview } from '../github/github.js';
2
+ import { fetchOpenPullRequests, getGitHubUsername, submitReview } from '../github/github.js';
3
3
  import { getRepoDetails } from '../github/repo.js';
4
4
  import { ensureGitHubAuth } from '../github/auth.js';
5
- import { postReviewComment } from '../github/postComments.js';
6
5
  import { reviewModule } from '../pipeline/modules/reviewModule.js';
7
6
  import chalk from 'chalk';
8
- // Function to fetch the PRs with requested reviews for a specific branch (default to 'main')
7
+ import fs from 'fs';
8
+ import os from 'os';
9
+ import path from 'path';
10
+ import { spawnSync } from 'child_process';
11
+ import columnify from 'columnify';
12
+ function truncate(str, length) {
13
+ return str.length > length ? str.slice(0, length - 3) + '...' : str;
14
+ }
15
+ // Fetch open PRs with review requested
16
+ import { Spinner } from '../lib/spinner.js'; // adjust path as needed
9
17
  export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
10
- const prs = await fetchOpenPullRequests(token, owner, repo);
18
+ const spinner = new Spinner('Fetching pull requests and diffs...');
19
+ spinner.start();
11
20
  const filtered = [];
12
21
  const failedPRs = [];
13
- for (const pr of prs) {
14
- const isDraft = pr.draft;
15
- const isMerged = pr.merged_at != null;
16
- const shouldInclude = !isDraft &&
17
- !isMerged &&
18
- (!filterForUser || pr.requested_reviewers?.some(r => r.login === username));
19
- if (shouldInclude) {
20
- const prNumber = pr.number;
22
+ try {
23
+ const prs = await fetchOpenPullRequests(token, owner, repo);
24
+ for (const pr of prs) {
25
+ const shouldInclude = !pr.draft &&
26
+ !pr.merged_at &&
27
+ (!filterForUser || pr.requested_reviewers?.some(r => r.login === username));
28
+ if (!shouldInclude)
29
+ continue;
21
30
  try {
22
- // Fetch full PR to get the body
31
+ const prNumber = pr.number;
23
32
  const prRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
24
33
  headers: {
25
34
  Authorization: `token ${token}`,
26
35
  Accept: 'application/vnd.github.v3+json',
27
36
  },
28
37
  });
29
- if (!prRes.ok) {
30
- throw new Error(`Failed to fetch full PR details for #${prNumber}: ${prRes.statusText}`);
31
- }
38
+ if (!prRes.ok)
39
+ throw new Error(`Failed to fetch full PR #${prNumber}`);
32
40
  const fullPR = await prRes.json();
33
- pr.body = fullPR.body ?? ''; // Assign the body if available
34
- // Fetch diff
35
- const diffUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}.diff`;
36
- const diffRes = await fetch(diffUrl, {
41
+ pr.body = fullPR.body ?? '';
42
+ const diffRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}.diff`, {
37
43
  headers: {
38
44
  Authorization: `token ${token}`,
39
45
  Accept: 'application/vnd.github.v3.diff',
40
46
  },
41
47
  });
42
- if (!diffRes.ok) {
43
- throw new Error(`${diffRes.status} ${diffRes.statusText}`);
44
- }
48
+ if (!diffRes.ok)
49
+ throw new Error(`Failed to fetch diff for PR #${prNumber}`);
45
50
  const diff = await diffRes.text();
46
51
  filtered.push({ pr, diff });
47
52
  }
48
53
  catch (err) {
49
- console.warn(`⚠️ Skipping PR #${pr.number} due to error: ${err.message}`);
54
+ console.warn(`⚠️ Skipping PR #${pr.number}: ${err.message}`);
50
55
  failedPRs.push(pr);
51
56
  }
52
57
  }
58
+ if (filtered.length === 0) {
59
+ const msg = filterForUser
60
+ ? `No open pull requests found for review by '${username}'.`
61
+ : `No open pull requests found.`;
62
+ spinner.succeed(msg);
63
+ }
64
+ else {
65
+ spinner.succeed(`Fetched ${filtered.length} PR(s) with diffs.`);
66
+ }
67
+ if (failedPRs.length > 0) {
68
+ const failedList = failedPRs.map(pr => `#${pr.number}`).join(', ');
69
+ console.warn(`⚠️ Skipped ${failedPRs.length} PR(s): ${failedList}`);
70
+ }
71
+ return filtered;
53
72
  }
54
- if (failedPRs.length > 0) {
55
- const failedList = failedPRs.map(pr => `#${pr.number}`).join(', ');
56
- console.warn(`⚠️ Skipped ${failedPRs.length} PR(s) (${failedList}) due to errors.`);
73
+ catch (err) {
74
+ spinner.fail(`Error fetching pull requests: ${err.message}`);
75
+ return [];
57
76
  }
58
- return filtered;
59
77
  }
60
- // Ask user to pick a PR to review
78
+ // Prompt user to select PR
61
79
  function askUserToPickPR(prs) {
62
80
  return new Promise((resolve) => {
63
81
  if (prs.length === 0) {
64
82
  console.log("⚠️ No pull requests with review requested.");
65
83
  return resolve(null);
66
84
  }
67
- console.log(chalk.blue("\n📦 Open Pull Requests with review requested:"));
68
- prs.forEach((pr, i) => {
69
- console.log(`${i + 1}) #${pr.number} - ${pr.title}`);
70
- });
71
- const rl = readline.createInterface({
72
- input: process.stdin,
73
- output: process.stdout,
85
+ const rows = prs.map((pr, i) => ({
86
+ '#': i + 1,
87
+ ID: `#${pr.number}`,
88
+ Title: truncate(pr.title, 50),
89
+ Author: pr.user || '—',
90
+ Status: pr.draft ? 'Draft' : 'Open',
91
+ Created: pr.created_at?.split('T')[0] || '',
92
+ Reviewers: pr.requested_reviewers?.map(r => r.login).join(', ') || '—',
93
+ }));
94
+ console.log(chalk.blue("\n📦 Open Pull Requests:"));
95
+ console.log(columnify(rows, {
96
+ columnSplitter: ' ',
97
+ headingTransform: (h) => chalk.cyan(h.toUpperCase()),
98
+ config: {
99
+ Title: { maxWidth: 50 },
100
+ Reviewers: { maxWidth: 30 }
101
+ }
102
+ }));
103
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
104
+ rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
105
+ rl.close();
106
+ const index = parseInt(answer, 10);
107
+ if (!isNaN(index) && index >= 1 && index <= prs.length) {
108
+ resolve(index - 1);
109
+ }
110
+ else {
111
+ console.log('⚠️ Invalid selection.');
112
+ resolve(null);
113
+ }
74
114
  });
75
- const askQuestion = () => {
76
- rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
77
- const index = parseInt(answer, 10);
78
- if (!isNaN(index) && index >= 1 && index <= prs.length) {
79
- resolve(index - 1); // Return array index, not PR number
80
- rl.close();
81
- }
82
- else {
83
- console.log('⚠️ Invalid selection. Please enter a number between 1 and ' + prs.length);
84
- askQuestion(); // Retry asking for input
85
- }
86
- });
87
- };
88
- askQuestion(); // Initial call to ask the user
89
115
  });
90
116
  }
91
- // Ask user to choose review method: whole PR or chunk-by-chunk
117
+ // Prompt for review method
92
118
  function askReviewMethod() {
93
119
  return new Promise((resolve) => {
94
- const rl = readline.createInterface({
95
- input: process.stdin,
96
- output: process.stdout,
97
- });
120
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
98
121
  console.log("\n🔍 Choose review method:");
99
122
  console.log('1) Review whole PR at once');
100
123
  console.log('2) Review chunk by chunk');
101
124
  rl.question(`👉 Choose an option [1-2]: `, (answer) => {
102
125
  rl.close();
103
- if (answer === '1') {
104
- resolve('whole');
105
- }
106
- else if (answer === '2') {
107
- resolve('chunk');
108
- }
109
- else {
110
- console.log('⚠️ Invalid selection. Defaulting to "whole".');
111
- resolve('whole');
112
- }
126
+ resolve(answer === '2' ? 'chunk' : 'whole');
113
127
  });
114
128
  });
115
129
  }
116
- // Ask user to approve or reject the review suggestion
117
- function askReviewApproval(suggestion) {
130
+ // Prompt for review approval
131
+ function askReviewApproval() {
118
132
  return new Promise((resolve) => {
119
- console.log('\n💡 AI-suggested review:\n');
120
- console.log(suggestion);
121
133
  console.log('\n---');
122
134
  console.log('1) ✅ Approve');
123
135
  console.log('2) ❌ Reject');
124
136
  console.log('3) ✍️ Edit');
125
137
  console.log('4) ⌨️ Write your own review');
126
138
  console.log('5) 🚫 Cancel');
127
- const rl = readline.createInterface({
128
- input: process.stdin,
129
- output: process.stdout,
130
- });
139
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
131
140
  rl.question(`\n👉 Choose an option [1-5]: `, (answer) => {
132
141
  rl.close();
133
- if (answer === '1') {
142
+ if (answer === '1')
134
143
  resolve('approve');
135
- }
136
- else if (answer === '2') {
144
+ else if (answer === '2')
137
145
  resolve('reject');
138
- }
139
- else if (answer === '3') {
146
+ else if (answer === '3')
140
147
  resolve('edit');
141
- }
142
- else if (answer === '4') {
148
+ else if (answer === '4')
143
149
  resolve('custom');
144
- }
145
- else if (answer === '5') {
150
+ else
146
151
  resolve('cancel');
147
- }
148
- else {
149
- console.log('⚠️ Invalid selection. Defaulting to "approve".');
150
- resolve('approve');
151
- }
152
152
  });
153
153
  });
154
154
  }
155
- // Prompt for custom review input
155
+ // Prompt for custom review
156
156
  function promptCustomReview() {
157
157
  return new Promise((resolve) => {
158
- const rl = readline.createInterface({
159
- input: process.stdin,
160
- output: process.stdout,
161
- });
158
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
162
159
  rl.question('\n📝 Enter your custom review:\n> ', (input) => {
163
160
  rl.close();
164
161
  resolve(input.trim());
165
162
  });
166
163
  });
167
164
  }
165
+ // Prompt for editing review
166
+ export async function promptEditReview(suggestedReview) {
167
+ const tmpFilePath = path.join(os.tmpdir(), 'scai-review.txt');
168
+ fs.writeFileSync(tmpFilePath, `# Edit your review below.\n# Lines starting with '#' will be ignored.\n\n${suggestedReview}`);
169
+ const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'vi');
170
+ spawnSync(editor, [tmpFilePath], { stdio: 'inherit' });
171
+ const editedContent = fs.readFileSync(tmpFilePath, 'utf-8');
172
+ return editedContent
173
+ .split('\n')
174
+ .filter(line => !line.trim().startsWith('#'))
175
+ .join('\n')
176
+ .trim() || suggestedReview;
177
+ }
178
+ // Split diff into file-based chunks
168
179
  function chunkDiff(diff) {
169
180
  const rawChunks = diff.split(/^diff --git /m).filter(Boolean);
170
181
  return rawChunks.map(chunk => {
171
182
  const fullChunk = 'diff --git ' + chunk;
172
- // Try to extract the file name from the diff header
173
- const match = fullChunk.match(/^diff --git a\/(.+?) b\/.+?\n/);
174
- const filePath = match ? match[1] : 'unknown';
175
- return { filePath, content: fullChunk };
183
+ const filePathMatch = fullChunk.match(/^diff --git a\/(.+?) b\//);
184
+ const filePath = filePathMatch ? filePathMatch[1] : 'unknown';
185
+ return {
186
+ filePath,
187
+ content: fullChunk,
188
+ };
176
189
  });
177
190
  }
178
- // Function to color diff lines
191
+ // Color lines in diff
179
192
  function colorDiffLine(line) {
180
- if (line.startsWith('+')) {
181
- return chalk.green(line); // New lines (added)
182
- }
183
- else if (line.startsWith('-')) {
184
- return chalk.red(line); // Deleted lines
185
- }
186
- else if (line.startsWith('@@')) {
187
- return chalk.yellow(line); // Modified lines (with context)
188
- }
189
- return line; // Default case (unchanged lines)
193
+ if (line.startsWith('+'))
194
+ return chalk.green(line);
195
+ if (line.startsWith('-'))
196
+ return chalk.red(line);
197
+ if (line.startsWith('@@'))
198
+ return chalk.yellow(line);
199
+ return line;
190
200
  }
191
- // Updated reviewChunk function to apply coloring
201
+ // Review a single chunk
192
202
  export async function reviewChunk(chunk, chunkIndex, totalChunks) {
193
- console.log(chalk.yellow(`\n🔍 Reviewing chunk ${chunkIndex + 1} of ${totalChunks}:`));
194
- console.log(`File: ${chunk.filePath}`);
195
- // Split the chunk content into lines and color each line accordingly
196
- const coloredDiff = chunk.content.split('\n').map(colorDiffLine).join('\n');
197
- // Log the colored diff with line numbers and progress
198
- console.log(`Starting line number: 30`); // Adjust based on actual starting line logic
203
+ const lines = chunk.content.split('\n');
204
+ const coloredDiff = lines.map(colorDiffLine).join('\n');
205
+ console.log(chalk.gray('\n' + '━'.repeat(60)));
206
+ console.log(`📄 ${chalk.bold('File')}: ${chalk.cyan(chunk.filePath)}`);
207
+ console.log(`🔢 ${chalk.bold('Chunk')}: ${chunkIndex + 1} of ${totalChunks}`);
208
+ const suggestion = await reviewModule.run({
209
+ content: chunk.content,
210
+ filepath: chunk.filePath
211
+ });
212
+ const summary = suggestion.content?.trim() || 'AI review summary not available.';
213
+ console.log(`🔍 ${chalk.bold('Summary')}: ${summary}`);
214
+ console.log(`\n${chalk.bold('--- Diff ---')}\n`);
199
215
  console.log(coloredDiff);
200
- // Get the review suggestion from the model (as before)
201
- const suggestion = await reviewModule.run({ content: chunk.content, filepath: chunk.filePath });
202
- console.log("\n💡 AI-suggested review:\n");
203
- console.log(suggestion.content);
204
- // Ask user to approve or reject the chunk
205
- const reviewChoice = await askReviewApproval('Do you approve this code chunk?');
206
- return reviewChoice;
216
+ console.log(`\n${chalk.bold('--- AI Review ---')}\n`);
217
+ console.log(chalk.blue(`💬 ${summary}`));
218
+ console.log(chalk.gray('━'.repeat(60)));
219
+ const choice = await promptChunkReviewMenu();
220
+ if (choice === 'edit') {
221
+ const edited = await promptEditReview(summary); // edit based on the suggestion
222
+ return { choice: edited, summary: edited };
223
+ }
224
+ else if (choice === 'skip') {
225
+ await waitForSpaceOrQ(); // pause between chunks
226
+ return { choice: 'cancel', summary }; // skip this one
227
+ }
228
+ return { choice, summary };
229
+ }
230
+ function waitForSpaceOrQ() {
231
+ return new Promise(resolve => {
232
+ process.stdin.setRawMode(true);
233
+ process.stdin.resume();
234
+ process.stdout.write('\n⏭️ (Press [space] to skip, [q] to quit, or any other key to show menu)\n');
235
+ function onKeyPress(chunk) {
236
+ const key = chunk.toString();
237
+ if (key === ' ' || key === 'q' || key === 'Q') {
238
+ process.stdin.setRawMode(false);
239
+ process.stdin.pause();
240
+ process.stdin.removeListener('data', onKeyPress);
241
+ resolve();
242
+ }
243
+ }
244
+ process.stdin.on('data', onKeyPress);
245
+ });
207
246
  }
247
+ export async function promptChunkReviewMenu() {
248
+ return new Promise((resolve) => {
249
+ console.log('\nReview options for this chunk:');
250
+ console.log(' 1) 💬 Post AI review as comment');
251
+ console.log(' 2) ✍️ Edit the review before posting');
252
+ console.log(' 3) ⌨️ Write a custom comment');
253
+ console.log(' 4) ❌ Mark this chunk as needing changes');
254
+ console.log(' 5) ⏭️ Skip this chunk without commenting');
255
+ console.log(chalk.gray(' (Press [space] to skip chunk, [q] to quit review, or any other key to show menu)\n'));
256
+ // Fallback to menu input if key was not space/q
257
+ function askWithReadline() {
258
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
259
+ rl.question('👉 Choose [1–5]: ', (answer) => {
260
+ rl.close();
261
+ switch (answer.trim()) {
262
+ case '1': return resolve('approve');
263
+ case '2': return resolve('edit');
264
+ case '3': return resolve('custom');
265
+ case '4': return resolve('reject');
266
+ case '5': return resolve('skip');
267
+ default:
268
+ console.log('⚠️ Invalid option. Skipping chunk.');
269
+ return resolve('skip');
270
+ }
271
+ });
272
+ }
273
+ // Raw key listener for quick actions
274
+ function onKeyPress(key) {
275
+ const keyStr = key.toString().toLowerCase();
276
+ process.stdin.setRawMode(false);
277
+ process.stdin.pause();
278
+ if (keyStr === ' ') {
279
+ return resolve('skip');
280
+ }
281
+ else if (keyStr === 'q') {
282
+ console.log('\n👋 Exiting review.');
283
+ process.exit(0);
284
+ }
285
+ else {
286
+ // flush any remaining input
287
+ process.stdin.removeAllListeners('data');
288
+ askWithReadline();
289
+ }
290
+ }
291
+ // Prepare for keypress
292
+ process.stdin.setRawMode(true);
293
+ process.stdin.resume();
294
+ process.stdin.once('data', onKeyPress);
295
+ });
296
+ }
297
+ // Main command to review PR
208
298
  export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
209
299
  try {
210
- console.log("🔍 Fetching pull requests and diffs...");
211
- const token = await ensureGitHubAuth(); // Get GitHub token
300
+ const token = await ensureGitHubAuth();
212
301
  const username = await getGitHubUsername(token);
213
- const { owner, repo } = getRepoDetails(); // Get repository details (owner, repo name)
214
- console.log(`👤 Authenticated user: ${username}`);
215
- console.log(`📦 GitHub repo: ${owner}/${repo}`);
216
- console.log(`🔍 Filtering ${showAll ? "all" : "user-specific"} PRs for branch: ${branch}`);
302
+ const { owner, repo } = getRepoDetails();
217
303
  const prsWithReviewRequested = await getPullRequestsForReview(token, owner, repo, username, branch, !showAll);
218
- console.log(`🔍 Found ${prsWithReviewRequested.length} PR(s) requiring review.`);
219
- if (prsWithReviewRequested.length === 0) {
220
- console.log("⚠️ No PRs found with review requested.");
304
+ if (prsWithReviewRequested.length === 0)
221
305
  return;
222
- }
223
306
  const selectedIndex = await askUserToPickPR(prsWithReviewRequested.map(p => p.pr));
224
307
  if (selectedIndex === null)
225
308
  return;
226
309
  const { pr, diff } = prsWithReviewRequested[selectedIndex];
227
310
  if (pr.body) {
228
- console.log(chalk.magentaBright('\n📝 Pull Request Description:\n'));
229
- console.log(chalk.gray(pr.body));
230
- console.log(chalk.magentaBright('\n---\n'));
311
+ console.log(chalk.magentaBright('\n📝 PR Description:\n') + chalk.gray(pr.body));
231
312
  }
232
- let prDiff = diff;
233
- if (!prDiff) {
234
- console.log(`🔍 Fetching diff for PR #${pr.number}...`);
235
- prDiff = await fetchPullRequestDiff(pr, token);
236
- }
237
- const chunkComments = {}; // Declare here for global access
238
- const reviewMethod = await askReviewMethod(); // Ask user for review method (whole or chunk)
313
+ // Skip review method if there's only one chunk
314
+ const chunks = chunkDiff(diff);
315
+ const reviewMethod = chunks.length > 1 ? await askReviewMethod() : 'chunk'; // Auto-select chunk review if only 1 chunk
316
+ let allApproved = true; // Track if everything is approved
239
317
  if (reviewMethod === 'whole') {
240
- console.log(`🔍 Reviewing the entire PR at once...`);
241
- const suggestion = await reviewModule.run({ content: prDiff, filepath: 'Whole PR Diff' });
242
- console.log("\n💡 AI-suggested review:\n");
243
- console.log(suggestion.content);
244
- // Store the entire review choice in chunkComments
245
- const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
318
+ const suggestion = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
319
+ console.log(chalk.yellowBright("Suggestion: ", suggestion));
320
+ const finalReviewChoice = await askReviewApproval();
321
+ let reviewText = '';
246
322
  if (finalReviewChoice === 'approve') {
247
- console.log(`✅ Review for PR #${pr.number} approved.`);
248
- await postReviewComments(pr, chunkComments, token);
249
- await submitReview(pr.number, 'PR approved', 'APPROVE');
323
+ reviewText = 'PR approved';
324
+ await submitReview(pr.number, suggestion.content, 'APPROVE'); // Final review approval
250
325
  }
251
326
  else if (finalReviewChoice === 'reject') {
252
- console.log(`❌ Review for PR #${pr.number} rejected.`);
253
- await postReviewComments(pr, chunkComments, token);
254
- await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
327
+ reviewText = 'Changes requested';
328
+ await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES'); // Final review rejection
255
329
  }
256
- else if (finalReviewChoice === 'cancel') {
257
- console.log(`🚪 Review process cancelled.`);
258
- return;
330
+ else if (finalReviewChoice === 'custom') {
331
+ reviewText = await promptCustomReview();
332
+ await submitReview(pr.number, reviewText, 'COMMENT'); // Custom comment
259
333
  }
260
- else {
261
- const customReview = await promptCustomReview();
262
- console.log(`💬 Custom review: ${customReview}`);
263
- await postReviewComments(pr, chunkComments, token);
264
- await submitReview(pr.number, customReview, 'COMMENT');
334
+ else if (finalReviewChoice === 'edit') {
335
+ reviewText = await promptEditReview(suggestion.content);
336
+ await submitReview(pr.number, reviewText, 'COMMENT'); // Edited comment
265
337
  }
266
338
  }
267
339
  else {
268
- const chunks = chunkDiff(prDiff); // Split the diff into chunks
269
- // Log the total number of chunks
270
340
  console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
271
- // Iterate over each chunk, passing the index and total chunk count
272
341
  for (let i = 0; i < chunks.length; i++) {
273
342
  const chunk = chunks[i];
274
- // Pass the chunk, index (i), and totalChunks to the reviewChunk function
275
- const reviewChoice = await reviewChunk(chunk, i, chunks.length); // Review each chunk
276
- if (reviewChoice === 'approve') {
277
- console.log('✅ Approving this chunk.');
278
- chunkComments[chunk.filePath] = ['Approved'];
343
+ const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
344
+ if (choice === 'approve') {
345
+ console.log(`✔️ Approved chunk ${i + 1}`);
346
+ await submitReview(pr.number, summary, 'COMMENT');
279
347
  }
280
- else if (reviewChoice === 'reject') {
281
- console.log('❌ Rejecting this chunk.');
282
- chunkComments[chunk.filePath] = ['Rejected'];
348
+ else if (choice === 'reject') {
349
+ allApproved = false;
350
+ const reviewText = `Changes requested for chunk ${i + 1}: ${summary}`;
351
+ await submitReview(pr.number, reviewText, 'REQUEST_CHANGES');
283
352
  }
284
- else if (reviewChoice === 'cancel') {
285
- console.log('🚪 Review process cancelled.');
286
- return; // Exit if user cancels
287
- }
288
- else {
289
- console.log('✍️ Custom review added.');
353
+ else if (choice === 'custom') {
290
354
  const customReview = await promptCustomReview();
291
- console.log(`💬 Custom review: ${customReview}`);
292
- chunkComments[chunk.filePath] = [customReview];
355
+ await submitReview(pr.number, customReview, 'COMMENT');
356
+ }
357
+ else if (choice === 'cancel' || choice === 'skip') {
358
+ console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
359
+ }
360
+ else if (typeof choice === 'string') {
361
+ // e.g., from 'edit' returning custom text
362
+ await submitReview(pr.number, choice, 'COMMENT');
293
363
  }
294
364
  }
295
- // After chunk review, ask for the final approval decision
296
- const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
297
- if (finalReviewChoice === 'approve') {
298
- console.log(`✅ Review for PR #${pr.number} approved.`);
299
- await postReviewComments(pr, chunkComments, token);
300
- await submitReview(pr.number, 'PR approved', 'APPROVE');
301
- }
302
- else if (finalReviewChoice === 'reject') {
303
- console.log(`❌ Review for PR #${pr.number} rejected.`);
304
- await postReviewComments(pr, chunkComments, token);
305
- await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
306
- }
307
- else if (finalReviewChoice === 'cancel') {
308
- console.log(`🚪 Review process cancelled.`);
309
- return;
365
+ // After reviewing all chunks, approve or reject the whole PR based on chunk reviews
366
+ if (allApproved) {
367
+ console.log(chalk.green('✔️ All chunks approved. Submitting final PR approval.'));
368
+ await submitReview(pr.number, 'PR approved after reviewing all chunks', 'APPROVE'); // Final PR approval
310
369
  }
311
370
  else {
312
- const customReview = await promptCustomReview();
313
- console.log(`💬 Custom review: ${customReview}`);
314
- await postReviewComments(pr, chunkComments, token);
315
- await submitReview(pr.number, customReview, 'COMMENT');
371
+ console.log(chalk.red('❌ Not all chunks were approved. Changes requested.'));
372
+ await submitReview(pr.number, 'PR changes requested based on chunk reviews', 'REQUEST_CHANGES'); // Final PR rejection
316
373
  }
317
374
  }
318
375
  }
@@ -320,15 +377,3 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
320
377
  console.error("❌ Error reviewing PR:", err.message);
321
378
  }
322
379
  }
323
- // Function to post all comments to GitHub after the review
324
- async function postReviewComments(pr, chunkComments, token) {
325
- const { owner, repo } = getRepoDetails(); // Get the repo details
326
- for (const chunk in chunkComments) {
327
- const comments = chunkComments[chunk];
328
- const fileName = chunk; // Use chunk's file path
329
- const lineNumber = 10; // Extract the actual line number from the chunk content
330
- for (const comment of comments) {
331
- await postReviewComment(token, owner, repo, pr.number, fileName, lineNumber, comment);
332
- }
333
- }
334
- }
@@ -7,8 +7,12 @@ export const reviewModule = {
7
7
  const model = Config.getModel();
8
8
  const prompt = `
9
9
  You are a senior software engineer reviewing a pull request.
10
- Give clear and very short constructive feedback based on the code changes below.
11
- Only mention issues of greater concern. Less is more.
10
+ ALWAYS make 3 concise suggestions for improvements based on the input code diff.
11
+ Use this format ONLY and output ONLY those suggestions:
12
+
13
+ 1. ...
14
+ 2. ...
15
+ 3. ...
12
16
 
13
17
  Changes:
14
18
  ${content}
@@ -1,4 +1,4 @@
1
- import columnify from 'columnify';
1
+ import columnify from "columnify";
2
2
  export function styleOutput(summaryText) {
3
3
  const terminalWidth = process.stdout.columns || 80;
4
4
  // You can control wrapping here
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"