@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.
Files changed (63) 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 +1962 -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 +2955 -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 +103 -20
  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 +1009 -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 +45 -11
  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 +272 -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 +274 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +424 -58
  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-annotator.js +75 -1
  61. package/src/utils/diff-file-list.js +57 -0
  62. package/src/routes/analysis.js +0 -1600
  63. 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 };