@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.
- package/README.md +67 -38
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/index.html +276 -624
- package/public/js/index.js +1071 -0
- package/public/js/local.js +140 -30
- package/public/js/modules/analysis-history.js +5 -1
- package/public/js/pr.js +57 -38
- package/public/local.html +45 -2
- package/src/ai/index.js +1 -0
- package/src/ai/pi-provider.js +859 -0
- package/src/ai/provider.js +32 -8
- package/src/ai/stream-parser.js +171 -2
- package/src/config.js +1 -1
- package/src/database.js +170 -40
- package/src/local-review.js +9 -0
- package/src/routes/local.js +390 -41
package/public/js/local.js
CHANGED
|
@@ -304,14 +304,62 @@ class LocalManager {
|
|
|
304
304
|
return;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
//
|
|
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
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
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 (${
|
|
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 (
|
|
3451
|
-
const stateLabel =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<!--
|
|
209
|
-
|
|
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">
|