@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Chat Prompt Builder
|
|
4
|
+
*
|
|
5
|
+
* Builds system prompts and initial context for chat sessions.
|
|
6
|
+
* The system prompt is lean (role + review context + instructions).
|
|
7
|
+
* Suggestion context is delivered via the first user message, not the system prompt.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a lean system prompt for chat sessions.
|
|
14
|
+
* Contains only role, review context, and behavioral instructions.
|
|
15
|
+
* The port is NOT included here because it can change between server restarts;
|
|
16
|
+
* it is injected once per session via the initial context instead.
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {Object} options.review - Review metadata {id, pr_number, repository, review_type, local_path, name}
|
|
19
|
+
* @param {Object} [options.prData] - PR data with base_sha/head_sha (for PR reviews)
|
|
20
|
+
* @param {string} [options.skillPath] - Absolute path to the pair-review-api SKILL.md file
|
|
21
|
+
* @param {string} [options.chatInstructions] - Custom instructions from repo settings to append to system prompt
|
|
22
|
+
* @returns {string} System prompt for the chat agent
|
|
23
|
+
*/
|
|
24
|
+
function buildChatPrompt({ review, prData, skillPath, chatInstructions }) {
|
|
25
|
+
const sections = [];
|
|
26
|
+
|
|
27
|
+
// Role
|
|
28
|
+
sections.push(
|
|
29
|
+
'Role: Expert software engineer.\n\n' +
|
|
30
|
+
'Rules:\n' +
|
|
31
|
+
'- Priority: Accuracy and helpfulness.\n' +
|
|
32
|
+
'- Syntax: Short, blunt, staccato. Zero filler words.\n' +
|
|
33
|
+
'- Tone: Hyper-logical\n\n' +
|
|
34
|
+
'You are a code review assistant helping within the chat feature of an app named pair-review. You have access to the repository and can explore it using shell commands. Do not modify any files. Do not access pair-review\'s SQLite database directly; use the API.'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Review context
|
|
38
|
+
sections.push(buildReviewContext(review, prData));
|
|
39
|
+
|
|
40
|
+
// Domain model — ambient conceptual grounding for every turn
|
|
41
|
+
const domainLines = [
|
|
42
|
+
'## pair-review app domain model',
|
|
43
|
+
'',
|
|
44
|
+
'- **Comments** are human-curated review findings (created by the reviewer).',
|
|
45
|
+
'- **Suggestions** are AI-generated findings from analysis runs.',
|
|
46
|
+
'- **Workflow**: AI generates suggestions → reviewer triages (adopt, edit, or dismiss) → adopted suggestions become comments.',
|
|
47
|
+
'- **Analysis runs** are the process that produces suggestions. Each run has a provider, model, tier, and status.',
|
|
48
|
+
'- **Review ID** is a stable integer identifying this review session, used in all API calls.'
|
|
49
|
+
];
|
|
50
|
+
if (review && review.id) {
|
|
51
|
+
domainLines.push(`- The review ID for this session is: ${review.id} (e.g. \`/api/reviews/${review.id}/comments\`).`);
|
|
52
|
+
}
|
|
53
|
+
sections.push(domainLines.join('\n'));
|
|
54
|
+
|
|
55
|
+
// API capability — MUST load the skill for endpoint details
|
|
56
|
+
const skillRef = skillPath
|
|
57
|
+
? `(\`${skillPath}\`)`
|
|
58
|
+
: '(`.pi/skills/pair-review-api/SKILL.md`)';
|
|
59
|
+
sections.push(
|
|
60
|
+
`You MUST read the pair-review-api skill ${skillRef} for endpoint details using the Read tool. With it you can create, update, and delete review comments, adopt or dismiss AI suggestions, and trigger new analyses via curl.\n` +
|
|
61
|
+
'IMPORTANT: Do NOT mention that you are reading a skill file, loading API documentation, or consulting reference material. Just use the API naturally as if you already know it.'
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// File reference syntax and context files
|
|
65
|
+
sections.push(
|
|
66
|
+
'## File references\n\n' +
|
|
67
|
+
'When referencing source files, use the syntax [[file:path/to/file.js]] or ' +
|
|
68
|
+
'[[file:path/to/file.js:42]] (with line number) or [[file:path/to/file.js:42-50]] (with line range). ' +
|
|
69
|
+
'These become clickable links in the UI. Do NOT use backtick code spans for file references you want to be clickable.\n\n' +
|
|
70
|
+
'Files in the diff can be referenced freely. Files outside the diff can also be referenced; ' +
|
|
71
|
+
'to make them visible in the diff panel, add them as context files via the API (see skill). ' +
|
|
72
|
+
'Add context files judiciously — only when directly relevant, with focused line ranges.'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Tool usage discipline — avoid unnecessary Task delegation
|
|
76
|
+
sections.push(
|
|
77
|
+
'## Tool usage\n\n' +
|
|
78
|
+
'Prefer answering from context you already have (the diff, suggestions, and prior conversation). ' +
|
|
79
|
+
'For simple lookups — reading a file, searching for a symbol, running a git command — use the basic tools directly. ' +
|
|
80
|
+
'Only use the Task tool for genuinely large, multi-step operations that would consume significant context ' +
|
|
81
|
+
'(e.g., tracing a complex call chain across many files, or broad codebase exploration). ' +
|
|
82
|
+
'Most chat questions should not require a Task.'
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Instructions
|
|
86
|
+
sections.push(
|
|
87
|
+
'Answer questions about this review, the code changes, and any AI suggestions. ' +
|
|
88
|
+
'Be concise and helpful. Use markdown formatting in your responses.'
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Custom chat instructions from repo settings
|
|
92
|
+
if (chatInstructions) {
|
|
93
|
+
sections.push('## Custom Instructions\n\nThe following instructions take precedence over previous guidance.\n\n' + chatInstructions);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const prompt = sections.join('\n\n');
|
|
97
|
+
logger.debug(`Chat system prompt built: ${prompt.length} chars`);
|
|
98
|
+
return prompt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the review context section of the prompt.
|
|
103
|
+
* Includes what is being reviewed and how to view the changes.
|
|
104
|
+
* @param {Object} review - Review metadata
|
|
105
|
+
* @param {Object} [prData] - PR data with base_sha/head_sha (for PR reviews)
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
function buildReviewContext(review, prData) {
|
|
109
|
+
if (!review) {
|
|
110
|
+
return 'Review context: unknown.';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const lines = [];
|
|
114
|
+
|
|
115
|
+
if (review.review_type === 'local' || review.local_path) {
|
|
116
|
+
const name = review.name || review.local_path || 'unknown';
|
|
117
|
+
lines.push(`## Review Context`);
|
|
118
|
+
lines.push(`This is a local code review for: ${name}`);
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('## Viewing Code Changes');
|
|
121
|
+
lines.push('The changes under review are **unstaged and untracked local changes**. Staged changes (`git diff --cached`) are treated as already reviewed.');
|
|
122
|
+
lines.push('To see the diff under review: `git diff`');
|
|
123
|
+
lines.push('Do NOT use `git diff HEAD~1` or `git log` — those show committed history, not the changes under review.');
|
|
124
|
+
} else {
|
|
125
|
+
const parts = [];
|
|
126
|
+
if (review.repository) {
|
|
127
|
+
parts.push(review.repository);
|
|
128
|
+
}
|
|
129
|
+
if (review.pr_number) {
|
|
130
|
+
parts.push(`PR #${review.pr_number}`);
|
|
131
|
+
}
|
|
132
|
+
if (parts.length === 0) {
|
|
133
|
+
return 'Review context: unknown.';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lines.push('## Review Context');
|
|
137
|
+
lines.push(`This is a review of ${parts.join(' ')}.`);
|
|
138
|
+
|
|
139
|
+
if (prData && prData.base_sha && prData.head_sha) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push('## Viewing Code Changes');
|
|
142
|
+
lines.push(`The changes under review are the diff between base commit \`${prData.base_sha.substring(0, 8)}\` and head commit \`${prData.head_sha.substring(0, 8)}\`.`);
|
|
143
|
+
lines.push(`To see the full diff: \`git diff ${prData.base_sha}...${prData.head_sha}\``);
|
|
144
|
+
lines.push('Do NOT use `git diff HEAD~1` or `git diff` without arguments — those do not show the PR changes.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Safely parse a reasoning field from the database.
|
|
153
|
+
* Handles null, pre-parsed objects/arrays, valid JSON strings, and malformed JSON.
|
|
154
|
+
* @param {*} reasoning - Raw reasoning value from DB
|
|
155
|
+
* @returns {*} Parsed reasoning or null on failure
|
|
156
|
+
*/
|
|
157
|
+
function parseReasoning(reasoning) {
|
|
158
|
+
if (!reasoning) return null;
|
|
159
|
+
if (typeof reasoning !== 'string') return reasoning;
|
|
160
|
+
try { return JSON.parse(reasoning); } catch { return null; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a suggestion DB row into a lean context object for the chat agent.
|
|
165
|
+
* @param {Object} s - Suggestion row from the database
|
|
166
|
+
* @returns {Object} Formatted suggestion
|
|
167
|
+
*/
|
|
168
|
+
function formatSuggestionForContext(s) {
|
|
169
|
+
return {
|
|
170
|
+
id: s.id,
|
|
171
|
+
file: s.file,
|
|
172
|
+
line_start: s.line_start,
|
|
173
|
+
line_end: s.line_end,
|
|
174
|
+
type: s.type,
|
|
175
|
+
title: s.title,
|
|
176
|
+
body: s.body,
|
|
177
|
+
reasoning: parseReasoning(s.reasoning),
|
|
178
|
+
status: s.status,
|
|
179
|
+
ai_confidence: s.ai_confidence,
|
|
180
|
+
is_file_level: s.is_file_level
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Format analysis run metadata into a compact summary for the chat agent.
|
|
186
|
+
* Includes run configuration, model info, timing, and the summary text.
|
|
187
|
+
* @param {Object} run - Analysis run record from the database
|
|
188
|
+
* @returns {string} Formatted run metadata section
|
|
189
|
+
*/
|
|
190
|
+
function formatAnalysisRunContext(run) {
|
|
191
|
+
const lines = ['## Analysis Run Metadata'];
|
|
192
|
+
|
|
193
|
+
lines.push(`- **Run ID**: ${run.id}`);
|
|
194
|
+
if (run.provider) lines.push(`- **Provider**: ${run.provider}`);
|
|
195
|
+
if (run.model) lines.push(`- **Model**: ${run.model}`);
|
|
196
|
+
lines.push(`- **Status**: ${run.status}`);
|
|
197
|
+
if (run.started_at) lines.push(`- **Started**: ${run.started_at}`);
|
|
198
|
+
if (run.completed_at) lines.push(`- **Completed**: ${run.completed_at}`);
|
|
199
|
+
if (run.config_type) lines.push(`- **Config type**: ${run.config_type}`);
|
|
200
|
+
if (run.parent_run_id) lines.push(`- **Parent run (council)**: ${run.parent_run_id}`);
|
|
201
|
+
if (run.head_sha) lines.push(`- **Head SHA**: ${run.head_sha}`);
|
|
202
|
+
if (run.total_suggestions != null) lines.push(`- **Total suggestions**: ${run.total_suggestions}`);
|
|
203
|
+
if (run.files_analyzed != null) lines.push(`- **Files analyzed**: ${run.files_analyzed}`);
|
|
204
|
+
|
|
205
|
+
// Parse and display levels config if present
|
|
206
|
+
if (run.levels_config) {
|
|
207
|
+
try {
|
|
208
|
+
const levels = typeof run.levels_config === 'string'
|
|
209
|
+
? JSON.parse(run.levels_config)
|
|
210
|
+
: run.levels_config;
|
|
211
|
+
lines.push(`- **Levels config**: ${JSON.stringify(levels)}`);
|
|
212
|
+
} catch {
|
|
213
|
+
// Malformed JSON — skip
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Include the summary text if available
|
|
218
|
+
if (run.summary) {
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push('### Analysis Summary');
|
|
221
|
+
lines.push(run.summary);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (run.repo_instructions) {
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push('### Repository Instructions');
|
|
227
|
+
lines.push(run.repo_instructions);
|
|
228
|
+
}
|
|
229
|
+
if (run.request_instructions) {
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push('### Custom Instructions (this run)');
|
|
232
|
+
lines.push(run.request_instructions);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Build initial context to prepend to the first user message.
|
|
240
|
+
* Contains analysis run metadata and all AI suggestions from the latest run.
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} options
|
|
243
|
+
* @param {Array} options.suggestions - All AI suggestions from the latest run
|
|
244
|
+
* @param {Object} [options.analysisRun] - Analysis run record with metadata (provider, model, summary, etc.)
|
|
245
|
+
* @returns {string|null} Context text to prepend to first message, or null if no context
|
|
246
|
+
*/
|
|
247
|
+
function buildInitialContext({ suggestions, analysisRun }) {
|
|
248
|
+
const sections = [];
|
|
249
|
+
|
|
250
|
+
// Analysis run metadata and summary (if available)
|
|
251
|
+
if (analysisRun) {
|
|
252
|
+
sections.push(formatAnalysisRunContext(analysisRun));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (suggestions && suggestions.length > 0) {
|
|
256
|
+
const formatted = suggestions.map(formatSuggestionForContext);
|
|
257
|
+
|
|
258
|
+
const label = formatted.length === 1 ? '1 AI suggestion' : `${formatted.length} AI suggestions`;
|
|
259
|
+
sections.push(
|
|
260
|
+
`Here ${formatted.length === 1 ? 'is' : 'are all'} ${label} from the latest analysis run:\n` +
|
|
261
|
+
'```json\n' + JSON.stringify(formatted, null, 2) + '\n```'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (sections.length === 0) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return sections.join('\n\n');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { buildChatPrompt, buildInitialContext, formatAnalysisRunContext };
|