scai 0.1.75 → 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.
@@ -13,15 +13,20 @@ function truncate(str, length) {
13
13
  return str.length > length ? str.slice(0, length - 3) + '...' : str;
14
14
  }
15
15
  // Fetch open PRs with review requested
16
+ import { Spinner } from '../lib/spinner.js'; // adjust path as needed
16
17
  export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
17
- const prs = await fetchOpenPullRequests(token, owner, repo);
18
+ const spinner = new Spinner('Fetching pull requests and diffs...');
19
+ spinner.start();
18
20
  const filtered = [];
19
21
  const failedPRs = [];
20
- for (const pr of prs) {
21
- const shouldInclude = !pr.draft &&
22
- !pr.merged_at &&
23
- (!filterForUser || pr.requested_reviewers?.some(r => r.login === username));
24
- if (shouldInclude) {
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;
25
30
  try {
26
31
  const prNumber = pr.number;
27
32
  const prRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
@@ -50,21 +55,25 @@ export async function getPullRequestsForReview(token, owner, repo, username, bra
50
55
  failedPRs.push(pr);
51
56
  }
52
57
  }
53
- }
54
- // After collecting filtered PRs
55
- if (filtered.length === 0) {
56
- if (filterForUser) {
57
- console.log(`ℹ️ No open pull requests found for review by '${username}'.`);
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);
58
63
  }
59
64
  else {
60
- console.log(`ℹ️ No open pull requests found.`);
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}`);
61
70
  }
71
+ return filtered;
62
72
  }
63
- if (failedPRs.length > 0) {
64
- const failedList = failedPRs.map(pr => `#${pr.number}`).join(', ');
65
- console.warn(`⚠️ Skipped ${failedPRs.length} PR(s): ${failedList}`);
73
+ catch (err) {
74
+ spinner.fail(`Error fetching pull requests: ${err.message}`);
75
+ return [];
66
76
  }
67
- return filtered;
68
77
  }
69
78
  // Prompt user to select PR
70
79
  function askUserToPickPR(prs) {
@@ -119,7 +128,7 @@ function askReviewMethod() {
119
128
  });
120
129
  }
121
130
  // Prompt for review approval
122
- function askReviewApproval(suggestion) {
131
+ function askReviewApproval() {
123
132
  return new Promise((resolve) => {
124
133
  console.log('\n---');
125
134
  console.log('1) ✅ Approve');
@@ -191,18 +200,99 @@ function colorDiffLine(line) {
191
200
  }
192
201
  // Review a single chunk
193
202
  export async function reviewChunk(chunk, chunkIndex, totalChunks) {
194
- console.log(chalk.yellow(`\n🔍 Reviewing chunk ${chunkIndex + 1} of ${totalChunks}:`));
195
- console.log(`File: ${chunk.filePath}`);
196
- const coloredDiff = chunk.content.split('\n').map(colorDiffLine).join('\n');
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`);
197
215
  console.log(coloredDiff);
198
- const suggestion = await reviewModule.run({ content: chunk.content, filepath: chunk.filePath });
199
- console.log("\n💡 AI-suggested review:\n");
200
- console.log(suggestion.content);
201
- const reviewChoice = await askReviewApproval(suggestion.content);
202
- if (reviewChoice === 'edit') {
203
- return await promptEditReview(suggestion.content);
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
204
227
  }
205
- return reviewChoice;
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
+ });
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
+ });
206
296
  }
207
297
  // Main command to review PR
208
298
  export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
@@ -220,49 +310,67 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
220
310
  if (pr.body) {
221
311
  console.log(chalk.magentaBright('\n📝 PR Description:\n') + chalk.gray(pr.body));
222
312
  }
223
- const reviewMethod = await askReviewMethod();
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
224
317
  if (reviewMethod === 'whole') {
225
318
  const suggestion = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
226
- console.log(suggestion.content);
227
- const finalReviewChoice = await askReviewApproval(suggestion.content);
319
+ console.log(chalk.yellowBright("Suggestion: ", suggestion));
320
+ const finalReviewChoice = await askReviewApproval();
228
321
  let reviewText = '';
229
322
  if (finalReviewChoice === 'approve') {
230
323
  reviewText = 'PR approved';
231
- await submitReview(pr.number, reviewText, 'APPROVE');
324
+ await submitReview(pr.number, suggestion.content, 'APPROVE'); // Final review approval
232
325
  }
233
326
  else if (finalReviewChoice === 'reject') {
234
327
  reviewText = 'Changes requested';
235
- await submitReview(pr.number, reviewText, 'REQUEST_CHANGES');
328
+ await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES'); // Final review rejection
236
329
  }
237
330
  else if (finalReviewChoice === 'custom') {
238
331
  reviewText = await promptCustomReview();
239
- await submitReview(pr.number, reviewText, 'COMMENT');
332
+ await submitReview(pr.number, reviewText, 'COMMENT'); // Custom comment
240
333
  }
241
334
  else if (finalReviewChoice === 'edit') {
242
335
  reviewText = await promptEditReview(suggestion.content);
243
- await submitReview(pr.number, reviewText, 'COMMENT');
336
+ await submitReview(pr.number, reviewText, 'COMMENT'); // Edited comment
244
337
  }
245
338
  }
246
339
  else {
247
- const chunks = chunkDiff(diff);
248
340
  console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
249
341
  for (let i = 0; i < chunks.length; i++) {
250
342
  const chunk = chunks[i];
251
- const reviewResult = await reviewChunk(chunk, i, chunks.length);
252
- if (reviewResult === 'approve') {
253
- await submitReview(pr.number, 'Approved chunk', 'APPROVE');
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');
254
347
  }
255
- else if (reviewResult === 'reject') {
256
- await submitReview(pr.number, 'Changes requested for chunk', 'REQUEST_CHANGES');
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');
257
352
  }
258
- else if (reviewResult === 'custom') {
353
+ else if (choice === 'custom') {
259
354
  const customReview = await promptCustomReview();
260
355
  await submitReview(pr.number, customReview, 'COMMENT');
261
356
  }
262
- else {
263
- await submitReview(pr.number, reviewResult, 'COMMENT');
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');
264
363
  }
265
364
  }
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
369
+ }
370
+ else {
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
373
+ }
266
374
  }
267
375
  }
268
376
  catch (err) {
@@ -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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.75",
3
+ "version": "0.1.76",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"