@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "2.3.1",
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.1",
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
- // Restore chat visibility from last session (only if chat is available)
83
- const chatState = document.documentElement.getAttribute('data-chat');
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
- this._restoreChatFromStorage();
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
- * Restore chat visibility from localStorage (called when chat becomes available)
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 savedChatVisible = localStorage.getItem(PanelGroup.CHAT_VISIBLE_KEY);
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
- localStorage.setItem(PanelGroup.CHAT_VISIBLE_KEY, visible ? 'true' : 'false');
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-')) {
@@ -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 filter state from localStorage)
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 PR-specific localStorage keys)
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">