@in-the-loop-labs/pair-review 1.6.2 → 2.0.0

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 (62) hide show
  1. package/README.md +77 -4
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/skills/review-requests/SKILL.md +4 -1
  5. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  6. package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
  7. package/public/css/pr.css +1930 -114
  8. package/public/js/CONVENTIONS.md +16 -0
  9. package/public/js/components/AIPanel.js +66 -0
  10. package/public/js/components/AnalysisConfigModal.js +2 -2
  11. package/public/js/components/ChatPanel.js +2952 -0
  12. package/public/js/components/CouncilProgressModal.js +12 -16
  13. package/public/js/components/KeyboardShortcuts.js +3 -0
  14. package/public/js/components/PanelGroup.js +723 -0
  15. package/public/js/components/PreviewModal.js +3 -8
  16. package/public/js/index.js +8 -0
  17. package/public/js/local.js +17 -615
  18. package/public/js/modules/analysis-history.js +19 -68
  19. package/public/js/modules/comment-manager.js +57 -19
  20. package/public/js/modules/diff-context.js +176 -0
  21. package/public/js/modules/diff-renderer.js +30 -0
  22. package/public/js/modules/file-comment-manager.js +126 -105
  23. package/public/js/modules/file-list-merger.js +64 -0
  24. package/public/js/modules/panel-resizer.js +25 -6
  25. package/public/js/modules/suggestion-manager.js +40 -125
  26. package/public/js/pr.js +964 -159
  27. package/public/js/repo-settings.js +36 -6
  28. package/public/js/utils/category-emoji.js +44 -0
  29. package/public/js/utils/time.js +32 -0
  30. package/public/local.html +107 -70
  31. package/public/pr.html +107 -70
  32. package/public/repo-settings.html +32 -0
  33. package/src/ai/analyzer.js +5 -1
  34. package/src/ai/copilot-provider.js +39 -9
  35. package/src/ai/cursor-agent-provider.js +36 -7
  36. package/src/ai/gemini-provider.js +17 -4
  37. package/src/ai/prompts/config.js +7 -1
  38. package/src/ai/provider-availability.js +1 -1
  39. package/src/ai/provider.js +25 -37
  40. package/src/chat/CONVENTIONS.md +18 -0
  41. package/src/chat/pi-bridge.js +491 -0
  42. package/src/chat/prompt-builder.js +262 -0
  43. package/src/chat/session-manager.js +619 -0
  44. package/src/config.js +14 -0
  45. package/src/database.js +322 -15
  46. package/src/main.js +4 -17
  47. package/src/routes/analyses.js +721 -0
  48. package/src/routes/chat.js +655 -0
  49. package/src/routes/config.js +29 -8
  50. package/src/routes/context-files.js +223 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +410 -52
  54. package/src/routes/reviews.js +1035 -0
  55. package/src/routes/shared.js +4 -29
  56. package/src/server.js +34 -12
  57. package/src/sse/review-events.js +46 -0
  58. package/src/utils/auto-context.js +88 -0
  59. package/src/utils/category-emoji.js +33 -0
  60. package/src/utils/diff-file-list.js +57 -0
  61. package/src/routes/analysis.js +0 -1600
  62. package/src/routes/comments.js +0 -534
@@ -0,0 +1,262 @@
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 load the pair-review-api skill ${skillRef} for endpoint details. 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
+ // Instructions
76
+ sections.push(
77
+ 'Answer questions about this review, the code changes, and any AI suggestions. ' +
78
+ 'Be concise and helpful. Use markdown formatting in your responses.'
79
+ );
80
+
81
+ // Custom chat instructions from repo settings
82
+ if (chatInstructions) {
83
+ sections.push('## Custom Instructions\n\nThe following instructions take precedence over previous guidance.\n\n' + chatInstructions);
84
+ }
85
+
86
+ const prompt = sections.join('\n\n');
87
+ logger.debug(`Chat system prompt built: ${prompt.length} chars`);
88
+ return prompt;
89
+ }
90
+
91
+ /**
92
+ * Build the review context section of the prompt.
93
+ * Includes what is being reviewed and how to view the changes.
94
+ * @param {Object} review - Review metadata
95
+ * @param {Object} [prData] - PR data with base_sha/head_sha (for PR reviews)
96
+ * @returns {string}
97
+ */
98
+ function buildReviewContext(review, prData) {
99
+ if (!review) {
100
+ return 'Review context: unknown.';
101
+ }
102
+
103
+ const lines = [];
104
+
105
+ if (review.review_type === 'local' || review.local_path) {
106
+ const name = review.name || review.local_path || 'unknown';
107
+ lines.push(`## Review Context`);
108
+ lines.push(`This is a local code review for: ${name}`);
109
+ lines.push('');
110
+ lines.push('## Viewing Code Changes');
111
+ lines.push('The changes under review are **unstaged and untracked local changes**. Staged changes (`git diff --cached`) are treated as already reviewed.');
112
+ lines.push('To see the diff under review: `git diff`');
113
+ lines.push('Do NOT use `git diff HEAD~1` or `git log` — those show committed history, not the changes under review.');
114
+ } else {
115
+ const parts = [];
116
+ if (review.repository) {
117
+ parts.push(review.repository);
118
+ }
119
+ if (review.pr_number) {
120
+ parts.push(`PR #${review.pr_number}`);
121
+ }
122
+ if (parts.length === 0) {
123
+ return 'Review context: unknown.';
124
+ }
125
+
126
+ lines.push('## Review Context');
127
+ lines.push(`This is a review of ${parts.join(' ')}.`);
128
+
129
+ if (prData && prData.base_sha && prData.head_sha) {
130
+ lines.push('');
131
+ lines.push('## Viewing Code Changes');
132
+ 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)}\`.`);
133
+ lines.push(`To see the full diff: \`git diff ${prData.base_sha}...${prData.head_sha}\``);
134
+ lines.push('Do NOT use `git diff HEAD~1` or `git diff` without arguments — those do not show the PR changes.');
135
+ }
136
+ }
137
+
138
+ return lines.join('\n');
139
+ }
140
+
141
+ /**
142
+ * Safely parse a reasoning field from the database.
143
+ * Handles null, pre-parsed objects/arrays, valid JSON strings, and malformed JSON.
144
+ * @param {*} reasoning - Raw reasoning value from DB
145
+ * @returns {*} Parsed reasoning or null on failure
146
+ */
147
+ function parseReasoning(reasoning) {
148
+ if (!reasoning) return null;
149
+ if (typeof reasoning !== 'string') return reasoning;
150
+ try { return JSON.parse(reasoning); } catch { return null; }
151
+ }
152
+
153
+ /**
154
+ * Format a suggestion DB row into a lean context object for the chat agent.
155
+ * @param {Object} s - Suggestion row from the database
156
+ * @returns {Object} Formatted suggestion
157
+ */
158
+ function formatSuggestionForContext(s) {
159
+ return {
160
+ id: s.id,
161
+ file: s.file,
162
+ line_start: s.line_start,
163
+ line_end: s.line_end,
164
+ type: s.type,
165
+ title: s.title,
166
+ body: s.body,
167
+ reasoning: parseReasoning(s.reasoning),
168
+ status: s.status,
169
+ ai_confidence: s.ai_confidence,
170
+ is_file_level: s.is_file_level
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Format analysis run metadata into a compact summary for the chat agent.
176
+ * Includes run configuration, model info, timing, and the summary text.
177
+ * @param {Object} run - Analysis run record from the database
178
+ * @returns {string} Formatted run metadata section
179
+ */
180
+ function formatAnalysisRunContext(run) {
181
+ const lines = ['## Analysis Run Metadata'];
182
+
183
+ lines.push(`- **Run ID**: ${run.id}`);
184
+ if (run.provider) lines.push(`- **Provider**: ${run.provider}`);
185
+ if (run.model) lines.push(`- **Model**: ${run.model}`);
186
+ lines.push(`- **Status**: ${run.status}`);
187
+ if (run.started_at) lines.push(`- **Started**: ${run.started_at}`);
188
+ if (run.completed_at) lines.push(`- **Completed**: ${run.completed_at}`);
189
+ if (run.config_type) lines.push(`- **Config type**: ${run.config_type}`);
190
+ if (run.parent_run_id) lines.push(`- **Parent run (council)**: ${run.parent_run_id}`);
191
+ if (run.head_sha) lines.push(`- **Head SHA**: ${run.head_sha}`);
192
+ if (run.total_suggestions != null) lines.push(`- **Total suggestions**: ${run.total_suggestions}`);
193
+ if (run.files_analyzed != null) lines.push(`- **Files analyzed**: ${run.files_analyzed}`);
194
+
195
+ // Parse and display levels config if present
196
+ if (run.levels_config) {
197
+ try {
198
+ const levels = typeof run.levels_config === 'string'
199
+ ? JSON.parse(run.levels_config)
200
+ : run.levels_config;
201
+ lines.push(`- **Levels config**: ${JSON.stringify(levels)}`);
202
+ } catch {
203
+ // Malformed JSON — skip
204
+ }
205
+ }
206
+
207
+ // Include the summary text if available
208
+ if (run.summary) {
209
+ lines.push('');
210
+ lines.push('### Analysis Summary');
211
+ lines.push(run.summary);
212
+ }
213
+
214
+ if (run.repo_instructions) {
215
+ lines.push('');
216
+ lines.push('### Repository Instructions');
217
+ lines.push(run.repo_instructions);
218
+ }
219
+ if (run.request_instructions) {
220
+ lines.push('');
221
+ lines.push('### Custom Instructions (this run)');
222
+ lines.push(run.request_instructions);
223
+ }
224
+
225
+ return lines.join('\n');
226
+ }
227
+
228
+ /**
229
+ * Build initial context to prepend to the first user message.
230
+ * Contains analysis run metadata and all AI suggestions from the latest run.
231
+ *
232
+ * @param {Object} options
233
+ * @param {Array} options.suggestions - All AI suggestions from the latest run
234
+ * @param {Object} [options.analysisRun] - Analysis run record with metadata (provider, model, summary, etc.)
235
+ * @returns {string|null} Context text to prepend to first message, or null if no context
236
+ */
237
+ function buildInitialContext({ suggestions, analysisRun }) {
238
+ const sections = [];
239
+
240
+ // Analysis run metadata and summary (if available)
241
+ if (analysisRun) {
242
+ sections.push(formatAnalysisRunContext(analysisRun));
243
+ }
244
+
245
+ if (suggestions && suggestions.length > 0) {
246
+ const formatted = suggestions.map(formatSuggestionForContext);
247
+
248
+ const label = formatted.length === 1 ? '1 AI suggestion' : `${formatted.length} AI suggestions`;
249
+ sections.push(
250
+ `Here ${formatted.length === 1 ? 'is' : 'are all'} ${label} from the latest analysis run:\n` +
251
+ '```json\n' + JSON.stringify(formatted, null, 2) + '\n```'
252
+ );
253
+ }
254
+
255
+ if (sections.length === 0) {
256
+ return null;
257
+ }
258
+
259
+ return sections.join('\n\n');
260
+ }
261
+
262
+ module.exports = { buildChatPrompt, buildInitialContext, formatAnalysisRunContext };