@in-the-loop-labs/pair-review 2.3.3 → 2.4.0
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/.pi/skills/review-model-guidance/SKILL.md +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/README.md +15 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +287 -14
- package/public/index.html +121 -57
- package/public/js/components/AIPanel.js +2 -1
- package/public/js/components/AdvancedConfigTab.js +2 -2
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +187 -28
- package/public/js/components/CouncilProgressModal.js +4 -7
- package/public/js/components/SplitButton.js +66 -1
- package/public/js/components/VoiceCentricConfigTab.js +2 -2
- package/public/js/index.js +274 -21
- package/public/js/pr.js +194 -5
- package/public/local.html +8 -1
- package/public/pr.html +17 -2
- package/src/ai/codex-provider.js +14 -2
- package/src/ai/copilot-provider.js +1 -10
- package/src/ai/cursor-agent-provider.js +1 -10
- package/src/ai/gemini-provider.js +8 -17
- package/src/chat/acp-bridge.js +442 -0
- package/src/chat/api-reference.js +539 -0
- package/src/chat/chat-providers.js +290 -0
- package/src/chat/claude-code-bridge.js +499 -0
- package/src/chat/codex-bridge.js +601 -0
- package/src/chat/pi-bridge.js +56 -3
- package/src/chat/prompt-builder.js +12 -11
- package/src/chat/session-manager.js +110 -29
- package/src/config.js +4 -2
- package/src/database.js +50 -2
- package/src/github/client.js +43 -0
- package/src/routes/chat.js +60 -27
- package/src/routes/config.js +24 -1
- package/src/routes/github-collections.js +126 -0
- package/src/routes/mcp.js +2 -1
- package/src/routes/pr.js +166 -2
- package/src/routes/reviews.js +2 -1
- package/src/routes/shared.js +70 -49
- package/src/server.js +27 -1
- package/src/utils/safe-parse-json.js +19 -0
- package/.pi/skills/pair-review-api/SKILL.md +0 -448
package/public/js/index.js
CHANGED
|
@@ -279,6 +279,204 @@
|
|
|
279
279
|
loaded: false
|
|
280
280
|
};
|
|
281
281
|
|
|
282
|
+
// ─── GitHub PR Collections (My Review Requests / My PRs) ───────────────────
|
|
283
|
+
|
|
284
|
+
var reviewRequestsState = {
|
|
285
|
+
loaded: false,
|
|
286
|
+
prs: [],
|
|
287
|
+
fetchedAt: null
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
var myPrsState = {
|
|
291
|
+
loaded: false,
|
|
292
|
+
prs: [],
|
|
293
|
+
fetchedAt: null
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Render a single row for a collection PR table.
|
|
298
|
+
* @param {Object} pr - PR object from the API
|
|
299
|
+
* @param {string} collection - The collection name ('review-requests' or 'my-prs')
|
|
300
|
+
* @returns {string} HTML string for the table row
|
|
301
|
+
*/
|
|
302
|
+
function renderCollectionPrRow(pr, collection) {
|
|
303
|
+
var repoFull = pr.owner + '/' + pr.repo;
|
|
304
|
+
var prUrl = pr.html_url || ('https://github.com/' + repoFull + '/pull/' + pr.number);
|
|
305
|
+
var relativeTime = formatRelativeTime(pr.updated_at);
|
|
306
|
+
|
|
307
|
+
var authorDisplay = pr.author
|
|
308
|
+
? '<a href="https://github.com/' + encodeURIComponent(pr.author) + '" target="_blank" rel="noopener">' + escapeHtml(pr.author) + '</a>'
|
|
309
|
+
: '';
|
|
310
|
+
|
|
311
|
+
var githubLinkHtml =
|
|
312
|
+
'<a href="' + escapeHtml(pr.html_url || prUrl) + '" target="_blank" rel="noopener" class="btn-github-link" title="Open on GitHub">' +
|
|
313
|
+
'<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/></svg>' +
|
|
314
|
+
'</a>';
|
|
315
|
+
|
|
316
|
+
var authorTd = collection === 'my-prs'
|
|
317
|
+
? ''
|
|
318
|
+
: '<td class="col-author">' + authorDisplay + '</td>';
|
|
319
|
+
|
|
320
|
+
return '' +
|
|
321
|
+
'<tr class="collection-pr-row" data-pr-url="' + escapeHtml(prUrl) + '">' +
|
|
322
|
+
'<td class="col-repo">' + escapeHtml(repoFull) + '</td>' +
|
|
323
|
+
'<td class="col-pr"><span class="collection-pr-number">#' + pr.number + '</span></td>' +
|
|
324
|
+
'<td class="col-title" title="' + escapeHtml(pr.title || '') + '">' + escapeHtml(pr.title || '') + '</td>' +
|
|
325
|
+
authorTd +
|
|
326
|
+
'<td class="col-time">' + relativeTime + '</td>' +
|
|
327
|
+
'<td class="col-actions">' + githubLinkHtml + '</td>' +
|
|
328
|
+
'</tr>';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Render the collection table into a container element.
|
|
333
|
+
* @param {HTMLElement} container - The container element
|
|
334
|
+
* @param {Object} state - The collection state object
|
|
335
|
+
* @param {string} collection - The collection name ('review-requests' or 'my-prs')
|
|
336
|
+
*/
|
|
337
|
+
function renderCollectionTable(container, state, collection) {
|
|
338
|
+
var fetchedAtId = collection === 'review-requests' ? 'review-requests-fetched-at' : 'my-prs-fetched-at';
|
|
339
|
+
var fetchedAtEl = document.getElementById(fetchedAtId);
|
|
340
|
+
if (fetchedAtEl) {
|
|
341
|
+
var lsKey = 'github-collection-fetched-at:' + collection;
|
|
342
|
+
var displayTs = localStorage.getItem(lsKey) || state.fetchedAt;
|
|
343
|
+
fetchedAtEl.textContent = displayTs
|
|
344
|
+
? 'Updated ' + formatRelativeTime(displayTs)
|
|
345
|
+
: '';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (state.prs.length === 0) {
|
|
349
|
+
var emptyMsg = collection === 'review-requests'
|
|
350
|
+
? 'No pull requests awaiting your review.'
|
|
351
|
+
: 'You have no open pull requests.';
|
|
352
|
+
|
|
353
|
+
if (!state.fetchedAt) {
|
|
354
|
+
emptyMsg = 'Click refresh to fetch from GitHub.';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
container.innerHTML =
|
|
358
|
+
'<div class="recent-reviews-empty">' +
|
|
359
|
+
'<p>' + emptyMsg + '</p>' +
|
|
360
|
+
'</div>';
|
|
361
|
+
container.classList.remove('recent-reviews-loading');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
var authorTh = collection === 'my-prs' ? '' : '<th>Author</th>';
|
|
366
|
+
|
|
367
|
+
container.innerHTML =
|
|
368
|
+
'<table class="recent-reviews-table">' +
|
|
369
|
+
'<thead>' +
|
|
370
|
+
'<tr>' +
|
|
371
|
+
'<th>Repository</th>' +
|
|
372
|
+
'<th>PR</th>' +
|
|
373
|
+
'<th>Title</th>' +
|
|
374
|
+
authorTh +
|
|
375
|
+
'<th>Updated</th>' +
|
|
376
|
+
'<th>Actions</th>' +
|
|
377
|
+
'</tr>' +
|
|
378
|
+
'</thead>' +
|
|
379
|
+
'<tbody>' +
|
|
380
|
+
state.prs.map(function (pr) { return renderCollectionPrRow(pr, collection); }).join('') +
|
|
381
|
+
'</tbody>' +
|
|
382
|
+
'</table>';
|
|
383
|
+
container.classList.remove('recent-reviews-loading');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Load collection PRs from cached backend data.
|
|
388
|
+
* @param {string} collection - 'review-requests' or 'my-prs'
|
|
389
|
+
* @param {string} containerId - DOM id of the container element
|
|
390
|
+
* @param {Object} state - The collection state object
|
|
391
|
+
*/
|
|
392
|
+
async function loadCollectionPrs(collection, containerId, state) {
|
|
393
|
+
var container = document.getElementById(containerId);
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
var response = await fetch('/api/github/' + collection);
|
|
397
|
+
if (!response.ok) throw new Error('Failed to fetch');
|
|
398
|
+
var data = await response.json();
|
|
399
|
+
|
|
400
|
+
state.loaded = true;
|
|
401
|
+
state.prs = data.prs || [];
|
|
402
|
+
state.fetchedAt = data.fetched_at;
|
|
403
|
+
|
|
404
|
+
renderCollectionTable(container, state, collection);
|
|
405
|
+
|
|
406
|
+
// Auto-refresh on first load if cache is empty
|
|
407
|
+
if (state.prs.length === 0 && !state.fetchedAt) {
|
|
408
|
+
refreshCollectionPrs(collection, containerId, state);
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error('Error loading ' + collection + ':', error);
|
|
412
|
+
container.innerHTML =
|
|
413
|
+
'<div class="recent-reviews-empty">' +
|
|
414
|
+
'<p>Failed to load. Click refresh to try again.</p>' +
|
|
415
|
+
'</div>';
|
|
416
|
+
container.classList.remove('recent-reviews-loading');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Refresh collection PRs by fetching fresh data from GitHub.
|
|
422
|
+
* @param {string} collection - 'review-requests' or 'my-prs'
|
|
423
|
+
* @param {string} containerId - DOM id of the container element
|
|
424
|
+
* @param {Object} state - The collection state object
|
|
425
|
+
*/
|
|
426
|
+
async function refreshCollectionPrs(collection, containerId, state) {
|
|
427
|
+
var container = document.getElementById(containerId);
|
|
428
|
+
var btnId = collection === 'review-requests' ? 'refresh-review-requests' : 'refresh-my-prs';
|
|
429
|
+
var btn = document.getElementById(btnId);
|
|
430
|
+
|
|
431
|
+
if (btn) btn.classList.add('refreshing');
|
|
432
|
+
|
|
433
|
+
// Show loading state only if this is the first load (no existing data)
|
|
434
|
+
if (state.prs.length === 0) {
|
|
435
|
+
container.innerHTML = '<div class="recent-reviews-loading">Fetching from GitHub...</div>';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
var response = await fetch('/api/github/' + collection + '/refresh', { method: 'POST' });
|
|
440
|
+
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
var errData = await response.json().catch(function() { return {}; });
|
|
443
|
+
if (response.status === 401) {
|
|
444
|
+
container.innerHTML =
|
|
445
|
+
'<div class="recent-reviews-empty">' +
|
|
446
|
+
'<p>Configure a GitHub token to see ' +
|
|
447
|
+
(collection === 'review-requests' ? 'review requests' : 'your pull requests') +
|
|
448
|
+
'.</p>' +
|
|
449
|
+
'</div>';
|
|
450
|
+
container.classList.remove('recent-reviews-loading');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
throw new Error(errData.error || 'Refresh failed');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
var data = await response.json();
|
|
457
|
+
state.prs = data.prs || [];
|
|
458
|
+
state.fetchedAt = data.fetched_at;
|
|
459
|
+
state.loaded = true;
|
|
460
|
+
localStorage.setItem('github-collection-fetched-at:' + collection, new Date().toISOString());
|
|
461
|
+
|
|
462
|
+
renderCollectionTable(container, state, collection);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error('Error refreshing ' + collection + ':', error);
|
|
465
|
+
// If we had existing data, keep showing it
|
|
466
|
+
if (state.prs.length > 0) {
|
|
467
|
+
renderCollectionTable(container, state, collection);
|
|
468
|
+
} else {
|
|
469
|
+
container.innerHTML =
|
|
470
|
+
'<div class="recent-reviews-empty">' +
|
|
471
|
+
'<p>Failed to fetch from GitHub. Check your token and try again.</p>' +
|
|
472
|
+
'</div>';
|
|
473
|
+
container.classList.remove('recent-reviews-loading');
|
|
474
|
+
}
|
|
475
|
+
} finally {
|
|
476
|
+
if (btn) btn.classList.remove('refreshing');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
282
480
|
/**
|
|
283
481
|
* Fetch and display local review sessions (initial load).
|
|
284
482
|
*/
|
|
@@ -554,6 +752,9 @@
|
|
|
554
752
|
'<td class="col-author">' + authorDisplay + '</td>' +
|
|
555
753
|
'<td class="col-time">' + relativeTime + '</td>' +
|
|
556
754
|
'<td class="col-actions">' +
|
|
755
|
+
'<a href="https://github.com/' + escapeHtml(worktree.repository) + '/pull/' + worktree.pr_number + '" target="_blank" rel="noopener" class="btn-github-link" title="Open on GitHub">' +
|
|
756
|
+
'<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/></svg>' +
|
|
757
|
+
'</a>' +
|
|
557
758
|
'<a href="' + settingsLink + '" class="btn-repo-settings" title="Repository settings">' +
|
|
558
759
|
'<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">' +
|
|
559
760
|
'<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"/>' +
|
|
@@ -649,8 +850,6 @@
|
|
|
649
850
|
async function loadRecentReviews() {
|
|
650
851
|
const container = document.getElementById('recent-reviews-container');
|
|
651
852
|
const section = document.getElementById('recent-reviews-section');
|
|
652
|
-
const usageInfo = document.getElementById('usage-info');
|
|
653
|
-
|
|
654
853
|
// Reset pagination state
|
|
655
854
|
recentReviewsPagination.lastTimestamp = null;
|
|
656
855
|
recentReviewsPagination.hasMore = false;
|
|
@@ -665,14 +864,14 @@
|
|
|
665
864
|
const data = await response.json();
|
|
666
865
|
|
|
667
866
|
if (!data.success || !data.worktrees || data.worktrees.length === 0) {
|
|
668
|
-
// Show friendly empty state
|
|
867
|
+
// Show friendly empty state
|
|
669
868
|
container.innerHTML =
|
|
670
869
|
'<div class="recent-reviews-empty">' +
|
|
671
870
|
'<p>No PR reviews yet. Paste a PR URL above to get started.</p>' +
|
|
672
871
|
'</div>';
|
|
673
872
|
container.classList.remove('recent-reviews-loading');
|
|
674
|
-
// Show
|
|
675
|
-
|
|
873
|
+
// Show help modal when no reviews exist
|
|
874
|
+
openHelpModal();
|
|
676
875
|
return;
|
|
677
876
|
}
|
|
678
877
|
|
|
@@ -703,9 +902,11 @@
|
|
|
703
902
|
|
|
704
903
|
} catch (error) {
|
|
705
904
|
console.error('Error loading recent reviews:', error);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
905
|
+
container.innerHTML =
|
|
906
|
+
'<div class="recent-reviews-empty">' +
|
|
907
|
+
'<p>Failed to load recent reviews. Please try refreshing the page.</p>' +
|
|
908
|
+
'</div>';
|
|
909
|
+
container.classList.remove('recent-reviews-loading');
|
|
709
910
|
}
|
|
710
911
|
}
|
|
711
912
|
|
|
@@ -896,10 +1097,17 @@
|
|
|
896
1097
|
const config = await response.json();
|
|
897
1098
|
updateCommandExamples(config.is_running_via_npx);
|
|
898
1099
|
|
|
899
|
-
//
|
|
1100
|
+
// Expose chat provider config to components (ChatPanel reads these)
|
|
1101
|
+
window.__pairReview = window.__pairReview || {};
|
|
1102
|
+
window.__pairReview.chatProvider = config.chat_provider || 'pi';
|
|
1103
|
+
const chatProviders = config.chat_providers || [];
|
|
1104
|
+
window.__pairReview.chatProviders = chatProviders;
|
|
1105
|
+
|
|
1106
|
+
// Set chat feature state based on config and provider availability
|
|
900
1107
|
let chatState = 'disabled';
|
|
901
1108
|
if (config.enable_chat) {
|
|
902
|
-
|
|
1109
|
+
const anyAvailable = chatProviders.some(p => p.available);
|
|
1110
|
+
chatState = anyAvailable ? 'available' : 'unavailable';
|
|
903
1111
|
}
|
|
904
1112
|
document.documentElement.setAttribute('data-chat', chatState);
|
|
905
1113
|
window.dispatchEvent(new CustomEvent('chat-state-changed', { detail: { state: chatState } }));
|
|
@@ -936,6 +1144,46 @@
|
|
|
936
1144
|
return;
|
|
937
1145
|
}
|
|
938
1146
|
|
|
1147
|
+
// Refresh buttons for GitHub collections
|
|
1148
|
+
var refreshReviewRequestsBtn = event.target.closest('#refresh-review-requests');
|
|
1149
|
+
if (refreshReviewRequestsBtn) {
|
|
1150
|
+
event.preventDefault();
|
|
1151
|
+
refreshCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
var refreshMyPrsBtn = event.target.closest('#refresh-my-prs');
|
|
1156
|
+
if (refreshMyPrsBtn) {
|
|
1157
|
+
event.preventDefault();
|
|
1158
|
+
refreshCollectionPrs('my-prs', 'my-prs-container', myPrsState);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Click on a collection PR row to start review
|
|
1163
|
+
var collectionRow = event.target.closest('.collection-pr-row');
|
|
1164
|
+
if (collectionRow && !event.target.closest('a')) {
|
|
1165
|
+
var prUrl = collectionRow.dataset.prUrl;
|
|
1166
|
+
if (prUrl) {
|
|
1167
|
+
// Switch to PR tab to show loading state
|
|
1168
|
+
var tabBar = document.getElementById('unified-tab-bar');
|
|
1169
|
+
var prTabBtn = tabBar.querySelector('[data-tab="pr-tab"]');
|
|
1170
|
+
switchTab(tabBar, prTabBtn);
|
|
1171
|
+
localStorage.setItem(TAB_STORAGE_KEY, 'pr-tab');
|
|
1172
|
+
|
|
1173
|
+
// Populate input and submit the form programmatically
|
|
1174
|
+
var input = document.getElementById('pr-url-input');
|
|
1175
|
+
if (input) {
|
|
1176
|
+
input.value = prUrl;
|
|
1177
|
+
// Trigger the form submit
|
|
1178
|
+
var form = document.getElementById('start-review-form');
|
|
1179
|
+
if (form) {
|
|
1180
|
+
form.dispatchEvent(new Event('submit', { cancelable: true }));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
939
1187
|
// Show more (PR reviews)
|
|
940
1188
|
const showMoreBtn = event.target.closest('#btn-show-more');
|
|
941
1189
|
if (showMoreBtn) {
|
|
@@ -963,6 +1211,13 @@
|
|
|
963
1211
|
if (tabId === 'local-tab' && !localReviewsPagination.loaded) {
|
|
964
1212
|
loadLocalReviews();
|
|
965
1213
|
}
|
|
1214
|
+
// Lazy-load GitHub collection tabs on first switch
|
|
1215
|
+
if (tabId === 'review-requests-tab' && !reviewRequestsState.loaded) {
|
|
1216
|
+
loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
|
|
1217
|
+
}
|
|
1218
|
+
if (tabId === 'my-prs-tab' && !myPrsState.loaded) {
|
|
1219
|
+
loadCollectionPrs('my-prs', 'my-prs-container', myPrsState);
|
|
1220
|
+
}
|
|
966
1221
|
});
|
|
967
1222
|
return;
|
|
968
1223
|
}
|
|
@@ -972,17 +1227,7 @@
|
|
|
972
1227
|
|
|
973
1228
|
document.addEventListener('DOMContentLoaded', function () {
|
|
974
1229
|
// Load config and update command examples based on npx detection
|
|
975
|
-
loadConfigAndUpdateUI()
|
|
976
|
-
// Sync help content to usage-info section AFTER command examples are updated
|
|
977
|
-
const helpContent = document.querySelector('.help-modal-content');
|
|
978
|
-
const usageInfo = document.getElementById('usage-info');
|
|
979
|
-
if (helpContent && usageInfo) {
|
|
980
|
-
usageInfo.innerHTML = '';
|
|
981
|
-
Array.from(helpContent.childNodes).forEach(function (node) {
|
|
982
|
-
usageInfo.appendChild(node.cloneNode(true));
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
});
|
|
1230
|
+
loadConfigAndUpdateUI();
|
|
986
1231
|
|
|
987
1232
|
// Restore saved tab from localStorage (default: 'pr-tab')
|
|
988
1233
|
const savedTab = localStorage.getItem(TAB_STORAGE_KEY) || 'pr-tab';
|
|
@@ -1003,6 +1248,14 @@
|
|
|
1003
1248
|
loadLocalReviews();
|
|
1004
1249
|
}
|
|
1005
1250
|
|
|
1251
|
+
// If a GitHub collection tab is active, load it immediately
|
|
1252
|
+
if (savedTab === 'review-requests-tab') {
|
|
1253
|
+
loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
|
|
1254
|
+
}
|
|
1255
|
+
if (savedTab === 'my-prs-tab') {
|
|
1256
|
+
loadCollectionPrs('my-prs', 'my-prs-container', myPrsState);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1006
1259
|
// Set up start review form handler
|
|
1007
1260
|
const form = document.getElementById('start-review-form');
|
|
1008
1261
|
if (form) {
|
package/public/js/pr.js
CHANGED
|
@@ -260,6 +260,9 @@ class PRManager {
|
|
|
260
260
|
refreshBtn.addEventListener('click', () => this.refreshPR());
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// PR description popover
|
|
264
|
+
this.setupPRDescriptionPopover();
|
|
265
|
+
|
|
263
266
|
// Setup comment form keyboard shortcut delegation
|
|
264
267
|
this.setupCommentFormDelegation();
|
|
265
268
|
|
|
@@ -494,6 +497,19 @@ class PRManager {
|
|
|
494
497
|
}
|
|
495
498
|
}
|
|
496
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Reload AI suggestions and user comments after an analysis completes.
|
|
502
|
+
* Shared by the foreground `analysis_completed` handler and the deferred
|
|
503
|
+
* `_dirtyAnalysis` branch in the visibilitychange listener.
|
|
504
|
+
*/
|
|
505
|
+
_reloadAfterAnalysis() {
|
|
506
|
+
const includeDismissed = window.aiPanel?.showDismissedComments ?? false;
|
|
507
|
+
return Promise.all([
|
|
508
|
+
this.loadAISuggestions(),
|
|
509
|
+
this.loadUserComments(includeDismissed)
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
|
|
497
513
|
/**
|
|
498
514
|
* Listen for review-scoped CustomEvents dispatched by ChatPanel's
|
|
499
515
|
* WebSocket pub/sub connection.
|
|
@@ -555,9 +571,9 @@ class PRManager {
|
|
|
555
571
|
debounced('analysis', () => {
|
|
556
572
|
if (this.analysisHistoryManager) {
|
|
557
573
|
this.analysisHistoryManager.refresh({ switchToNew: true })
|
|
558
|
-
.then(() => this.
|
|
574
|
+
.then(() => this._reloadAfterAnalysis());
|
|
559
575
|
} else {
|
|
560
|
-
this.
|
|
576
|
+
this._reloadAfterAnalysis();
|
|
561
577
|
}
|
|
562
578
|
});
|
|
563
579
|
});
|
|
@@ -590,9 +606,9 @@ class PRManager {
|
|
|
590
606
|
this._dirtySuggestions = false; // analysis refresh includes suggestion reload
|
|
591
607
|
if (this.analysisHistoryManager) {
|
|
592
608
|
this.analysisHistoryManager.refresh({ switchToNew: true })
|
|
593
|
-
.then(() => this.
|
|
609
|
+
.then(() => this._reloadAfterAnalysis());
|
|
594
610
|
} else {
|
|
595
|
-
this.
|
|
611
|
+
this._reloadAfterAnalysis();
|
|
596
612
|
}
|
|
597
613
|
} else if (this._dirtySuggestions) {
|
|
598
614
|
this._dirtySuggestions = false;
|
|
@@ -774,6 +790,18 @@ class PRManager {
|
|
|
774
790
|
titleElement.textContent = pr.title;
|
|
775
791
|
}
|
|
776
792
|
|
|
793
|
+
// Show/hide PR description info button
|
|
794
|
+
const descToggle = document.getElementById('pr-description-toggle');
|
|
795
|
+
if (descToggle) {
|
|
796
|
+
if (pr.body) {
|
|
797
|
+
descToggle.style.display = '';
|
|
798
|
+
this._prBody = pr.body;
|
|
799
|
+
} else {
|
|
800
|
+
descToggle.style.display = 'none';
|
|
801
|
+
this._prBody = null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
777
805
|
// Update meta info - show only head branch, full info in tooltip
|
|
778
806
|
const branchName = document.getElementById('pr-branch-name');
|
|
779
807
|
const branchContainer = document.getElementById('pr-branch');
|
|
@@ -875,6 +903,81 @@ class PRManager {
|
|
|
875
903
|
this.updatePendingDraftIndicator(pr.pendingDraft);
|
|
876
904
|
}
|
|
877
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Set up the PR description popover toggle (called once during init).
|
|
908
|
+
*/
|
|
909
|
+
setupPRDescriptionPopover() {
|
|
910
|
+
const toggle = document.getElementById('pr-description-toggle');
|
|
911
|
+
if (!toggle) return;
|
|
912
|
+
|
|
913
|
+
const wrapper = toggle.closest('.pr-title-wrapper');
|
|
914
|
+
if (!wrapper) return;
|
|
915
|
+
|
|
916
|
+
const closePopover = () => {
|
|
917
|
+
const existing = wrapper.querySelector('.pr-description-popover');
|
|
918
|
+
if (existing) existing.remove();
|
|
919
|
+
toggle.classList.remove('active');
|
|
920
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
toggle.addEventListener('click', (e) => {
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
const existing = wrapper.querySelector('.pr-description-popover');
|
|
926
|
+
if (existing) {
|
|
927
|
+
closePopover();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const body = this._prBody || '';
|
|
932
|
+
const rendered = window.renderMarkdown ? window.renderMarkdown(body) : this.escapeHtml(body);
|
|
933
|
+
|
|
934
|
+
const popover = document.createElement('div');
|
|
935
|
+
popover.className = 'pr-description-popover';
|
|
936
|
+
|
|
937
|
+
const arrow = document.createElement('div');
|
|
938
|
+
arrow.className = 'pr-description-popover-arrow';
|
|
939
|
+
|
|
940
|
+
const header = document.createElement('div');
|
|
941
|
+
header.className = 'pr-description-popover-header';
|
|
942
|
+
|
|
943
|
+
const title = document.createElement('span');
|
|
944
|
+
title.className = 'pr-description-popover-title';
|
|
945
|
+
title.textContent = 'PR Description';
|
|
946
|
+
|
|
947
|
+
const closeBtn = document.createElement('button');
|
|
948
|
+
closeBtn.className = 'pr-description-popover-close';
|
|
949
|
+
closeBtn.title = 'Close';
|
|
950
|
+
closeBtn.innerHTML = '×';
|
|
951
|
+
|
|
952
|
+
header.append(title, closeBtn);
|
|
953
|
+
|
|
954
|
+
const content = document.createElement('div');
|
|
955
|
+
content.className = 'pr-description-popover-content';
|
|
956
|
+
content.innerHTML = rendered;
|
|
957
|
+
|
|
958
|
+
popover.append(arrow, header, content);
|
|
959
|
+
|
|
960
|
+
wrapper.appendChild(popover);
|
|
961
|
+
toggle.classList.add('active');
|
|
962
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
963
|
+
|
|
964
|
+
closeBtn.addEventListener('click', (ev) => {
|
|
965
|
+
ev.stopPropagation();
|
|
966
|
+
closePopover();
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
popover.addEventListener('click', (ev) => ev.stopPropagation());
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
document.addEventListener('click', () => {
|
|
973
|
+
if (toggle.classList.contains('active')) closePopover();
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
document.addEventListener('keydown', (e) => {
|
|
977
|
+
if (e.key === 'Escape' && toggle.classList.contains('active')) closePopover();
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
878
981
|
/**
|
|
879
982
|
* Update the pending draft indicator in the toolbar
|
|
880
983
|
* @param {Object|null} pendingDraft - Pending draft data or null if no draft
|
|
@@ -2940,14 +3043,37 @@ class PRManager {
|
|
|
2940
3043
|
// Clear placeholder in case of any orphaned elements
|
|
2941
3044
|
placeholder.innerHTML = '';
|
|
2942
3045
|
|
|
3046
|
+
const shareConfig = window.__pairReview?.share;
|
|
3047
|
+
let validatedShareUrl = null;
|
|
3048
|
+
if (shareConfig?.url) {
|
|
3049
|
+
try {
|
|
3050
|
+
new URL(shareConfig.url);
|
|
3051
|
+
validatedShareUrl = shareConfig.url;
|
|
3052
|
+
} catch {
|
|
3053
|
+
// Invalid share URL in config — don't render share button
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
2943
3056
|
this.splitButton = new window.SplitButton({
|
|
2944
3057
|
onSubmit: () => this.openReviewModal(),
|
|
2945
3058
|
onPreview: () => this.openPreviewModal(),
|
|
2946
|
-
onClear: () => this.clearAllUserComments()
|
|
3059
|
+
onClear: () => this.clearAllUserComments(),
|
|
3060
|
+
onShare: () => this.openSharePage(),
|
|
3061
|
+
shareUrl: validatedShareUrl,
|
|
3062
|
+
shareIcon: shareConfig?.icon || null,
|
|
3063
|
+
shareLabel: shareConfig?.label || 'Share',
|
|
3064
|
+
shareDescription: shareConfig?.description || null
|
|
2947
3065
|
});
|
|
2948
3066
|
const buttonElement = this.splitButton.render();
|
|
2949
3067
|
placeholder.appendChild(buttonElement);
|
|
2950
3068
|
this.updateCommentCount();
|
|
3069
|
+
|
|
3070
|
+
// Handle late config arrival — update share config when config fetch resolves
|
|
3071
|
+
window.addEventListener('chat-state-changed', () => {
|
|
3072
|
+
const lateCfg = window.__pairReview?.share;
|
|
3073
|
+
if (this.splitButton) {
|
|
3074
|
+
this.splitButton.setShareConfig(lateCfg || null);
|
|
3075
|
+
}
|
|
3076
|
+
}, { once: true });
|
|
2951
3077
|
}
|
|
2952
3078
|
}
|
|
2953
3079
|
}
|
|
@@ -2972,6 +3098,69 @@ class PRManager {
|
|
|
2972
3098
|
this.previewModal.show();
|
|
2973
3099
|
}
|
|
2974
3100
|
|
|
3101
|
+
/**
|
|
3102
|
+
* Open share page in a new tab
|
|
3103
|
+
* Builds the share URL with a callback_url pointing to this PR's share endpoint
|
|
3104
|
+
* Validates that there is analysis data to share before opening.
|
|
3105
|
+
*/
|
|
3106
|
+
async openSharePage() {
|
|
3107
|
+
const shareConfig = window.__pairReview?.share;
|
|
3108
|
+
if (!shareConfig?.url) return;
|
|
3109
|
+
|
|
3110
|
+
const pr = this.currentPR;
|
|
3111
|
+
if (!pr) return;
|
|
3112
|
+
|
|
3113
|
+
// Validate the share URL before attempting to use it
|
|
3114
|
+
let shareUrl;
|
|
3115
|
+
try {
|
|
3116
|
+
shareUrl = new URL(shareConfig.url);
|
|
3117
|
+
} catch {
|
|
3118
|
+
console.error('Invalid share URL in configuration:', shareConfig.url);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
// Build the callback URL for the share endpoint
|
|
3123
|
+
let callbackUrl = `${window.location.origin}/api/pr/${encodeURIComponent(pr.owner)}/${encodeURIComponent(pr.repo)}/${pr.number}/share`;
|
|
3124
|
+
|
|
3125
|
+
// Include selected run ID if one is explicitly selected
|
|
3126
|
+
if (this.selectedRunId) {
|
|
3127
|
+
callbackUrl += `?runId=${encodeURIComponent(this.selectedRunId)}`;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// Check that there is analysis data to share before opening the page
|
|
3131
|
+
try {
|
|
3132
|
+
const response = await fetch(callbackUrl);
|
|
3133
|
+
if (!response.ok) {
|
|
3134
|
+
if (window.toast) {
|
|
3135
|
+
window.toast.showError('Unable to share: could not load review data');
|
|
3136
|
+
}
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
const data = await response.json().catch(() => null);
|
|
3140
|
+
if (!data) {
|
|
3141
|
+
if (window.toast) {
|
|
3142
|
+
window.toast.showError('Unable to share: unexpected response from server');
|
|
3143
|
+
}
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
if (!data.run) {
|
|
3147
|
+
if (window.toast) {
|
|
3148
|
+
window.toast.showError('Nothing to share: no completed analysis found. Run an AI analysis first.');
|
|
3149
|
+
}
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
console.error('Error checking share data:', error);
|
|
3154
|
+
if (window.toast) {
|
|
3155
|
+
window.toast.showError('Unable to share: ' + error.message);
|
|
3156
|
+
}
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
shareUrl.searchParams.set('callback_url', callbackUrl);
|
|
3161
|
+
window.open(shareUrl.toString(), '_blank');
|
|
3162
|
+
}
|
|
3163
|
+
|
|
2975
3164
|
/**
|
|
2976
3165
|
* Update comment count display
|
|
2977
3166
|
* Note: Dismissed comments are never in the diff DOM (design decision), so we simply count all visible elements.
|
package/public/local.html
CHANGED
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
// Fetch chat availability early so PanelGroup sees the correct state
|
|
11
11
|
fetch('/api/config').then(r => r.ok ? r.json() : null).then(config => {
|
|
12
12
|
if (!config) return;
|
|
13
|
+
const chatProviders = config.chat_providers || [];
|
|
13
14
|
let state = 'disabled';
|
|
14
|
-
if (config.enable_chat)
|
|
15
|
+
if (config.enable_chat) {
|
|
16
|
+
const anyAvailable = chatProviders.some(p => p.available);
|
|
17
|
+
state = anyAvailable ? 'available' : 'unavailable';
|
|
18
|
+
}
|
|
19
|
+
window.__pairReview = window.__pairReview || {};
|
|
20
|
+
window.__pairReview.chatProvider = config.chat_provider || 'pi';
|
|
21
|
+
window.__pairReview.chatProviders = chatProviders;
|
|
15
22
|
document.documentElement.setAttribute('data-chat', state);
|
|
16
23
|
const shortcutsState = config.chat_enable_shortcuts === false ? 'disabled' : 'enabled';
|
|
17
24
|
document.documentElement.setAttribute('data-chat-shortcuts', shortcutsState);
|