@in-the-loop-labs/pair-review 2.3.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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.0",
3
+ "version": "2.3.1",
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.0",
3
+ "version": "2.3.1",
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",
@@ -28,7 +28,7 @@ class ChatPanel {
28
28
  this._contextItemId = null; // suggestion ID or comment ID from context
29
29
  this._contextLineMeta = null; // { file, line_start, line_end } — set when opened with line context
30
30
  this._pendingActionContext = null; // { type, itemId } — set by action button handlers, consumed by sendMessage
31
- this._resizeConfig = { min: 300, default: 400, storageKey: 'chat-panel-width' };
31
+ this._resizeConfig = ChatPanel.RESIZE_CONFIG;
32
32
  this._analysisContextRemoved = false;
33
33
  this._sessionAnalysisRunId = null; // tracks which AI run ID's context is loaded in the current session
34
34
  this._openPromise = null; // concurrency guard for open()
@@ -3007,6 +3007,9 @@ class ChatPanel {
3007
3007
  }
3008
3008
  }
3009
3009
 
3010
+ /** Resize configuration for the chat panel, exposed as a static for cross-module use. */
3011
+ ChatPanel.RESIZE_CONFIG = { min: 300, default: 400, cssVar: '--chat-panel-width', storageKey: 'chat-panel-width' };
3012
+
3010
3013
  // Make ChatPanel available globally
3011
3014
  window.ChatPanel = ChatPanel;
3012
3015
 
@@ -60,11 +60,13 @@ class PanelGroup {
60
60
  window.chatPanel = this.chatPanel;
61
61
 
62
62
  // Create a full-height group resize handle for vertical layouts.
63
- // Uses data-panel="ai-panel" so the existing PanelResizer picks it up automatically.
63
+ // Uses data-panel="panel-group" so PanelResizer updates both --ai-panel-width
64
+ // and --chat-panel-width in tandem (needed because the group CSS uses
65
+ // `width: max(--ai-panel-width, --chat-panel-width)`).
64
66
  if (this.groupEl) {
65
67
  this._groupResizeHandle = document.createElement('div');
66
68
  this._groupResizeHandle.className = 'panel-group-resize-handle resize-handle resize-handle-left';
67
- this._groupResizeHandle.dataset.panel = 'ai-panel';
69
+ this._groupResizeHandle.dataset.panel = 'panel-group';
68
70
  this.groupEl.insertBefore(this._groupResizeHandle, this.groupEl.firstChild);
69
71
  }
70
72
 
@@ -26,6 +26,30 @@ window.PanelResizer = (function() {
26
26
  // Note: chat-panel resize is handled by ChatPanel itself (see ChatPanel._bindResizeEvents)
27
27
  };
28
28
 
29
+ // panel-group is a virtual panel used by the vertical-layout group resize handle.
30
+ // In vertical mode the group width is `max(--ai-panel-width, --chat-panel-width)`,
31
+ // so dragging the group handle must update BOTH CSS vars in tandem.
32
+ //
33
+ // Built lazily because ChatPanel.RESIZE_CONFIG is defined after this IIFE runs.
34
+ let _panelGroupConfig = null;
35
+ function getPanelGroupConfig() {
36
+ if (!_panelGroupConfig) {
37
+ const aiCfg = CONFIG['ai-panel'];
38
+ // ChatPanel.RESIZE_CONFIG is the canonical source for chat-panel sizing.
39
+ const chatCfg = window.ChatPanel?.RESIZE_CONFIG
40
+ ?? { cssVar: '--chat-panel-width', storageKey: 'chat-panel-width', default: 400, min: 300 };
41
+ const panels = [
42
+ { cssVar: aiCfg.cssVar, storageKey: aiCfg.storageKey, default: aiCfg.default },
43
+ { cssVar: chatCfg.cssVar, storageKey: chatCfg.storageKey, default: chatCfg.default }
44
+ ];
45
+ _panelGroupConfig = {
46
+ min: Math.max(aiCfg.min, chatCfg.min),
47
+ panels
48
+ };
49
+ }
50
+ return _panelGroupConfig;
51
+ }
52
+
29
53
  /**
30
54
  * Compute the effective max width for a panel.
31
55
  * For panels with a static max, returns that value.
@@ -39,6 +63,39 @@ window.PanelResizer = (function() {
39
63
  return window.innerWidth - sidebarWidth - 100;
40
64
  }
41
65
 
66
+ /**
67
+ * Get the current panel-group width (the max of its sub-panel CSS vars).
68
+ * This matches the CSS `max()` expression used in vertical layouts.
69
+ * @returns {number} Current group width in pixels
70
+ */
71
+ function getPanelGroupWidth() {
72
+ let maxWidth = 0;
73
+ for (const p of getPanelGroupConfig().panels) {
74
+ const val = parseInt(
75
+ getComputedStyle(document.documentElement).getPropertyValue(p.cssVar), 10
76
+ ) || p.default;
77
+ if (val > maxWidth) maxWidth = val;
78
+ }
79
+ return maxWidth;
80
+ }
81
+
82
+ /**
83
+ * Set the panel-group width by updating ALL sub-panel CSS vars in tandem.
84
+ * @param {number} width - Desired width in pixels
85
+ * @param {boolean} save - Whether to persist to localStorage
86
+ */
87
+ function setPanelGroupWidth(width, save = true) {
88
+ const effectiveMax = getEffectiveMax('ai-panel'); // same viewport constraint
89
+ const clamped = Math.max(getPanelGroupConfig().min, Math.min(effectiveMax, width));
90
+
91
+ for (const p of getPanelGroupConfig().panels) {
92
+ document.documentElement.style.setProperty(p.cssVar, `${clamped}px`);
93
+ if (save) {
94
+ localStorage.setItem(p.storageKey, clamped.toString());
95
+ }
96
+ }
97
+ }
98
+
42
99
  // State
43
100
  let isDragging = false;
44
101
  let currentPanel = null;
@@ -127,6 +184,20 @@ window.PanelResizer = (function() {
127
184
  if (!handle) return;
128
185
 
129
186
  const panelName = handle.dataset.panel;
187
+
188
+ // panel-group: virtual panel — start width is the max of the sub-panels
189
+ if (panelName === 'panel-group') {
190
+ isDragging = true;
191
+ currentPanel = panelName;
192
+ startX = e.clientX;
193
+ startWidth = getPanelGroupWidth();
194
+
195
+ handle.classList.add('dragging');
196
+ document.body.classList.add('resizing');
197
+ e.preventDefault();
198
+ return;
199
+ }
200
+
130
201
  const panelEl = panelName === 'sidebar'
131
202
  ? document.getElementById('files-sidebar')
132
203
  : panelName === 'chat-panel'
@@ -157,6 +228,14 @@ window.PanelResizer = (function() {
157
228
  function onMouseMove(e) {
158
229
  if (!isDragging || !currentPanel) return;
159
230
 
231
+ // panel-group: update both sub-panel CSS vars in tandem
232
+ if (currentPanel === 'panel-group') {
233
+ const delta = startX - e.clientX; // right-side panel: left = wider
234
+ const newWidth = startWidth + delta;
235
+ setPanelGroupWidth(newWidth, false);
236
+ return;
237
+ }
238
+
160
239
  const config = CONFIG[currentPanel];
161
240
  if (!config) return;
162
241
 
@@ -182,7 +261,11 @@ window.PanelResizer = (function() {
182
261
  if (!isDragging) return;
183
262
 
184
263
  // Save final width
185
- if (currentPanel) {
264
+ if (currentPanel === 'panel-group') {
265
+ // Persist both sub-panel widths
266
+ const finalWidth = getPanelGroupWidth();
267
+ setPanelGroupWidth(finalWidth, true);
268
+ } else if (currentPanel) {
186
269
  const finalWidth = getPanelWidth(currentPanel);
187
270
  const config = CONFIG[currentPanel];
188
271
  if (config) {
package/public/js/pr.js CHANGED
@@ -1400,6 +1400,15 @@ class PRManager {
1400
1400
  }
1401
1401
  } else {
1402
1402
  this.viewedFiles.delete(filePath);
1403
+ // Auto-expand when unchecking viewed (match GitHub behavior)
1404
+ if (wrapper && wrapper.classList.contains('collapsed')) {
1405
+ wrapper.classList.remove('collapsed');
1406
+ this.collapsedFiles.delete(filePath);
1407
+ const header = wrapper.querySelector('.d2h-file-header');
1408
+ if (header) {
1409
+ window.DiffRenderer.updateFileHeaderState(header, true);
1410
+ }
1411
+ }
1403
1412
  }
1404
1413
 
1405
1414
  // Persist viewed state