@in-the-loop-labs/pair-review 2.3.1 → 2.3.2
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/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/components/AIPanel.js +51 -0
- package/public/js/components/PanelGroup.js +38 -13
- package/public/js/local.js +3 -1
- package/public/js/pr.js +23 -2
- package/public/local.html +1 -1
- package/public/pr.html +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -207,6 +207,45 @@ class AIPanel {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Get the localStorage key for the collapsed state (per-review)
|
|
212
|
+
* @returns {string|null} Storage key or null if no PR context
|
|
213
|
+
*/
|
|
214
|
+
_getCollapsedStorageKey() {
|
|
215
|
+
if (!this.currentPRKey) return null;
|
|
216
|
+
return `pair-review-panel-collapsed_${this.currentPRKey}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Save the collapsed state to localStorage (per-review)
|
|
221
|
+
*/
|
|
222
|
+
_saveCollapsedState() {
|
|
223
|
+
const key = this._getCollapsedStorageKey();
|
|
224
|
+
if (key) {
|
|
225
|
+
localStorage.setItem(key, this.isCollapsed ? 'true' : 'false');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Restore the collapsed state from localStorage, or collapse for new reviews.
|
|
231
|
+
* If the user had the panel expanded for this review, expand it.
|
|
232
|
+
* Otherwise (new review or previously collapsed), collapse it.
|
|
233
|
+
*/
|
|
234
|
+
_restoreOrCollapsePanel() {
|
|
235
|
+
const key = this._getCollapsedStorageKey();
|
|
236
|
+
if (key) {
|
|
237
|
+
const stored = localStorage.getItem(key);
|
|
238
|
+
if (stored === 'false') {
|
|
239
|
+
this.expand();
|
|
240
|
+
} else {
|
|
241
|
+
// 'true' or no saved state (new review) → collapse
|
|
242
|
+
this.collapse();
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
this.collapse();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
210
249
|
/**
|
|
211
250
|
* Get the localStorage key for the filter state (per-review)
|
|
212
251
|
* @returns {string|null} Storage key or null if no PR context
|
|
@@ -275,6 +314,7 @@ class AIPanel {
|
|
|
275
314
|
// Set CSS variable to 0 so width calculations don't reserve space
|
|
276
315
|
document.documentElement.style.setProperty('--ai-panel-width', '0px');
|
|
277
316
|
window.panelGroup?._onReviewVisibilityChanged(false);
|
|
317
|
+
this._saveCollapsedState();
|
|
278
318
|
}
|
|
279
319
|
|
|
280
320
|
expand() {
|
|
@@ -285,6 +325,7 @@ class AIPanel {
|
|
|
285
325
|
// Restore CSS variable from saved width or default
|
|
286
326
|
document.documentElement.style.setProperty('--ai-panel-width', `${this.getEffectivePanelWidth()}px`);
|
|
287
327
|
window.panelGroup?._onReviewVisibilityChanged(true);
|
|
328
|
+
this._saveCollapsedState();
|
|
288
329
|
}
|
|
289
330
|
|
|
290
331
|
/**
|
|
@@ -296,6 +337,7 @@ class AIPanel {
|
|
|
296
337
|
*/
|
|
297
338
|
setPR(owner, repo, number) {
|
|
298
339
|
this.currentPRKey = `${owner}/${repo}#${number}`;
|
|
340
|
+
this._restoreOrCollapsePanel();
|
|
299
341
|
this.restoreSegmentSelection();
|
|
300
342
|
this.restoreFilterState();
|
|
301
343
|
}
|
|
@@ -327,6 +369,10 @@ class AIPanel {
|
|
|
327
369
|
*/
|
|
328
370
|
setAnalysisState(state) {
|
|
329
371
|
this.analysisState = state;
|
|
372
|
+
// Auto-expand panel when analysis starts
|
|
373
|
+
if (state === 'loading' && this.isCollapsed) {
|
|
374
|
+
this.expand();
|
|
375
|
+
}
|
|
330
376
|
// Re-render if currently showing empty state
|
|
331
377
|
if (this.findings.length === 0 && this.selectedSegment === 'ai') {
|
|
332
378
|
this.renderFindings();
|
|
@@ -1737,3 +1783,8 @@ class AIPanel {
|
|
|
1737
1783
|
document.addEventListener('DOMContentLoaded', () => {
|
|
1738
1784
|
window.aiPanel = new AIPanel();
|
|
1739
1785
|
});
|
|
1786
|
+
|
|
1787
|
+
// Export for testing
|
|
1788
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1789
|
+
module.exports = { AIPanel };
|
|
1790
|
+
}
|
|
@@ -41,6 +41,7 @@ class PanelGroup {
|
|
|
41
41
|
this._reviewVisible = !document.getElementById('ai-panel')?.classList.contains('collapsed');
|
|
42
42
|
this._chatVisible = false;
|
|
43
43
|
this._popoverVisible = false;
|
|
44
|
+
this._currentPRKey = null; // Set via setPR() for per-review chat persistence
|
|
44
45
|
|
|
45
46
|
// Read persisted layout
|
|
46
47
|
const savedLayout = localStorage.getItem(PanelGroup.STORAGE_KEY);
|
|
@@ -79,20 +80,17 @@ class PanelGroup {
|
|
|
79
80
|
// Apply initial layout
|
|
80
81
|
this._applyLayout(this._layout);
|
|
81
82
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
if (chatState === 'available') {
|
|
85
|
-
this._restoreChatFromStorage();
|
|
86
|
-
} else {
|
|
87
|
-
// Chat not available yet — zero out CSS variable so max-width calcs are correct.
|
|
88
|
-
document.documentElement.style.setProperty('--chat-panel-width', '0px');
|
|
89
|
-
}
|
|
83
|
+
// Chat starts hidden; per-review state restored when setPR() is called.
|
|
84
|
+
document.documentElement.style.setProperty('--chat-panel-width', '0px');
|
|
90
85
|
|
|
91
86
|
// Listen for late chat-state transitions (config fetch may complete after constructor)
|
|
92
87
|
window.addEventListener('chat-state-changed', (e) => {
|
|
93
88
|
const state = e.detail?.state;
|
|
94
89
|
if (state === 'available') {
|
|
95
|
-
|
|
90
|
+
// Only restore if PR context is already set (setPR was called before chat became available)
|
|
91
|
+
if (this._currentPRKey) {
|
|
92
|
+
this._restoreChatFromStorage();
|
|
93
|
+
}
|
|
96
94
|
} else if (state === 'unavailable' && this.chatToggleBtn) {
|
|
97
95
|
this.chatToggleBtn.title = 'Install and configure Pi to enable chat';
|
|
98
96
|
}
|
|
@@ -422,10 +420,34 @@ class PanelGroup {
|
|
|
422
420
|
}
|
|
423
421
|
|
|
424
422
|
/**
|
|
425
|
-
*
|
|
423
|
+
* Set the current PR for per-review chat visibility persistence.
|
|
424
|
+
* Call this when a PR/review loads (after aiPanel.setPR).
|
|
425
|
+
* @param {string} prKey - e.g. "owner/repo#123" or "local/local#2"
|
|
426
|
+
*/
|
|
427
|
+
setPR(prKey) {
|
|
428
|
+
this._currentPRKey = prKey;
|
|
429
|
+
// Now that we know the review context, restore chat state if chat is available
|
|
430
|
+
if (this._isChatAvailable()) {
|
|
431
|
+
this._restoreChatFromStorage();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get the per-review localStorage key for chat visibility.
|
|
437
|
+
* @returns {string|null} Storage key or null if no PR context
|
|
438
|
+
*/
|
|
439
|
+
_getChatVisibleStorageKey() {
|
|
440
|
+
if (!this._currentPRKey) return null;
|
|
441
|
+
return `${PanelGroup.CHAT_VISIBLE_KEY}_${this._currentPRKey}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Restore chat visibility from localStorage (per-review).
|
|
446
|
+
* For new reviews (no saved state), chat stays hidden.
|
|
426
447
|
*/
|
|
427
448
|
_restoreChatFromStorage() {
|
|
428
|
-
const
|
|
449
|
+
const key = this._getChatVisibleStorageKey();
|
|
450
|
+
const savedChatVisible = key ? localStorage.getItem(key) : null;
|
|
429
451
|
if (savedChatVisible === 'true') {
|
|
430
452
|
this._chatVisible = true;
|
|
431
453
|
this.chatPanel.open({ suppressFocus: true });
|
|
@@ -478,8 +500,11 @@ class PanelGroup {
|
|
|
478
500
|
this.chatToggleBtn.classList.toggle('active', visible);
|
|
479
501
|
}
|
|
480
502
|
|
|
481
|
-
// Persist chat visibility
|
|
482
|
-
|
|
503
|
+
// Persist chat visibility (per-review)
|
|
504
|
+
const chatKey = this._getChatVisibleStorageKey();
|
|
505
|
+
if (chatKey) {
|
|
506
|
+
localStorage.setItem(chatKey, visible ? 'true' : 'false');
|
|
507
|
+
}
|
|
483
508
|
|
|
484
509
|
// Clear inline flex heights so the remaining panel fills the space
|
|
485
510
|
if (this._layout.startsWith('v-')) {
|
package/public/js/local.js
CHANGED
|
@@ -374,6 +374,7 @@ class LocalManager {
|
|
|
374
374
|
if (data.running && data.analysisId) {
|
|
375
375
|
manager.currentAnalysisId = data.analysisId;
|
|
376
376
|
manager.isAnalyzing = true;
|
|
377
|
+
window.aiPanel?.setAnalysisState('loading');
|
|
377
378
|
manager.setButtonAnalyzing(data.analysisId);
|
|
378
379
|
|
|
379
380
|
// Show the appropriate progress modal
|
|
@@ -791,10 +792,11 @@ class LocalManager {
|
|
|
791
792
|
window.aiPanel = new window.AIPanel();
|
|
792
793
|
}
|
|
793
794
|
|
|
794
|
-
// Set local context for AI Panel (restores
|
|
795
|
+
// Set local context for AI Panel and Panel Group (restores per-review state from localStorage)
|
|
795
796
|
if (window.aiPanel?.setPR) {
|
|
796
797
|
window.aiPanel.setPR('local', reviewData.repository, this.reviewId);
|
|
797
798
|
}
|
|
799
|
+
window.panelGroup?.setPR(`local/${reviewData.repository}#${this.reviewId}`);
|
|
798
800
|
|
|
799
801
|
// Load saved comments using the restored filter state from AI Panel
|
|
800
802
|
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
package/public/js/pr.js
CHANGED
|
@@ -368,6 +368,9 @@ class PRManager {
|
|
|
368
368
|
|
|
369
369
|
/**
|
|
370
370
|
* Auto-trigger analysis if ?analyze=true is present in the URL.
|
|
371
|
+
* Skips refresh if data was just loaded fresh by loadPR (to avoid redundant fetches).
|
|
372
|
+
* Otherwise, refreshes PR data first to ensure we analyze the latest code.
|
|
373
|
+
* If refresh fails, proceeds with existing data rather than failing entirely.
|
|
371
374
|
* Cleans up the query parameter afterwards regardless of success or failure.
|
|
372
375
|
* @param {string} owner - Repository owner
|
|
373
376
|
* @param {string} repo - Repository name
|
|
@@ -378,6 +381,21 @@ class PRManager {
|
|
|
378
381
|
if (autoAnalyze === 'true' && !this.isAnalyzing) {
|
|
379
382
|
this._autoAnalyzeRequested = true;
|
|
380
383
|
try {
|
|
384
|
+
// Skip refresh if we just loaded fresh data (loadPR sets _justLoaded = true).
|
|
385
|
+
// Otherwise, refresh to ensure we have the latest PR data in case the worktree
|
|
386
|
+
// already existed but the PR has new commits since last load.
|
|
387
|
+
if (this._justLoaded) {
|
|
388
|
+
this._justLoaded = false;
|
|
389
|
+
} else {
|
|
390
|
+
try {
|
|
391
|
+
await this.refreshPR();
|
|
392
|
+
} catch (e) {
|
|
393
|
+
// If refresh fails, proceed with existing data - this is intentional.
|
|
394
|
+
// We'd rather analyze stale data than fail entirely.
|
|
395
|
+
console.warn('Pre-analysis refresh failed, proceeding with existing data', e);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
381
399
|
await this.startAnalysis(owner, repo, prNumber, null, {});
|
|
382
400
|
} finally {
|
|
383
401
|
this._autoAnalyzeRequested = false;
|
|
@@ -426,11 +444,12 @@ class PRManager {
|
|
|
426
444
|
window.aiPanel = new window.AIPanel();
|
|
427
445
|
}
|
|
428
446
|
|
|
429
|
-
// Set PR context for AI Panel (for
|
|
430
|
-
// This restores the filter state from localStorage
|
|
447
|
+
// Set PR context for AI Panel and Panel Group (for per-review localStorage keys)
|
|
448
|
+
// This restores the filter state and chat visibility from localStorage
|
|
431
449
|
if (window.aiPanel?.setPR) {
|
|
432
450
|
window.aiPanel.setPR(owner, repo, number);
|
|
433
451
|
}
|
|
452
|
+
window.panelGroup?.setPR(`${owner}/${repo}#${number}`);
|
|
434
453
|
|
|
435
454
|
// Load saved comments using the restored filter state from AI Panel
|
|
436
455
|
// If AI Panel has showDismissedComments=true (restored from localStorage), use that
|
|
@@ -470,6 +489,8 @@ class PRManager {
|
|
|
470
489
|
this.showError(error.message);
|
|
471
490
|
} finally {
|
|
472
491
|
this.setLoading(false);
|
|
492
|
+
// Mark that we just loaded fresh data - used by _maybeAutoAnalyze to skip redundant refresh
|
|
493
|
+
this._justLoaded = true;
|
|
473
494
|
}
|
|
474
495
|
}
|
|
475
496
|
|
package/public/local.html
CHANGED
|
@@ -404,7 +404,7 @@
|
|
|
404
404
|
<!-- Right Panel Group (AI Panel + Chat) -->
|
|
405
405
|
<div class="right-panel-group" id="right-panel-group">
|
|
406
406
|
<!-- AI Analysis Panel (Right) -->
|
|
407
|
-
<aside class="ai-panel" id="ai-panel">
|
|
407
|
+
<aside class="ai-panel collapsed" id="ai-panel">
|
|
408
408
|
<div class="resize-handle resize-handle-left" data-panel="ai-panel"></div>
|
|
409
409
|
<div class="ai-panel-header">
|
|
410
410
|
<div class="ai-panel-title">
|
package/public/pr.html
CHANGED
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
<!-- Right Panel Group (AI Panel + Chat) -->
|
|
228
228
|
<div class="right-panel-group" id="right-panel-group">
|
|
229
229
|
<!-- AI Analysis Panel (Right) -->
|
|
230
|
-
<aside class="ai-panel" id="ai-panel">
|
|
230
|
+
<aside class="ai-panel collapsed" id="ai-panel">
|
|
231
231
|
<div class="resize-handle resize-handle-left" data-panel="ai-panel"></div>
|
|
232
232
|
<div class="ai-panel-header">
|
|
233
233
|
<div class="ai-panel-title">
|