@in-the-loop-labs/pair-review 1.6.2 → 2.0.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/README.md +77 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +4 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
- package/public/css/pr.css +1962 -114
- package/public/js/CONVENTIONS.md +16 -0
- package/public/js/components/AIPanel.js +66 -0
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +2955 -0
- package/public/js/components/CouncilProgressModal.js +12 -16
- package/public/js/components/KeyboardShortcuts.js +3 -0
- package/public/js/components/PanelGroup.js +723 -0
- package/public/js/components/PreviewModal.js +3 -8
- package/public/js/index.js +8 -0
- package/public/js/local.js +17 -615
- package/public/js/modules/analysis-history.js +19 -68
- package/public/js/modules/comment-manager.js +103 -20
- package/public/js/modules/diff-context.js +176 -0
- package/public/js/modules/diff-renderer.js +30 -0
- package/public/js/modules/file-comment-manager.js +126 -105
- package/public/js/modules/file-list-merger.js +64 -0
- package/public/js/modules/panel-resizer.js +25 -6
- package/public/js/modules/suggestion-manager.js +40 -125
- package/public/js/pr.js +1009 -159
- package/public/js/repo-settings.js +36 -6
- package/public/js/utils/category-emoji.js +44 -0
- package/public/js/utils/time.js +32 -0
- package/public/local.html +107 -70
- package/public/pr.html +107 -70
- package/public/repo-settings.html +32 -0
- package/src/ai/analyzer.js +5 -1
- package/src/ai/copilot-provider.js +39 -9
- package/src/ai/cursor-agent-provider.js +45 -11
- package/src/ai/gemini-provider.js +17 -4
- package/src/ai/prompts/config.js +7 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +25 -37
- package/src/chat/CONVENTIONS.md +18 -0
- package/src/chat/pi-bridge.js +491 -0
- package/src/chat/prompt-builder.js +272 -0
- package/src/chat/session-manager.js +619 -0
- package/src/config.js +14 -0
- package/src/database.js +322 -15
- package/src/main.js +4 -17
- package/src/routes/analyses.js +721 -0
- package/src/routes/chat.js +655 -0
- package/src/routes/config.js +29 -8
- package/src/routes/context-files.js +274 -0
- package/src/routes/local.js +225 -1133
- package/src/routes/mcp.js +39 -30
- package/src/routes/pr.js +424 -58
- package/src/routes/reviews.js +1035 -0
- package/src/routes/shared.js +4 -29
- package/src/server.js +34 -12
- package/src/sse/review-events.js +46 -0
- package/src/utils/auto-context.js +88 -0
- package/src/utils/category-emoji.js +33 -0
- package/src/utils/diff-annotator.js +75 -1
- package/src/utils/diff-file-list.js +57 -0
- package/src/routes/analysis.js +0 -1600
- package/src/routes/comments.js +0 -534
|
@@ -5,28 +5,60 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
class FileCommentManager {
|
|
8
|
-
// Category to emoji mapping for formatting adopted comments (matches SuggestionManager)
|
|
9
|
-
static CATEGORY_EMOJI_MAP = {
|
|
10
|
-
'bug': '\u{1F41B}', // bug
|
|
11
|
-
'improvement': '\u{1F4A1}', // lightbulb
|
|
12
|
-
'suggestion': '\u{1F4AD}', // thought balloon
|
|
13
|
-
'design': '\u{1F3D7}', // building construction
|
|
14
|
-
'performance': '\u{1F680}', // rocket
|
|
15
|
-
'security': '\u{1F512}', // lock
|
|
16
|
-
'refactor': '\u{1F527}', // wrench
|
|
17
|
-
'documentation': '\u{1F4DD}', // memo
|
|
18
|
-
'test': '\u{2705}', // white heavy check mark
|
|
19
|
-
'style': '\u{1F3A8}', // artist palette
|
|
20
|
-
'chore': '\u{1F9F9}', // broom
|
|
21
|
-
'feat': '\u{2728}', // sparkles
|
|
22
|
-
'fix': '\u{1F527}' // wrench
|
|
23
|
-
};
|
|
24
|
-
|
|
25
8
|
constructor(prManagerRef) {
|
|
26
9
|
// Reference to parent PRManager for API calls and state access
|
|
27
10
|
this.prManager = prManagerRef;
|
|
28
11
|
// Track file-level comments by file path
|
|
29
12
|
this.fileComments = new Map();
|
|
13
|
+
|
|
14
|
+
// Event delegation for "Ask about this" chat button on file-level suggestions
|
|
15
|
+
document.addEventListener('click', (e) => {
|
|
16
|
+
const chatBtn = e.target.closest('.file-comments-zone .ai-action-chat');
|
|
17
|
+
if (chatBtn && window.chatPanel) {
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
const suggestionCard = chatBtn.closest('.ai-suggestion');
|
|
20
|
+
const bodyText = suggestionCard?.dataset?.originalBody
|
|
21
|
+
? JSON.parse(suggestionCard.dataset.originalBody) : '';
|
|
22
|
+
window.chatPanel.open({
|
|
23
|
+
reviewId: this.prManager?.currentPR?.id,
|
|
24
|
+
suggestionId: chatBtn.dataset.suggestionId,
|
|
25
|
+
suggestionContext: {
|
|
26
|
+
title: chatBtn.dataset.title || '',
|
|
27
|
+
body: bodyText,
|
|
28
|
+
type: suggestionCard?.querySelector('.ai-suggestion-badge')?.dataset?.type || '',
|
|
29
|
+
file: chatBtn.dataset.file || '',
|
|
30
|
+
line_start: null,
|
|
31
|
+
line_end: null,
|
|
32
|
+
side: suggestionCard?.dataset?.side || 'RIGHT',
|
|
33
|
+
reasoning: null
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Event delegation for "Ask about this" chat button on file-level user comments
|
|
40
|
+
document.addEventListener('click', (e) => {
|
|
41
|
+
const chatBtn = e.target.closest('.file-comments-zone .btn-chat-comment');
|
|
42
|
+
if (chatBtn && window.chatPanel) {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
const commentCard = chatBtn.closest('.file-comment-card');
|
|
45
|
+
const bodyEl = commentCard?.querySelector('.user-comment-body');
|
|
46
|
+
const originalMarkdown = bodyEl?.dataset?.originalMarkdown || bodyEl?.textContent || '';
|
|
47
|
+
window.chatPanel.open({
|
|
48
|
+
reviewId: this.prManager?.currentPR?.id,
|
|
49
|
+
commentContext: {
|
|
50
|
+
commentId: chatBtn.dataset.chatCommentId,
|
|
51
|
+
body: originalMarkdown,
|
|
52
|
+
file: chatBtn.dataset.chatFile || '',
|
|
53
|
+
line_start: null,
|
|
54
|
+
line_end: null,
|
|
55
|
+
parentId: chatBtn.dataset.chatParentId || null,
|
|
56
|
+
source: 'user',
|
|
57
|
+
isFileLevel: true
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
30
62
|
}
|
|
31
63
|
|
|
32
64
|
/**
|
|
@@ -35,7 +67,7 @@ class FileCommentManager {
|
|
|
35
67
|
* @returns {string} Emoji character
|
|
36
68
|
*/
|
|
37
69
|
getCategoryEmoji(category) {
|
|
38
|
-
return
|
|
70
|
+
return window.CategoryEmoji?.getEmoji(category) || '\u{1F4AC}';
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
/**
|
|
@@ -66,50 +98,33 @@ class FileCommentManager {
|
|
|
66
98
|
*/
|
|
67
99
|
_getFileCommentEndpoint(operation, options = {}) {
|
|
68
100
|
const reviewId = this.prManager?.currentPR?.id;
|
|
69
|
-
const reviewType = this.prManager?.currentPR?.reviewType;
|
|
70
101
|
const headSha = this.prManager?.currentPR?.head_sha;
|
|
71
|
-
const isLocal = reviewType === 'local';
|
|
72
102
|
|
|
73
103
|
let endpoint;
|
|
74
104
|
let requestBody = null;
|
|
75
105
|
|
|
76
106
|
switch (operation) {
|
|
77
107
|
case 'create':
|
|
78
|
-
endpoint =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
title: options.title
|
|
89
|
-
}
|
|
90
|
-
: {
|
|
91
|
-
review_id: reviewId,
|
|
92
|
-
file: options.file,
|
|
93
|
-
body: options.body,
|
|
94
|
-
commit_sha: headSha,
|
|
95
|
-
parent_id: options.parent_id,
|
|
96
|
-
type: options.type,
|
|
97
|
-
title: options.title
|
|
98
|
-
};
|
|
108
|
+
endpoint = `/api/reviews/${reviewId}/comments`;
|
|
109
|
+
|
|
110
|
+
requestBody = {
|
|
111
|
+
file: options.file,
|
|
112
|
+
body: options.body,
|
|
113
|
+
commit_sha: headSha,
|
|
114
|
+
parent_id: options.parent_id,
|
|
115
|
+
type: options.type,
|
|
116
|
+
title: options.title
|
|
117
|
+
};
|
|
99
118
|
break;
|
|
100
119
|
|
|
101
120
|
case 'update':
|
|
102
|
-
endpoint =
|
|
103
|
-
? `/api/local/${reviewId}/file-comment/${options.commentId}`
|
|
104
|
-
: `/api/user-comment/${options.commentId}`;
|
|
121
|
+
endpoint = `/api/reviews/${reviewId}/comments/${options.commentId}`;
|
|
105
122
|
|
|
106
123
|
requestBody = { body: options.body };
|
|
107
124
|
break;
|
|
108
125
|
|
|
109
126
|
case 'delete':
|
|
110
|
-
endpoint =
|
|
111
|
-
? `/api/local/${reviewId}/file-comment/${options.commentId}`
|
|
112
|
-
: `/api/user-comment/${options.commentId}`;
|
|
127
|
+
endpoint = `/api/reviews/${reviewId}/comments/${options.commentId}`;
|
|
113
128
|
|
|
114
129
|
// No body needed for DELETE
|
|
115
130
|
break;
|
|
@@ -174,6 +189,10 @@ class FileCommentManager {
|
|
|
174
189
|
></textarea>
|
|
175
190
|
<div class="file-comment-form-footer">
|
|
176
191
|
<button class="file-comment-form-btn submit submit-btn" disabled>Save</button>
|
|
192
|
+
<button class="ai-action ai-action-chat btn-chat-from-comment" title="Chat about this file">
|
|
193
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/></svg>
|
|
194
|
+
Chat
|
|
195
|
+
</button>
|
|
177
196
|
<button class="file-comment-form-btn cancel cancel-btn">Cancel</button>
|
|
178
197
|
</div>
|
|
179
198
|
`;
|
|
@@ -221,6 +240,27 @@ class FileCommentManager {
|
|
|
221
240
|
}
|
|
222
241
|
});
|
|
223
242
|
|
|
243
|
+
// Chat button handler - opens chat panel with file-level context
|
|
244
|
+
const chatFromCommentBtn = form.querySelector('.btn-chat-from-comment');
|
|
245
|
+
if (chatFromCommentBtn) {
|
|
246
|
+
chatFromCommentBtn.addEventListener('click', () => {
|
|
247
|
+
if (!window.chatPanel) return;
|
|
248
|
+
const unsavedText = textarea.value.trim();
|
|
249
|
+
this.hideCommentForm(zone);
|
|
250
|
+
window.chatPanel.open({
|
|
251
|
+
commentContext: {
|
|
252
|
+
type: 'line',
|
|
253
|
+
body: unsavedText || null,
|
|
254
|
+
file: fileName || '',
|
|
255
|
+
line_start: null,
|
|
256
|
+
line_end: null,
|
|
257
|
+
source: 'user',
|
|
258
|
+
isFileLevel: true
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
224
264
|
}
|
|
225
265
|
|
|
226
266
|
/**
|
|
@@ -362,8 +402,10 @@ class FileCommentManager {
|
|
|
362
402
|
<span class="file-comment-badge" title="Comment applies to the entire file">File comment</span>
|
|
363
403
|
${praiseBadge}
|
|
364
404
|
${titleHtml}
|
|
365
|
-
<span class="user-comment-timestamp">${this.formatTimestamp(comment.created_at)}</span>
|
|
366
405
|
<div class="user-comment-actions">
|
|
406
|
+
<button class="btn-chat-comment" title="Chat about comment" data-chat-comment-id="${comment.id}" data-chat-file="${this.escapeHtml(comment.file || '')}" data-chat-parent-id="${comment.parent_id || ''}">
|
|
407
|
+
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/></svg>
|
|
408
|
+
</button>
|
|
367
409
|
<button class="btn-edit-comment" title="Edit comment">
|
|
368
410
|
<svg class="octicon" viewBox="0 0 16 16" width="16" height="16">
|
|
369
411
|
<path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path>
|
|
@@ -455,13 +497,13 @@ class FileCommentManager {
|
|
|
455
497
|
${categoryLabel ? `<span class="ai-suggestion-category">${this.escapeHtml(categoryLabel)}</span>` : ''}
|
|
456
498
|
<span class="ai-title">${this.escapeHtml(suggestion.title || '')}</span>
|
|
457
499
|
</div>
|
|
458
|
-
${suggestion.reasoning && suggestion.reasoning.length > 0 ? `
|
|
459
500
|
<div class="ai-suggestion-header-right">
|
|
501
|
+
${suggestion.reasoning && suggestion.reasoning.length > 0 ? `
|
|
460
502
|
<button class="btn-reasoning-toggle" title="View reasoning" data-suggestion-id="${suggestion.id}" data-reasoning="${encodeURIComponent(JSON.stringify(suggestion.reasoning))}">
|
|
461
503
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M21.33 12.91c.09 1.55-.62 3.04-1.89 3.95l.77 1.49c.23.45.26.98.06 1.45c-.19.47-.58.84-1.06 1l-.79.25a1.69 1.69 0 0 1-1.86-.55L14.44 18c-.89-.15-1.73-.53-2.44-1.1c-.5.15-1 .23-1.5.23c-.88 0-1.76-.27-2.5-.79c-.53.16-1.07.23-1.62.22c-.79.01-1.57-.15-2.3-.45a4.1 4.1 0 0 1-2.43-3.61c-.08-.72.04-1.45.35-2.11c-.29-.75-.32-1.57-.07-2.33C2.3 7.11 3 6.32 3.87 5.82c.58-1.69 2.21-2.82 4-2.7c1.6-1.5 4.05-1.66 5.83-.37c.42-.11.86-.17 1.3-.17c1.36-.03 2.65.57 3.5 1.64c2.04.53 3.5 2.35 3.58 4.47c.05 1.11-.25 2.2-.86 3.13c.07.36.11.72.11 1.09m-5-1.41c.57.07 1.02.5 1.02 1.07a1 1 0 0 1-1 1h-.63c-.32.9-.88 1.69-1.62 2.29c.25.09.51.14.77.21c5.13-.07 4.53-3.2 4.53-3.25a2.59 2.59 0 0 0-2.69-2.49a1 1 0 0 1-1-1a1 1 0 0 1 1-1c1.23.03 2.41.49 3.33 1.3c.05-.29.08-.59.08-.89c-.06-1.24-.62-2.32-2.87-2.53c-1.25-2.96-4.4-1.32-4.4-.4c-.03.23.21.72.25.75a1 1 0 0 1 1 1c0 .55-.45 1-1 1c-.53-.02-1.03-.22-1.43-.56c-.48.31-1.03.5-1.6.56c-.57.05-1.04-.35-1.07-.9a.97.97 0 0 1 .88-1.1c.16-.02.94-.14.94-.77c0-.66.25-1.29.68-1.79c-.92-.25-1.91.08-2.91 1.29C6.75 5 6 5.25 5.45 7.2C4.5 7.67 4 8 3.78 9c1.08-.22 2.19-.13 3.22.25c.5.19.78.75.59 1.29c-.19.52-.77.78-1.29.59c-.73-.32-1.55-.34-2.3-.06c-.32.27-.32.83-.32 1.27c0 .74.37 1.43 1 1.83c.53.27 1.12.41 1.71.4q-.225-.39-.39-.81a1.038 1.038 0 0 1 1.96-.68c.4 1.14 1.42 1.92 2.62 2.05c1.37-.07 2.59-.88 3.19-2.13c.23-1.38 1.34-1.5 2.56-1.5m2 7.47l-.62-1.3l-.71.16l1 1.25zm-4.65-8.61a1 1 0 0 0-.91-1.03c-.71-.04-1.4.2-1.93.67c-.57.58-.87 1.38-.84 2.19a1 1 0 0 0 1 1c.57 0 1-.45 1-1c0-.27.07-.54.23-.76c.12-.1.27-.15.43-.15c.55.03 1.02-.38 1.02-.92"/></svg>
|
|
462
504
|
</button>
|
|
505
|
+
` : ''}
|
|
463
506
|
</div>
|
|
464
|
-
` : ''}
|
|
465
507
|
</div>
|
|
466
508
|
<div class="ai-suggestion-collapsed-content">
|
|
467
509
|
${suggestion.type === 'praise'
|
|
@@ -495,6 +537,10 @@ class FileCommentManager {
|
|
|
495
537
|
<svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>
|
|
496
538
|
Edit
|
|
497
539
|
</button>
|
|
540
|
+
<button class="ai-action ai-action-chat" title="Chat about suggestion" data-suggestion-id="${suggestion.id}" data-file="${this.escapeHtml(suggestion.file || '')}" data-title="${this.escapeHtml(suggestion.title || '')}">
|
|
541
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/></svg>
|
|
542
|
+
Chat
|
|
543
|
+
</button>
|
|
498
544
|
<button class="ai-action ai-action-dismiss">
|
|
499
545
|
<svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>
|
|
500
546
|
Dismiss
|
|
@@ -534,37 +580,19 @@ class FileCommentManager {
|
|
|
534
580
|
*/
|
|
535
581
|
async adoptAISuggestion(zone, suggestion) {
|
|
536
582
|
try {
|
|
537
|
-
//
|
|
538
|
-
|
|
583
|
+
// Use the atomic /adopt endpoint which creates the user comment, sets parent_id
|
|
584
|
+
// linkage, and updates suggestion status to 'adopted' in a single request
|
|
585
|
+
const reviewId = this.prManager?.currentPR?.id;
|
|
586
|
+
const adoptEndpoint = `/api/reviews/${reviewId}/suggestions/${suggestion.id}/adopt`;
|
|
539
587
|
|
|
540
|
-
|
|
541
|
-
const { endpoint, requestBody } = this._getFileCommentEndpoint('create', {
|
|
542
|
-
file: suggestion.file,
|
|
543
|
-
body: formattedBody,
|
|
544
|
-
parent_id: suggestion.id,
|
|
545
|
-
type: suggestion.type,
|
|
546
|
-
title: suggestion.title
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
const createResponse = await fetch(endpoint, {
|
|
588
|
+
const adoptResponse = await fetch(adoptEndpoint, {
|
|
550
589
|
method: 'POST',
|
|
551
|
-
headers: { 'Content-Type': 'application/json' }
|
|
552
|
-
body: JSON.stringify(requestBody)
|
|
590
|
+
headers: { 'Content-Type': 'application/json' }
|
|
553
591
|
});
|
|
554
592
|
|
|
555
|
-
if (!
|
|
556
|
-
|
|
557
|
-
const createResult = await createResponse.json();
|
|
558
|
-
|
|
559
|
-
// Update the AI suggestion status to adopted (mode-aware endpoint)
|
|
560
|
-
const statusEndpoint = this._getSuggestionStatusEndpoint(suggestion.id);
|
|
561
|
-
const statusResponse = await fetch(statusEndpoint, {
|
|
562
|
-
method: 'POST',
|
|
563
|
-
headers: { 'Content-Type': 'application/json' },
|
|
564
|
-
body: JSON.stringify({ status: 'adopted' })
|
|
565
|
-
});
|
|
593
|
+
if (!adoptResponse.ok) throw new Error('Failed to adopt suggestion');
|
|
566
594
|
|
|
567
|
-
|
|
595
|
+
const adoptResult = await adoptResponse.json();
|
|
568
596
|
|
|
569
597
|
// Collapse the AI suggestion card instead of removing it
|
|
570
598
|
const suggestionCard = zone.querySelector(`[data-suggestion-id="${suggestion.id}"]`);
|
|
@@ -577,9 +605,12 @@ class FileCommentManager {
|
|
|
577
605
|
}
|
|
578
606
|
}
|
|
579
607
|
|
|
608
|
+
// Format the comment body with category prefix for display (matches server-side formatting)
|
|
609
|
+
const formattedBody = this.formatAdoptedComment(suggestion.body, suggestion.type);
|
|
610
|
+
|
|
580
611
|
// Display as user comment with formatted body
|
|
581
612
|
const commentData = {
|
|
582
|
-
id:
|
|
613
|
+
id: adoptResult.userCommentId,
|
|
583
614
|
file: suggestion.file,
|
|
584
615
|
body: formattedBody,
|
|
585
616
|
source: 'user',
|
|
@@ -793,34 +824,23 @@ class FileCommentManager {
|
|
|
793
824
|
// Format the edited body with category prefix (matches line-level behavior)
|
|
794
825
|
const formattedBody = this.formatAdoptedComment(editedBody, suggestion.type);
|
|
795
826
|
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
parent_id: suggestion.id,
|
|
801
|
-
type: suggestion.type,
|
|
802
|
-
title: suggestion.title
|
|
803
|
-
});
|
|
827
|
+
// Use the /edit endpoint which atomically creates a user comment with the edited
|
|
828
|
+
// body and sets the suggestion status to 'adopted' with parent_id linkage
|
|
829
|
+
const reviewId = this.prManager?.currentPR?.id;
|
|
830
|
+
const editEndpoint = `/api/reviews/${reviewId}/suggestions/${suggestion.id}/edit`;
|
|
804
831
|
|
|
805
|
-
const
|
|
832
|
+
const editResponse = await fetch(editEndpoint, {
|
|
806
833
|
method: 'POST',
|
|
807
834
|
headers: { 'Content-Type': 'application/json' },
|
|
808
|
-
body: JSON.stringify(
|
|
835
|
+
body: JSON.stringify({
|
|
836
|
+
action: 'adopt_edited',
|
|
837
|
+
editedText: formattedBody
|
|
838
|
+
})
|
|
809
839
|
});
|
|
810
840
|
|
|
811
|
-
if (!
|
|
841
|
+
if (!editResponse.ok) throw new Error('Failed to adopt suggestion with edits');
|
|
812
842
|
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
// Update the AI suggestion status to adopted (mode-aware endpoint)
|
|
816
|
-
const statusEndpoint = this._getSuggestionStatusEndpoint(suggestion.id);
|
|
817
|
-
const statusResponse = await fetch(statusEndpoint, {
|
|
818
|
-
method: 'POST',
|
|
819
|
-
headers: { 'Content-Type': 'application/json' },
|
|
820
|
-
body: JSON.stringify({ status: 'adopted' })
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
if (!statusResponse.ok) throw new Error('Failed to update suggestion status');
|
|
843
|
+
const editResult = await editResponse.json();
|
|
824
844
|
|
|
825
845
|
// Collapse the AI suggestion card instead of removing it
|
|
826
846
|
const suggestionCard = zone.querySelector(`[data-suggestion-id="${suggestion.id}"]`);
|
|
@@ -835,7 +855,7 @@ class FileCommentManager {
|
|
|
835
855
|
|
|
836
856
|
// Display as user comment with formatted body
|
|
837
857
|
const commentData = {
|
|
838
|
-
id:
|
|
858
|
+
id: editResult.userCommentId,
|
|
839
859
|
file: suggestion.file,
|
|
840
860
|
body: formattedBody,
|
|
841
861
|
source: 'user',
|
|
@@ -897,6 +917,8 @@ class FileCommentManager {
|
|
|
897
917
|
const saveBtn = bodyEl.querySelector('.save-edit-btn');
|
|
898
918
|
const cancelBtn = bodyEl.querySelector('.cancel-edit-btn');
|
|
899
919
|
|
|
920
|
+
card.classList.add('editing-mode');
|
|
921
|
+
|
|
900
922
|
textarea.focus();
|
|
901
923
|
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
902
924
|
|
|
@@ -906,6 +928,7 @@ class FileCommentManager {
|
|
|
906
928
|
}
|
|
907
929
|
|
|
908
930
|
const restoreView = () => {
|
|
931
|
+
card.classList.remove('editing-mode');
|
|
909
932
|
const renderedBody = window.renderMarkdown
|
|
910
933
|
? window.renderMarkdown(originalMarkdown)
|
|
911
934
|
: this.escapeHtml(originalMarkdown);
|
|
@@ -962,6 +985,8 @@ class FileCommentManager {
|
|
|
962
985
|
if (!response.ok) throw new Error('Failed to update comment');
|
|
963
986
|
|
|
964
987
|
// Update the display
|
|
988
|
+
const card = bodyEl?.closest('.file-comment-card');
|
|
989
|
+
if (card) card.classList.remove('editing-mode');
|
|
965
990
|
const renderedBody = window.renderMarkdown
|
|
966
991
|
? window.renderMarkdown(newBody)
|
|
967
992
|
: this.escapeHtml(newBody);
|
|
@@ -1042,12 +1067,8 @@ class FileCommentManager {
|
|
|
1042
1067
|
*/
|
|
1043
1068
|
_getSuggestionStatusEndpoint(suggestionId) {
|
|
1044
1069
|
const reviewId = this.prManager?.currentPR?.id;
|
|
1045
|
-
const reviewType = this.prManager?.currentPR?.reviewType;
|
|
1046
|
-
const isLocal = reviewType === 'local';
|
|
1047
1070
|
|
|
1048
|
-
return
|
|
1049
|
-
? `/api/local/${reviewId}/ai-suggestion/${suggestionId}/status`
|
|
1050
|
-
: `/api/ai-suggestion/${suggestionId}/status`;
|
|
1071
|
+
return `/api/reviews/${reviewId}/suggestions/${suggestionId}/status`;
|
|
1051
1072
|
}
|
|
1052
1073
|
|
|
1053
1074
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* File List Merger - Merges diff files with context files for sidebar display.
|
|
4
|
+
*
|
|
5
|
+
* This module provides the core logic for rebuilding the sidebar file list
|
|
6
|
+
* when context files are added to a review. It merges stored diff files with
|
|
7
|
+
* context files, deduplicates by path, and sorts alphabetically.
|
|
8
|
+
*
|
|
9
|
+
* Used by both PR mode (pr.js) and Local mode (local.js).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Merge diff files with context files into a single sorted array.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - Diff files take precedence: context files whose path matches a diff file
|
|
17
|
+
* are excluded (the diff version is kept).
|
|
18
|
+
* - Context files are deduplicated by path, keeping the first occurrence.
|
|
19
|
+
* - The result is sorted alphabetically by file path.
|
|
20
|
+
*
|
|
21
|
+
* @param {Array<{file: string}>} diffFiles - Files from the PR diff
|
|
22
|
+
* @param {Array<{file: string, id: number, label: string, line_start: number, line_end: number}>} contextFiles - Additional context files
|
|
23
|
+
* @returns {Array<{file: string}>} Merged and sorted file list
|
|
24
|
+
*/
|
|
25
|
+
function mergeFileListWithContext(diffFiles, contextFiles) {
|
|
26
|
+
const merged = [...(diffFiles || [])];
|
|
27
|
+
|
|
28
|
+
// Build set of diff file paths so context files don't duplicate them
|
|
29
|
+
const diffPaths = new Set((diffFiles || []).map(f => f.file));
|
|
30
|
+
|
|
31
|
+
// Deduplicate context files by path and skip any that overlap with diff files
|
|
32
|
+
const seenContextPaths = new Set();
|
|
33
|
+
for (const cf of (contextFiles || [])) {
|
|
34
|
+
if (diffPaths.has(cf.file) || seenContextPaths.has(cf.file)) continue;
|
|
35
|
+
seenContextPaths.add(cf.file);
|
|
36
|
+
merged.push({
|
|
37
|
+
file: cf.file,
|
|
38
|
+
contextFile: true,
|
|
39
|
+
contextId: cf.id,
|
|
40
|
+
label: cf.label,
|
|
41
|
+
lineStart: cf.line_start,
|
|
42
|
+
lineEnd: cf.line_end,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Sort by file path (context files interleave naturally)
|
|
47
|
+
merged.sort((a, b) => a.file.localeCompare(b.file));
|
|
48
|
+
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Export for browser usage (attach to window)
|
|
53
|
+
if (typeof window !== 'undefined') {
|
|
54
|
+
window.FileListMerger = {
|
|
55
|
+
mergeFileListWithContext
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Export for Node.js/test usage
|
|
60
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
61
|
+
module.exports = {
|
|
62
|
+
mergeFileListWithContext
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -18,13 +18,27 @@ window.PanelResizer = (function() {
|
|
|
18
18
|
},
|
|
19
19
|
'ai-panel': {
|
|
20
20
|
min: 200,
|
|
21
|
-
max:
|
|
21
|
+
max: null, // dynamic — computed from viewport in getEffectiveMax()
|
|
22
22
|
default: 320,
|
|
23
23
|
storageKey: 'ai-panel-width',
|
|
24
24
|
cssVar: '--ai-panel-width'
|
|
25
25
|
}
|
|
26
|
+
// Note: chat-panel resize is handled by ChatPanel itself (see ChatPanel._bindResizeEvents)
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Compute the effective max width for a panel.
|
|
31
|
+
* For panels with a static max, returns that value.
|
|
32
|
+
* For panels with max: null, computes a dynamic max from the viewport.
|
|
33
|
+
*/
|
|
34
|
+
function getEffectiveMax(panelName) {
|
|
35
|
+
const config = CONFIG[panelName];
|
|
36
|
+
if (!config) return Infinity;
|
|
37
|
+
if (config.max != null) return config.max;
|
|
38
|
+
const sidebarWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width'), 10) || 260;
|
|
39
|
+
return window.innerWidth - sidebarWidth - 100;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
// State
|
|
29
43
|
let isDragging = false;
|
|
30
44
|
let currentPanel = null;
|
|
@@ -59,7 +73,7 @@ window.PanelResizer = (function() {
|
|
|
59
73
|
|
|
60
74
|
if (savedWidth) {
|
|
61
75
|
const width = parseInt(savedWidth, 10);
|
|
62
|
-
if (width >= config.min
|
|
76
|
+
if (width >= config.min) {
|
|
63
77
|
document.documentElement.style.setProperty(config.cssVar, `${width}px`);
|
|
64
78
|
}
|
|
65
79
|
}
|
|
@@ -92,8 +106,8 @@ window.PanelResizer = (function() {
|
|
|
92
106
|
const config = CONFIG[panelName];
|
|
93
107
|
if (!config) return;
|
|
94
108
|
|
|
95
|
-
// Clamp to min/max
|
|
96
|
-
const clampedWidth = Math.max(config.min, Math.min(
|
|
109
|
+
// Clamp to min/max (max may be dynamic)
|
|
110
|
+
const clampedWidth = Math.max(config.min, Math.min(getEffectiveMax(panelName), width));
|
|
97
111
|
|
|
98
112
|
// Apply to CSS
|
|
99
113
|
document.documentElement.style.setProperty(config.cssVar, `${clampedWidth}px`);
|
|
@@ -115,7 +129,9 @@ window.PanelResizer = (function() {
|
|
|
115
129
|
const panelName = handle.dataset.panel;
|
|
116
130
|
const panelEl = panelName === 'sidebar'
|
|
117
131
|
? document.getElementById('files-sidebar')
|
|
118
|
-
:
|
|
132
|
+
: panelName === 'chat-panel'
|
|
133
|
+
? document.querySelector('.chat-panel')
|
|
134
|
+
: document.getElementById('ai-panel');
|
|
119
135
|
|
|
120
136
|
// Don't allow resize if panel is collapsed
|
|
121
137
|
if (panelEl && panelEl.classList.contains('collapsed')) {
|
|
@@ -181,6 +197,9 @@ window.PanelResizer = (function() {
|
|
|
181
197
|
}
|
|
182
198
|
document.body.classList.remove('resizing');
|
|
183
199
|
|
|
200
|
+
// Notify PanelGroup so --right-panel-group-width stays in sync
|
|
201
|
+
window.panelGroup?._updateRightPanelGroupWidth();
|
|
202
|
+
|
|
184
203
|
isDragging = false;
|
|
185
204
|
currentPanel = null;
|
|
186
205
|
startX = 0;
|
|
@@ -199,7 +218,7 @@ window.PanelResizer = (function() {
|
|
|
199
218
|
const saved = localStorage.getItem(config.storageKey);
|
|
200
219
|
if (saved) {
|
|
201
220
|
const width = parseInt(saved, 10);
|
|
202
|
-
if (width >= config.min
|
|
221
|
+
if (width >= config.min) {
|
|
203
222
|
return width;
|
|
204
223
|
}
|
|
205
224
|
}
|