@in-the-loop-labs/pair-review 1.3.3 → 1.4.1

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.
@@ -304,14 +304,62 @@ class LocalManager {
304
304
  return;
305
305
  }
306
306
 
307
- // Check staleness FIRST, before showing config modal
307
+ // Run stale check and settings fetch in parallel to minimize dialog delay.
308
+ // Use AbortController so the fetch is truly cancelled on timeout,
309
+ // freeing the HTTP connection for subsequent requests.
310
+ const STALE_TIMEOUT = 2000;
311
+ const staleAbort = new AbortController();
312
+ const staleTimer = setTimeout(() => {
313
+ console.debug(`[Analyze] stale-check timed out after ${STALE_TIMEOUT}ms, aborting`);
314
+ staleAbort.abort();
315
+ }, STALE_TIMEOUT);
316
+ const staleCheckPromise = fetch(`/api/local/${reviewId}/check-stale`, { signal: staleAbort.signal })
317
+ .then(r => {
318
+ clearTimeout(staleTimer);
319
+ if (!r.ok) {
320
+ console.warn(`Stale check failed with status ${r.status}`);
321
+ return { _fetchError: true, status: r.status };
322
+ }
323
+ return r.json();
324
+ })
325
+ .catch(err => {
326
+ clearTimeout(staleTimer);
327
+ if (err.name === 'AbortError') {
328
+ console.debug('[Analyze] stale-check aborted (timeout)');
329
+ } else {
330
+ console.warn('[Analyze] stale-check fetch error:', err);
331
+ }
332
+ return null;
333
+ });
334
+
308
335
  try {
309
- const staleResponse = await fetch(`/api/local/${reviewId}/check-stale`);
310
- if (staleResponse.ok) {
311
- const staleData = await staleResponse.json();
336
+ // Show analysis config modal
337
+ if (!manager.analysisConfigModal) {
338
+ clearTimeout(staleTimer);
339
+ console.warn('AnalysisConfigModal not initialized, proceeding without config');
340
+ await self.startLocalAnalysis(btn, {});
341
+ return;
342
+ }
343
+
344
+ // Fetch stale check, repo settings, and last instructions in parallel
345
+ const [staleResult, repoSettings, lastInstructions] = await Promise.all([
346
+ staleCheckPromise,
347
+ manager.fetchRepoSettings().catch(() => null),
348
+ manager.fetchLastCustomInstructions().catch(() => '')
349
+ ]);
312
350
 
313
- if (staleData.isStale === true) {
314
- // Working directory has changed - show dialog with options
351
+ // Process stale check result
352
+ try {
353
+ if (staleResult === null) {
354
+ // Timed out or network error — fail open
355
+ if (window.toast) {
356
+ window.toast.showWarning('Could not verify working directory is current.');
357
+ }
358
+ } else if (staleResult._fetchError) {
359
+ if (window.toast) {
360
+ window.toast.showWarning(`Could not verify working directory is current (${staleResult.status}).`);
361
+ }
362
+ } else if (staleResult.isStale === true) {
315
363
  if (window.confirmDialog) {
316
364
  const choice = await window.confirmDialog.show({
317
365
  title: 'Files Have Changed',
@@ -323,40 +371,22 @@ class LocalManager {
323
371
  });
324
372
 
325
373
  if (choice === 'confirm') {
326
- // User wants to refresh first, then continue to analysis
327
374
  await self.refreshDiff();
328
- // Continue to config modal after refresh (don't return)
329
375
  } else if (choice !== 'secondary') {
330
- // User cancelled
331
376
  return;
332
377
  }
333
- // Both 'confirm' (after refresh) and 'secondary' continue to config modal
334
378
  }
335
- } else if (staleData.isStale === null && staleData.error) {
336
- // Couldn't verify - show toast warning
379
+ } else if (staleResult.isStale === null && staleResult.error) {
337
380
  if (window.toast) {
338
381
  window.toast.showWarning('Could not verify working directory is current.');
339
382
  }
340
383
  }
384
+ } catch (staleError) {
385
+ console.warn('[Local] Error processing staleness:', staleError);
386
+ if (window.toast) {
387
+ window.toast.showWarning('Could not verify working directory is current.');
388
+ }
341
389
  }
342
- } catch (staleError) {
343
- console.warn('[Local] Error checking staleness:', staleError);
344
- if (window.toast) {
345
- window.toast.showWarning('Could not verify working directory is current.');
346
- }
347
- }
348
-
349
- try {
350
- // Show analysis config modal
351
- if (!manager.analysisConfigModal) {
352
- console.warn('AnalysisConfigModal not initialized, proceeding without config');
353
- await self.startLocalAnalysis(btn, {});
354
- return;
355
- }
356
-
357
- // Get repo settings for default instructions
358
- const repoSettings = await manager.fetchRepoSettings().catch(() => null);
359
- const lastInstructions = await manager.fetchLastCustomInstructions().catch(() => '');
360
390
 
361
391
  // Determine model and provider
362
392
  const modelStorageKey = `pair-review-model:local-${reviewId}`;
@@ -1361,10 +1391,90 @@ class LocalManager {
1361
1391
  });
1362
1392
  }
1363
1393
 
1394
+ /**
1395
+ * Initialize inline name editing for the review title in the header
1396
+ */
1397
+ initNameEditing() {
1398
+ const nameEl = document.getElementById('local-review-name');
1399
+ if (!nameEl || nameEl.dataset.listenerAttached) return;
1400
+ nameEl.dataset.listenerAttached = 'true';
1401
+
1402
+ const reviewId = this.reviewId;
1403
+
1404
+ nameEl.addEventListener('click', () => {
1405
+ if (nameEl.querySelector('input')) return; // already editing
1406
+
1407
+ const currentName = nameEl.dataset.currentName || '';
1408
+ const input = document.createElement('input');
1409
+ input.type = 'text';
1410
+ input.className = 'local-review-name-input';
1411
+ input.value = currentName;
1412
+ input.placeholder = 'Untitled';
1413
+
1414
+ nameEl.textContent = '';
1415
+ nameEl.appendChild(input);
1416
+ input.focus();
1417
+ input.select();
1418
+
1419
+ let saved = false;
1420
+
1421
+ async function save() {
1422
+ if (saved) return;
1423
+ saved = true;
1424
+ const newName = input.value.trim() || null;
1425
+ try {
1426
+ const response = await fetch(`/api/local/${reviewId}/name`, {
1427
+ method: 'PATCH',
1428
+ headers: { 'Content-Type': 'application/json' },
1429
+ body: JSON.stringify({ name: newName })
1430
+ });
1431
+ if (!response.ok) throw new Error('Save failed');
1432
+ nameEl.dataset.currentName = newName || '';
1433
+ nameEl.textContent = newName || 'Untitled';
1434
+ nameEl.classList.toggle('unnamed', !newName);
1435
+ nameEl.title = 'Click to rename';
1436
+ } catch (error) {
1437
+ // Revert the display to the previous name on failure
1438
+ cancel();
1439
+ }
1440
+ }
1441
+
1442
+ function cancel() {
1443
+ nameEl.textContent = currentName || 'Untitled';
1444
+ nameEl.classList.toggle('unnamed', !currentName);
1445
+ nameEl.title = 'Click to rename';
1446
+ }
1447
+
1448
+ input.addEventListener('blur', save);
1449
+ input.addEventListener('keydown', function(e) {
1450
+ if (e.key === 'Enter') {
1451
+ e.preventDefault();
1452
+ input.removeEventListener('blur', save);
1453
+ save();
1454
+ } else if (e.key === 'Escape') {
1455
+ e.preventDefault();
1456
+ input.removeEventListener('blur', save);
1457
+ cancel();
1458
+ }
1459
+ });
1460
+ });
1461
+ }
1462
+
1364
1463
  /**
1365
1464
  * Update local header with review info
1366
1465
  */
1367
1466
  updateLocalHeader(reviewData) {
1467
+ // Update review name/title in header
1468
+ const nameEl = document.getElementById('local-review-name');
1469
+ if (nameEl) {
1470
+ const name = reviewData.name || '';
1471
+ nameEl.textContent = name || 'Untitled';
1472
+ nameEl.dataset.currentName = name;
1473
+ nameEl.classList.toggle('unnamed', !name);
1474
+ nameEl.title = 'Click to rename';
1475
+ this.initNameEditing();
1476
+ }
1477
+
1368
1478
  // Update repository name
1369
1479
  const repoName = document.getElementById('local-repo-name');
1370
1480
  if (repoName) {
@@ -748,7 +748,11 @@ class AnalysisHistoryManager {
748
748
  'o1': 'thorough',
749
749
  'o1-mini': 'balanced',
750
750
  // Copilot models
751
- 'gpt-4': 'balanced'
751
+ 'gpt-4': 'balanced',
752
+ // Pi models
753
+ 'default': 'balanced',
754
+ 'multi-model': 'thorough',
755
+ 'review-roulette': 'thorough'
752
756
  };
753
757
 
754
758
  return modelTiers[modelId] || null;
package/public/js/pr.js CHANGED
@@ -3432,37 +3432,74 @@ class PRManager {
3432
3432
  }
3433
3433
 
3434
3434
  try {
3435
- // Check if PR has new commits before analysis
3435
+ // Run stale check and settings fetch in parallel to minimize dialog delay.
3436
+ // Use AbortController so the fetch is truly cancelled on timeout,
3437
+ // freeing the HTTP connection for subsequent requests.
3438
+ const STALE_TIMEOUT = 2000;
3439
+ const staleAbort = new AbortController();
3440
+ const staleTimer = setTimeout(() => {
3441
+ console.debug(`[Analyze] stale-check timed out after ${STALE_TIMEOUT}ms, aborting`);
3442
+ staleAbort.abort();
3443
+ }, STALE_TIMEOUT);
3444
+ const staleCheckPromise = fetch(`/api/pr/${owner}/${repo}/${number}/check-stale`, { signal: staleAbort.signal })
3445
+ .then(r => {
3446
+ clearTimeout(staleTimer);
3447
+ if (!r.ok) {
3448
+ console.warn(`Stale check failed with status ${r.status}`);
3449
+ return { _fetchError: true, status: r.status };
3450
+ }
3451
+ return r.json();
3452
+ })
3453
+ .catch(err => {
3454
+ clearTimeout(staleTimer);
3455
+ if (err.name === 'AbortError') {
3456
+ console.debug('[Analyze] stale-check aborted (timeout)');
3457
+ } else {
3458
+ console.warn('[Analyze] stale-check fetch error:', err);
3459
+ }
3460
+ return null;
3461
+ });
3462
+
3463
+ // Show analysis config modal
3464
+ if (!this.analysisConfigModal) {
3465
+ clearTimeout(staleTimer);
3466
+ console.warn('AnalysisConfigModal not initialized, proceeding without config');
3467
+ await this.startAnalysis(owner, repo, number, btn, {});
3468
+ return;
3469
+ }
3470
+
3471
+ // Fetch stale check, repo settings, and last instructions in parallel
3472
+ const [staleResult, repoSettings, lastInstructions] = await Promise.all([
3473
+ staleCheckPromise,
3474
+ this.fetchRepoSettings().catch(() => null),
3475
+ this.fetchLastCustomInstructions().catch(() => '')
3476
+ ]);
3477
+
3478
+ // Process stale check result
3436
3479
  try {
3437
- const staleResponse = await fetch(`/api/pr/${owner}/${repo}/${number}/check-stale`);
3438
- if (!staleResponse.ok) {
3439
- // Handle non-OK responses (401/403/500 etc)
3440
- const errorText = await staleResponse.text().catch(() => 'Unknown error');
3441
- console.warn(`Stale check failed with status ${staleResponse.status}:`, errorText);
3480
+ if (staleResult === null) {
3481
+ // Timed out or network error — fail open
3482
+ if (window.toast) {
3483
+ window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
3484
+ }
3485
+ } else if (staleResult._fetchError) {
3442
3486
  if (window.toast) {
3443
- window.toast.showWarning(`Could not verify PR is current (${staleResponse.status}). Proceeding with analysis.`);
3487
+ window.toast.showWarning(`Could not verify PR is current (${staleResult.status}). Proceeding with analysis.`);
3444
3488
  }
3445
- // Fall through to continue with analysis
3446
3489
  } else {
3447
- const staleData = await staleResponse.json();
3448
-
3449
3490
  // Handle PR state - show info for closed/merged PRs but still allow analysis
3450
- if (staleData.prState && (staleData.prState !== 'open' || staleData.merged)) {
3451
- const stateLabel = staleData.merged ? 'merged' : 'closed';
3491
+ if (staleResult.prState && (staleResult.prState !== 'open' || staleResult.merged)) {
3492
+ const stateLabel = staleResult.merged ? 'merged' : 'closed';
3452
3493
  if (window.toast) {
3453
3494
  window.toast.showWarning(`This PR is ${stateLabel}. Analysis will proceed on the existing data.`);
3454
3495
  }
3455
3496
  }
3456
3497
 
3457
- // Handle isStale === null (unknown - couldn't check)
3458
- if (staleData.isStale === null) {
3459
- // Couldn't verify - show toast and proceed
3498
+ if (staleResult.isStale === null) {
3460
3499
  if (window.toast) {
3461
3500
  window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
3462
3501
  }
3463
- // Continue with analysis
3464
- } else if (staleData.isStale === true) {
3465
- // PR is stale - show single dialog with 3 options
3502
+ } else if (staleResult.isStale === true) {
3466
3503
  if (!window.confirmDialog) {
3467
3504
  console.warn('ConfirmDialog not available for stale PR check');
3468
3505
  } else {
@@ -3476,40 +3513,22 @@ class PRManager {
3476
3513
  });
3477
3514
 
3478
3515
  if (choice === 'confirm') {
3479
- // User wants to refresh first
3480
3516
  await this.refreshPR();
3481
- // After refresh, continue with analysis
3482
3517
  } else if (choice === 'secondary') {
3483
- // User chose to analyze anyway - continue with stale data
3518
+ // User chose to analyze anyway
3484
3519
  } else {
3485
- // User cancelled
3486
3520
  return;
3487
3521
  }
3488
3522
  }
3489
3523
  }
3490
- // If isStale === false, PR is up-to-date, just continue
3491
3524
  }
3492
3525
  } catch (staleError) {
3493
- // Fail-open: show toast warning and continue with analysis
3494
- console.warn('Error checking PR staleness:', staleError);
3526
+ console.warn('Error processing PR staleness:', staleError);
3495
3527
  if (window.toast) {
3496
3528
  window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
3497
3529
  }
3498
3530
  }
3499
3531
 
3500
- // Show analysis config modal
3501
- if (!this.analysisConfigModal) {
3502
- console.warn('AnalysisConfigModal not initialized, proceeding without config');
3503
- await this.startAnalysis(owner, repo, number, btn, {});
3504
- return;
3505
- }
3506
-
3507
- // Fetch repo settings and last used instructions in parallel
3508
- const [repoSettings, lastInstructions] = await Promise.all([
3509
- this.fetchRepoSettings(),
3510
- this.fetchLastCustomInstructions()
3511
- ]);
3512
-
3513
3532
  // Determine the model and provider to use (priority: remembered > repo default > defaults)
3514
3533
  const modelStorageKey = PRManager.getRepoStorageKey('pair-review-model', owner, repo);
3515
3534
  const providerStorageKey = PRManager.getRepoStorageKey('pair-review-provider', owner, repo);
package/public/local.html CHANGED
@@ -165,6 +165,49 @@
165
165
  unicode-bidi: bidi-override;
166
166
  }
167
167
 
168
+ /* Editable review name in header */
169
+ .local-review-name {
170
+ font-size: 15px;
171
+ font-weight: 500;
172
+ color: var(--color-text-primary);
173
+ cursor: text;
174
+ padding: 2px 6px;
175
+ border-radius: 4px;
176
+ border: 1px dashed var(--color-border-primary);
177
+ transition: all 0.15s ease;
178
+ max-width: 360px;
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ white-space: nowrap;
182
+ position: relative;
183
+ }
184
+
185
+ .local-review-name:hover {
186
+ background: var(--color-bg-secondary);
187
+ border-color: var(--ai-primary);
188
+ border-style: solid;
189
+ }
190
+
191
+ .local-review-name.unnamed {
192
+ color: var(--color-text-tertiary);
193
+ font-style: italic;
194
+ }
195
+
196
+ .local-review-name-input {
197
+ font-family: 'DM Sans', -apple-system, sans-serif;
198
+ font-size: 15px;
199
+ font-weight: 500;
200
+ padding: 2px 6px;
201
+ border: 1px solid #d97706;
202
+ border-radius: 4px;
203
+ background: var(--color-bg-primary);
204
+ color: var(--color-text-primary);
205
+ outline: none;
206
+ box-shadow: 0 0 0 2px rgba(217, 119, 6, 0.15);
207
+ width: 280px;
208
+ max-width: 360px;
209
+ }
210
+
168
211
  </style>
169
212
  </head>
170
213
  <body>
@@ -205,8 +248,8 @@
205
248
  </div>
206
249
  </div>
207
250
  <div class="header-center">
208
- <!-- Title area intentionally left empty for local mode -->
209
- <!-- The "Local Mode" badge and header info provide sufficient context -->
251
+ <!-- Editable review name/title -->
252
+ <span class="local-review-name" id="local-review-name" title="Click to rename">Untitled</span>
210
253
  </div>
211
254
  <div class="header-right">
212
255
  <div class="header-icon-group">
package/src/ai/index.js CHANGED
@@ -41,6 +41,7 @@ require('./codex-provider');
41
41
  require('./copilot-provider');
42
42
  require('./opencode-provider');
43
43
  require('./cursor-agent-provider');
44
+ require('./pi-provider');
44
45
 
45
46
  // Export the unified API
46
47
  module.exports = {