@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.
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
- package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
- package/public/css/analysis-config.css +103 -0
- package/public/css/pr.css +191 -4
- package/public/index.html +20 -0
- package/public/js/components/AIPanel.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +87 -9
- package/public/js/components/AnalysisConfigModal.js +206 -5
- package/public/js/components/ChatPanel.js +22 -5
- package/public/js/components/CouncilProgressModal.js +241 -23
- package/public/js/components/TimeoutSelect.js +2 -0
- package/public/js/components/VoiceCentricConfigTab.js +183 -13
- package/public/js/index.js +119 -1
- package/public/js/local.js +166 -51
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +71 -12
- package/public/js/repo-settings.js +2 -2
- package/public/local.html +32 -11
- package/public/pr.html +2 -0
- package/src/ai/analyzer.js +371 -111
- package/src/ai/claude-provider.js +2 -0
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +2 -0
- package/src/ai/executable-provider.js +538 -0
- package/src/ai/gemini-provider.js +2 -0
- package/src/ai/index.js +9 -1
- package/src/ai/pi-provider.js +10 -8
- package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
- package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
- package/src/ai/prompts/baseline/level1/balanced.js +12 -0
- package/src/ai/prompts/baseline/level1/fast.js +11 -0
- package/src/ai/prompts/baseline/level1/thorough.js +12 -0
- package/src/ai/prompts/baseline/level2/balanced.js +13 -0
- package/src/ai/prompts/baseline/level2/fast.js +12 -0
- package/src/ai/prompts/baseline/level2/thorough.js +13 -0
- package/src/ai/prompts/baseline/level3/balanced.js +13 -0
- package/src/ai/prompts/baseline/level3/fast.js +12 -0
- package/src/ai/prompts/baseline/level3/thorough.js +13 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
- package/src/ai/prompts/render-for-skill.js +3 -0
- package/src/ai/prompts/shared/output-schema.js +8 -0
- package/src/ai/provider.js +91 -4
- package/src/chat/prompt-builder.js +39 -4
- package/src/chat/session-manager.js +32 -28
- package/src/config.js +15 -2
- package/src/database.js +59 -15
- package/src/git/base-branch.js +113 -29
- package/src/github/parser.js +1 -1
- package/src/local-review.js +15 -9
- package/src/local-scope.js +83 -0
- package/src/main.js +3 -2
- package/src/routes/analyses.js +34 -8
- package/src/routes/chat.js +15 -8
- package/src/routes/config.js +3 -120
- package/src/routes/councils.js +15 -6
- package/src/routes/executable-analysis.js +494 -0
- package/src/routes/local.js +152 -15
- package/src/routes/mcp.js +9 -4
- package/src/routes/pr.js +166 -29
- package/src/routes/reviews.js +31 -5
- package/src/routes/shared.js +72 -5
- package/src/routes/worktrees.js +4 -2
- package/src/utils/comment-formatter.js +28 -11
- package/src/utils/instructions.js +22 -8
- 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'
|
|
254
|
-
e.
|
|
255
|
-
|
|
256
|
-
|
|
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
|
});
|