@in-the-loop-labs/pair-review 3.0.6 → 3.1.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.
Files changed (81) hide show
  1. package/package.json +2 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
  5. package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
  6. package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
  7. package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
  8. package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
  9. package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
  10. package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
  11. package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
  12. package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
  13. package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
  14. package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
  15. package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
  16. package/public/css/analysis-config.css +103 -0
  17. package/public/css/pr.css +191 -4
  18. package/public/index.html +20 -0
  19. package/public/js/components/AIPanel.js +1 -1
  20. package/public/js/components/AdvancedConfigTab.js +87 -9
  21. package/public/js/components/AnalysisConfigModal.js +206 -5
  22. package/public/js/components/ChatPanel.js +22 -5
  23. package/public/js/components/CouncilProgressModal.js +241 -23
  24. package/public/js/components/TimeoutSelect.js +2 -0
  25. package/public/js/components/VoiceCentricConfigTab.js +183 -13
  26. package/public/js/index.js +119 -1
  27. package/public/js/local.js +166 -51
  28. package/public/js/modules/suggestion-manager.js +2 -1
  29. package/public/js/pr.js +71 -12
  30. package/public/js/repo-settings.js +2 -2
  31. package/public/local.html +32 -11
  32. package/public/pr.html +2 -0
  33. package/src/ai/analyzer.js +371 -111
  34. package/src/ai/claude-provider.js +2 -0
  35. package/src/ai/codex-provider.js +1 -1
  36. package/src/ai/copilot-provider.js +2 -0
  37. package/src/ai/executable-provider.js +538 -0
  38. package/src/ai/gemini-provider.js +2 -0
  39. package/src/ai/index.js +9 -1
  40. package/src/ai/pi-provider.js +10 -8
  41. package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
  42. package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
  43. package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
  44. package/src/ai/prompts/baseline/level1/balanced.js +12 -0
  45. package/src/ai/prompts/baseline/level1/fast.js +11 -0
  46. package/src/ai/prompts/baseline/level1/thorough.js +12 -0
  47. package/src/ai/prompts/baseline/level2/balanced.js +13 -0
  48. package/src/ai/prompts/baseline/level2/fast.js +12 -0
  49. package/src/ai/prompts/baseline/level2/thorough.js +13 -0
  50. package/src/ai/prompts/baseline/level3/balanced.js +13 -0
  51. package/src/ai/prompts/baseline/level3/fast.js +12 -0
  52. package/src/ai/prompts/baseline/level3/thorough.js +13 -0
  53. package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
  54. package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
  55. package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
  56. package/src/ai/prompts/render-for-skill.js +3 -0
  57. package/src/ai/prompts/shared/output-schema.js +8 -0
  58. package/src/ai/provider.js +91 -4
  59. package/src/chat/prompt-builder.js +39 -4
  60. package/src/chat/session-manager.js +32 -28
  61. package/src/config.js +15 -2
  62. package/src/database.js +59 -15
  63. package/src/git/base-branch.js +113 -29
  64. package/src/github/parser.js +1 -1
  65. package/src/local-review.js +15 -9
  66. package/src/local-scope.js +83 -0
  67. package/src/main.js +3 -2
  68. package/src/routes/analyses.js +34 -8
  69. package/src/routes/chat.js +15 -8
  70. package/src/routes/config.js +3 -120
  71. package/src/routes/councils.js +15 -6
  72. package/src/routes/executable-analysis.js +494 -0
  73. package/src/routes/local.js +152 -15
  74. package/src/routes/mcp.js +9 -4
  75. package/src/routes/pr.js +166 -29
  76. package/src/routes/reviews.js +31 -5
  77. package/src/routes/shared.js +72 -5
  78. package/src/routes/worktrees.js +4 -2
  79. package/src/utils/comment-formatter.js +28 -11
  80. package/src/utils/instructions.js +22 -8
  81. package/src/utils/logger.js +20 -10
@@ -275,6 +275,28 @@ class AnalysisConfigModal {
275
275
  </div>
276
276
  </section>
277
277
 
278
+ <!-- Exclude Previous Findings -->
279
+ <details class="config-section exclude-previous-section">
280
+ <summary class="section-title">
281
+ Exclude Previous Findings
282
+ <span class="section-hint">(optional)</span>
283
+ </summary>
284
+ <div class="exclude-previous-options">
285
+ <label class="remember-toggle">
286
+ <input type="checkbox" id="exclude-github-comments" />
287
+ <span class="toggle-switch"></span>
288
+ <span class="toggle-label">GitHub PR review comments</span>
289
+ </label>
290
+ <p class="option-hint">Skip issues already noted in inline PR review comments</p>
291
+ <label class="remember-toggle">
292
+ <input type="checkbox" id="exclude-pr-feedback" />
293
+ <span class="toggle-switch"></span>
294
+ <span class="toggle-label">Existing pair-review feedback</span>
295
+ </label>
296
+ <p class="option-hint">Skip issues from previous AI suggestions and reviewer comments</p>
297
+ </div>
298
+ </details>
299
+
278
300
  <!-- Focus Presets - Hidden for now, may reintroduce later -->
279
301
  <section class="config-section" style="display: none;">
280
302
  <h4 class="section-title">
@@ -499,7 +521,7 @@ class AnalysisConfigModal {
499
521
  container.innerHTML = this.models.map(model => `
500
522
  <button class="model-card ${model.id === this.selectedModel ? 'selected' : ''}" data-model="${model.id}" data-tier="${model.tier}">
501
523
  <div class="model-badge ${model.badgeClass || ''}">${model.badge || ''}</div>
502
- <div class="model-icon">${this.getModelIcon(model.tier)}</div>
524
+ <div class="model-icon">${model.icon || this.getModelIcon(model.tier)}</div>
503
525
  <div class="model-info">
504
526
  <span class="model-name">${model.name}</span>
505
527
  <span class="model-tagline">${model.tagline || ''}</span>
@@ -549,10 +571,54 @@ class AnalysisConfigModal {
549
571
  // Re-render model cards (handles its own event listeners)
550
572
  this.renderModelCards();
551
573
 
574
+ // Toggle level toggles visibility based on provider's capabilities
575
+ const provider = this.providers[providerId];
576
+ const supportsLevels = provider.capabilities?.review_levels !== false;
577
+ const supportsCustomInstructions = provider.capabilities?.custom_instructions !== false;
578
+ const levelToggles = this.modal.querySelector('.single-level-toggles');
579
+ const skipLevel3Info = this.modal.querySelector('#skip-level3-info');
580
+ const existingLevelsNote = this.modal.querySelector('.executable-provider-levels-note');
581
+ const customInstructionsSection = this.modal.querySelector('#custom-instructions')?.closest('.config-section');
582
+
583
+ if (!supportsLevels) {
584
+ if (levelToggles) levelToggles.style.display = 'none';
585
+ if (skipLevel3Info) skipLevel3Info.style.display = 'none';
586
+ if (!existingLevelsNote) {
587
+ const note = document.createElement('div');
588
+ note.className = 'executable-provider-note executable-provider-levels-note';
589
+ note.textContent = 'This provider runs its own analysis pipeline';
590
+ levelToggles?.parentNode?.appendChild(note);
591
+ }
592
+ } else {
593
+ if (levelToggles) levelToggles.style.display = '';
594
+ if (skipLevel3Info) skipLevel3Info.style.display = '';
595
+ if (existingLevelsNote) existingLevelsNote.remove();
596
+ }
597
+
598
+ // Toggle custom instructions visibility based on provider's capabilities
599
+ const instructionsContainer = customInstructionsSection?.querySelector('.instructions-container');
600
+ const existingInstructionsNote = customInstructionsSection?.querySelector('.executable-provider-instructions-note');
601
+
602
+ if (!supportsCustomInstructions) {
603
+ if (instructionsContainer) instructionsContainer.style.display = 'none';
604
+ if (!existingInstructionsNote && customInstructionsSection) {
605
+ const note = document.createElement('div');
606
+ note.className = 'executable-provider-note executable-provider-instructions-note';
607
+ note.textContent = 'This provider runs its own analysis pipeline and does not accept custom instructions.';
608
+ customInstructionsSection.appendChild(note);
609
+ }
610
+ } else {
611
+ if (instructionsContainer) instructionsContainer.style.display = '';
612
+ if (existingInstructionsNote) existingInstructionsNote.remove();
613
+ }
614
+
552
615
  // Update selection state for the selected model
553
616
  if (this.selectedModel) {
554
617
  this.selectModel(this.selectedModel);
555
618
  }
619
+
620
+ // Update exclude-previous section based on provider's exclude_previous capability
621
+ this._updateExcludePreviousState();
556
622
  }
557
623
 
558
624
  /**
@@ -711,7 +777,8 @@ class AnalysisConfigModal {
711
777
  councilName: selectedCouncil?.name || null,
712
778
  councilConfig: councilConfig,
713
779
  customInstructions: this.modal.querySelector('#vc-custom-instructions')?.value?.trim() || '',
714
- repoInstructions: this.repoInstructions
780
+ repoInstructions: this.repoInstructions,
781
+ excludePrevious: this._getAndSaveExcludePrevious()
715
782
  };
716
783
 
717
784
  if (this.onSubmit) this.onSubmit(config);
@@ -736,7 +803,8 @@ class AnalysisConfigModal {
736
803
  councilName: selectedCouncil?.name || null,
737
804
  councilConfig: councilConfig,
738
805
  customInstructions: this.modal.querySelector('#council-custom-instructions')?.value?.trim() || '',
739
- repoInstructions: this.repoInstructions
806
+ repoInstructions: this.repoInstructions,
807
+ excludePrevious: this._getAndSaveExcludePrevious()
740
808
  };
741
809
 
742
810
  if (this.onSubmit) this.onSubmit(config);
@@ -748,16 +816,23 @@ class AnalysisConfigModal {
748
816
  const selectedModelCard = this.modal.querySelector('.model-card.selected');
749
817
  const tier = selectedModelCard?.dataset?.tier || 'balanced';
750
818
 
819
+ const selectedProvider = this.providers[this.selectedProvider];
820
+ const noLevels = selectedProvider?.capabilities?.review_levels === false;
821
+
751
822
  const config = {
752
823
  provider: this.selectedProvider,
753
824
  model: this.selectedModel,
754
825
  tier: tier,
755
826
  instructions: this.buildInstructions(),
756
- customInstructions: this.modal.querySelector('#custom-instructions')?.value?.trim() || '',
827
+ customInstructions: selectedProvider?.capabilities?.custom_instructions === false ? '' : (this.modal.querySelector('#custom-instructions')?.value?.trim() || ''),
757
828
  presets: Array.from(this.selectedPresets),
758
829
  repoInstructions: this.repoInstructions,
759
830
  enabledLevels: [...this.enabledLevels],
760
- skipLevel3: !this.enabledLevels.includes(3)
831
+ skipLevel3: !this.enabledLevels.includes(3),
832
+ noLevels,
833
+ excludePrevious: selectedProvider?.capabilities?.exclude_previous === false
834
+ ? undefined
835
+ : this._getAndSaveExcludePrevious()
761
836
  };
762
837
 
763
838
  if (this.onSubmit) this.onSubmit(config);
@@ -895,6 +970,9 @@ class AnalysisConfigModal {
895
970
  }
896
971
  }
897
972
 
973
+ // Restore exclude-previous checkbox state from localStorage
974
+ this._restoreExcludePrevious(options);
975
+
898
976
  // Remove loading state and reveal content
899
977
  this._showLoading(false);
900
978
 
@@ -953,6 +1031,10 @@ class AnalysisConfigModal {
953
1031
  <button class="analysis-tab" data-tab="advanced">Advanced</button>
954
1032
  `;
955
1033
 
1034
+ // Hoist the exclude-previous section out of the single-model panel
1035
+ // so it remains visible across all tabs.
1036
+ const excludePreviousSection = singlePanel.querySelector('.exclude-previous-section');
1037
+
956
1038
  // Assemble
957
1039
  modalBody.innerHTML = '';
958
1040
  modalBody.appendChild(tabBar);
@@ -960,6 +1042,12 @@ class AnalysisConfigModal {
960
1042
  modalBody.appendChild(councilPanel);
961
1043
  modalBody.appendChild(advancedPanel);
962
1044
 
1045
+ // Place the exclude-previous section after all tab panels,
1046
+ // just before the modal footer, so it's visible on every tab.
1047
+ if (excludePreviousSection) {
1048
+ modalBody.appendChild(excludePreviousSection);
1049
+ }
1050
+
963
1051
  // Tab click listeners
964
1052
  tabBar.querySelectorAll('.analysis-tab').forEach(tab => {
965
1053
  tab.addEventListener('click', () => {
@@ -1068,6 +1156,9 @@ class AnalysisConfigModal {
1068
1156
  dirtyHintContainer.style.display = 'none';
1069
1157
  }
1070
1158
  }
1159
+
1160
+ // Re-evaluate exclude-previous section based on new tab context
1161
+ this._updateExcludePreviousState();
1071
1162
  }
1072
1163
 
1073
1164
  /**
@@ -1212,6 +1303,116 @@ class AnalysisConfigModal {
1212
1303
  }, 200);
1213
1304
  }
1214
1305
 
1306
+ /**
1307
+ * Restore exclude-previous checkbox state from localStorage.
1308
+ * Disables the GitHub checkbox when there is no PR or no GitHub token.
1309
+ * @param {Object} options - show() options
1310
+ * @private
1311
+ */
1312
+ _restoreExcludePrevious(options) {
1313
+ const githubCb = this.modal.querySelector('#exclude-github-comments');
1314
+ const feedbackCb = this.modal.querySelector('#exclude-pr-feedback');
1315
+ if (!githubCb || !feedbackCb) return;
1316
+
1317
+ // Restore saved state
1318
+ let saved = { github: false, feedback: false };
1319
+ try {
1320
+ const raw = localStorage.getItem('pair-review-exclude-previous');
1321
+ if (raw) saved = JSON.parse(raw);
1322
+ } catch (_) { /* ignore parse errors */ }
1323
+
1324
+ githubCb.checked = !!saved.github;
1325
+ feedbackCb.checked = !!saved.feedback;
1326
+
1327
+ // Disable the GitHub checkbox when it cannot be used
1328
+ const canUseGithub = options.hasPr !== false && options.hasGithubToken !== false;
1329
+ githubCb.disabled = !canUseGithub;
1330
+ if (!canUseGithub) {
1331
+ const label = githubCb.closest('.remember-toggle');
1332
+ if (label) label.title = 'Not available — requires a PR with a GitHub token';
1333
+ } else {
1334
+ const label = githubCb.closest('.remember-toggle');
1335
+ if (label) label.title = '';
1336
+ }
1337
+ }
1338
+
1339
+ /**
1340
+ * Read current exclude-previous checkbox state, save to localStorage,
1341
+ * and return the config object.
1342
+ * @returns {{ github: boolean, feedback: boolean }}
1343
+ * @private
1344
+ */
1345
+ _getAndSaveExcludePrevious() {
1346
+ const githubCb = this.modal.querySelector('#exclude-github-comments');
1347
+ const feedbackCb = this.modal.querySelector('#exclude-pr-feedback');
1348
+
1349
+ // When the GitHub checkbox is disabled, preserve the existing localStorage
1350
+ // value for `github` so the user's preference survives contexts where the
1351
+ // option is unavailable.
1352
+ let preservedGithub = false;
1353
+ if (githubCb?.disabled) {
1354
+ try {
1355
+ const raw = localStorage.getItem('pair-review-exclude-previous');
1356
+ if (raw) {
1357
+ const prev = JSON.parse(raw);
1358
+ preservedGithub = !!prev.github;
1359
+ }
1360
+ } catch (_) { /* ignore parse errors */ }
1361
+ }
1362
+
1363
+ const state = {
1364
+ github: githubCb?.disabled ? preservedGithub : !!githubCb?.checked,
1365
+ feedback: !!feedbackCb?.checked
1366
+ };
1367
+ try {
1368
+ localStorage.setItem('pair-review-exclude-previous', JSON.stringify(state));
1369
+ } catch (_) { /* ignore storage errors */ }
1370
+ return state;
1371
+ }
1372
+
1373
+ /**
1374
+ * Update the exclude-previous section's enabled/disabled state based on
1375
+ * the active tab and the selected provider's capabilities.
1376
+ *
1377
+ * Council/Advanced tabs: always enabled (orchestration handles dedup).
1378
+ * Single Model tab: disabled when provider has exclude_previous === false.
1379
+ *
1380
+ * This only toggles the visual state of the entire section — individual
1381
+ * checkbox checked/disabled state is never modified, so persisted
1382
+ * localStorage values remain untouched.
1383
+ * @private
1384
+ */
1385
+ _updateExcludePreviousState() {
1386
+ const section = this.modal.querySelector('.exclude-previous-section');
1387
+ if (!section) return;
1388
+
1389
+ const provider = this.providers[this.selectedProvider];
1390
+ const disabledByCapability = this.activeTab === 'single'
1391
+ && provider?.capabilities?.exclude_previous === false;
1392
+
1393
+ const existingNote = section.querySelector('.executable-provider-exclude-note');
1394
+
1395
+ if (disabledByCapability) {
1396
+ section.classList.add('exclude-previous-disabled');
1397
+ section.setAttribute('open', ''); // Ensure note is visible even if section was collapsed
1398
+
1399
+ if (!existingNote) {
1400
+ const note = document.createElement('div');
1401
+ note.className = 'executable-provider-note executable-provider-exclude-note';
1402
+ note.textContent = 'This provider does not support excluding previous findings.';
1403
+ const options = section.querySelector('.exclude-previous-options');
1404
+ if (options) {
1405
+ section.insertBefore(note, options);
1406
+ } else {
1407
+ section.appendChild(note);
1408
+ }
1409
+ }
1410
+ } else {
1411
+ section.classList.remove('exclude-previous-disabled');
1412
+ if (existingNote) existingNote.remove();
1413
+ }
1414
+ }
1415
+
1215
1416
  /**
1216
1417
  * Cleanup event listeners and pending timeouts
1217
1418
  */
@@ -43,6 +43,7 @@ class ChatPanel {
43
43
  this._sessionWarm = false; // true once the session has been used in this page load
44
44
  this._activeProvider = window.__pairReview?.chatProvider || 'pi';
45
45
  this._chatProviders = window.__pairReview?.chatProviders || [];
46
+ this._enterToSend = window.__pairReview?.chatEnterToSend ?? true;
46
47
 
47
48
  this._render();
48
49
  this._bindEvents();
@@ -141,7 +142,7 @@ class ChatPanel {
141
142
  <div class="chat-panel__input-area">
142
143
  <textarea class="chat-panel__input" placeholder="Ask about this review..." rows="1"></textarea>
143
144
  <div class="chat-panel__input-footer">
144
- <span class="chat-panel__input-hint">${typeof navigator !== 'undefined' && navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Enter to send</span>
145
+ <span class="chat-panel__input-hint" title="Configure with chat.enter_to_send">${this._enterToSend ? 'Enter to send, Shift+Enter for newline' : `${typeof navigator !== 'undefined' && navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Enter to send`}</span>
145
146
  <div class="chat-panel__input-actions">
146
147
  <button class="chat-panel__send-btn" title="Send" disabled>
147
148
  <svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14">
@@ -250,10 +251,26 @@ class ChatPanel {
250
251
 
251
252
  // Keyboard shortcuts
252
253
  this.inputEl.addEventListener('keydown', (e) => {
253
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
254
- e.preventDefault();
255
- if (this.inputEl.value.trim() && !this.isStreaming) {
256
- this.sendMessage();
254
+ if (e.key === 'Enter') {
255
+ // Ignore Enter during IME composition (e.g. CJK input) so the
256
+ // composition-confirming keystroke is not swallowed.
257
+ if (e.isComposing) return;
258
+
259
+ if (this._enterToSend) {
260
+ // Enter sends, Shift+Enter inserts newline
261
+ if (e.shiftKey) return; // let browser insert newline
262
+ e.preventDefault();
263
+ if (this.inputEl.value.trim() && !this.isStreaming) {
264
+ this.sendMessage();
265
+ }
266
+ } else {
267
+ // Cmd+Enter / Ctrl+Enter sends, plain Enter inserts newline
268
+ if (e.metaKey || e.ctrlKey) {
269
+ e.preventDefault();
270
+ if (this.inputEl.value.trim() && !this.isStreaming) {
271
+ this.sendMessage();
272
+ }
273
+ }
257
274
  }
258
275
  }
259
276
  });