@in-the-loop-labs/pair-review 2.1.0 → 2.2.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.
- package/README.md +5 -0
- package/package.json +1 -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 +9 -0
- package/plugin-code-critic/skills/analyze/references/level1-fast.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level1-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level2-fast.md +9 -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 +9 -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 +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
- package/public/css/pr.css +10 -0
- package/public/js/components/ChatPanel.js +1 -1
- package/public/js/components/ReviewModal.js +65 -2
- package/src/ai/analyzer.js +5 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +9 -0
- package/src/ai/prompts/baseline/consolidation/fast.js +9 -0
- package/src/ai/prompts/baseline/consolidation/thorough.js +9 -0
- package/src/ai/prompts/baseline/level1/balanced.js +9 -0
- package/src/ai/prompts/baseline/level1/fast.js +9 -0
- package/src/ai/prompts/baseline/level1/thorough.js +9 -0
- package/src/ai/prompts/baseline/level2/balanced.js +9 -0
- package/src/ai/prompts/baseline/level2/fast.js +9 -0
- package/src/ai/prompts/baseline/level2/thorough.js +9 -0
- package/src/ai/prompts/baseline/level3/balanced.js +9 -0
- package/src/ai/prompts/baseline/level3/fast.js +9 -0
- package/src/ai/prompts/baseline/level3/thorough.js +9 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +9 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +9 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +9 -0
- package/src/ai/prompts/shared/output-schema.js +10 -1
- package/src/chat/prompt-builder.js +6 -1
- package/src/config.js +2 -1
- package/src/database.js +5 -1
- package/src/github/parser.js +29 -2
- package/src/main.js +48 -166
- package/src/protocol-handler.js +122 -0
- package/src/routes/config.js +2 -1
package/README.md
CHANGED
|
@@ -195,6 +195,9 @@ pair-review --local [path]
|
|
|
195
195
|
| `-h`, `--help` | Show help message with full CLI documentation |
|
|
196
196
|
| `-l`, `--local [path]` | Review local uncommitted changes. Optional path defaults to current directory |
|
|
197
197
|
| `--model <name>` | Override the AI model for any provider. Model availability depends on provider configuration. |
|
|
198
|
+
| `--register` | Register `pair-review://` URL scheme handler (macOS only) |
|
|
199
|
+
| `--unregister` | Unregister `pair-review://` URL scheme handler (macOS only) |
|
|
200
|
+
| `--command <cmd>` | Custom CLI command for `--register` (default: `npx @in-the-loop-labs/pair-review`) |
|
|
198
201
|
| `-v`, `--version` | Show version number |
|
|
199
202
|
|
|
200
203
|
### Examples
|
|
@@ -204,6 +207,8 @@ pair-review 123 # Review PR #123 in current repo
|
|
|
204
207
|
pair-review https://github.com/owner/repo/pull/456
|
|
205
208
|
pair-review --local # Review uncommitted local changes
|
|
206
209
|
pair-review 123 --ai # Auto-run AI analysis
|
|
210
|
+
pair-review --register # Register pair-review:// URL scheme (macOS)
|
|
211
|
+
pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
207
212
|
```
|
|
208
213
|
|
|
209
214
|
## Configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -92,6 +92,15 @@ Do NOT modify files or run write commands. Analyze and report only.
|
|
|
92
92
|
"summary": "Brief summary of findings"
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
### GitHub Suggestion Syntax
|
|
96
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
97
|
+
|
|
98
|
+
```suggestion
|
|
99
|
+
replacement content here
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
103
|
+
|
|
95
104
|
## Line Number Reference (old_or_new field)
|
|
96
105
|
The "old_or_new" field indicates which line number column to use:
|
|
97
106
|
- **"NEW"** (default): Use the NEW column number for:
|
|
@@ -78,6 +78,15 @@ Annotated diff tool, `cat -n`, ls, find, grep. Do NOT modify files.
|
|
|
78
78
|
"summary": "Brief summary"
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
### GitHub Suggestion Syntax
|
|
82
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
83
|
+
|
|
84
|
+
```suggestion
|
|
85
|
+
replacement content here
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
89
|
+
|
|
81
90
|
## Line Numbers (old_or_new)
|
|
82
91
|
- "NEW" (default): added [+] and context lines
|
|
83
92
|
- "OLD": deleted [-] lines only
|
|
@@ -138,6 +138,15 @@ Output JSON with this structure:
|
|
|
138
138
|
"summary": "Brief summary of findings"
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
### GitHub Suggestion Syntax
|
|
142
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
143
|
+
|
|
144
|
+
```suggestion
|
|
145
|
+
replacement content here
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
149
|
+
|
|
141
150
|
## Line Number Reference (old_or_new field)
|
|
142
151
|
The "old_or_new" field indicates which line number column to use:
|
|
143
152
|
- **"NEW"** (default): Use the NEW column number. This is correct for:
|
|
@@ -103,6 +103,15 @@ Note: You may use parallel read-only Tasks to examine multiple files simultaneou
|
|
|
103
103
|
"summary": "Brief summary of file context findings"
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
### GitHub Suggestion Syntax
|
|
107
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
108
|
+
|
|
109
|
+
```suggestion
|
|
110
|
+
replacement content here
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
114
|
+
|
|
106
115
|
## Line Number Reference (old_or_new field)
|
|
107
116
|
- **"NEW"** (default): For ADDED [+] lines and CONTEXT lines
|
|
108
117
|
- **"OLD"**: ONLY for DELETED [-] lines
|
|
@@ -87,6 +87,15 @@ Annotated diff tool (preferred), `cat -n <file>`, ls, find, grep. Do NOT modify
|
|
|
87
87
|
"summary": "Brief summary"
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
### GitHub Suggestion Syntax
|
|
91
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
92
|
+
|
|
93
|
+
```suggestion
|
|
94
|
+
replacement content here
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
98
|
+
|
|
90
99
|
## Line Numbers
|
|
91
100
|
"NEW" (default): added [+] and context lines. "OLD": only deleted [-] lines.
|
|
92
101
|
|
|
@@ -177,6 +177,15 @@ Output JSON with this structure:
|
|
|
177
177
|
"summary": "Brief summary of file context findings"
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
### GitHub Suggestion Syntax
|
|
181
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
182
|
+
|
|
183
|
+
```suggestion
|
|
184
|
+
replacement content here
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
188
|
+
|
|
180
189
|
## old_or_new Field Reference
|
|
181
190
|
Use "NEW" (the default) for added lines [+] and context lines. Use "OLD" only for deleted lines [-]. When uncertain, use "NEW".
|
|
182
191
|
|
|
@@ -140,6 +140,15 @@ Output JSON with this structure:
|
|
|
140
140
|
"summary": "Brief summary of how these changes connect to and impact the codebase"
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
### GitHub Suggestion Syntax
|
|
144
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
145
|
+
|
|
146
|
+
```suggestion
|
|
147
|
+
replacement content here
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
151
|
+
|
|
143
152
|
## Line Number Reference (old_or_new field)
|
|
144
153
|
The "old_or_new" field indicates which line number column to use:
|
|
145
154
|
- **"NEW"** (default): Use the NEW column number. This is correct for:
|
|
@@ -110,6 +110,15 @@ Output JSON with this structure:
|
|
|
110
110
|
"summary": "Brief summary of how these changes connect to and impact the codebase"
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
### GitHub Suggestion Syntax
|
|
114
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
115
|
+
|
|
116
|
+
```suggestion
|
|
117
|
+
replacement content here
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
121
|
+
|
|
113
122
|
## Line Numbers (old_or_new)
|
|
114
123
|
- **"NEW"** (default): For added lines [+] and context lines
|
|
115
124
|
- **"OLD"**: Only for deleted lines [-]
|
|
@@ -251,6 +251,15 @@ Output JSON with this structure:
|
|
|
251
251
|
"summary": "Brief summary of how these changes connect to and impact the codebase"
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
### GitHub Suggestion Syntax
|
|
255
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
256
|
+
|
|
257
|
+
```suggestion
|
|
258
|
+
replacement content here
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
262
|
+
|
|
254
263
|
## Line Number Reference (old_or_new field)
|
|
255
264
|
The "old_or_new" field indicates which line number column to use:
|
|
256
265
|
- **"NEW"** (default): Use the NEW column number. This is correct for:
|
|
@@ -120,6 +120,15 @@ Output JSON with this structure:
|
|
|
120
120
|
"summary": "Brief summary of the key findings and their significance to the reviewer. Focus on WHAT was found, not HOW it was found. Do NOT mention 'orchestration', 'levels', 'merged from Level 1/2/3' etc. Write as if a single reviewer produced this analysis."
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
### GitHub Suggestion Syntax
|
|
124
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
125
|
+
|
|
126
|
+
```suggestion
|
|
127
|
+
replacement content here
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
131
|
+
|
|
123
132
|
## Line Number Reference (old_or_new field)
|
|
124
133
|
The "old_or_new" field indicates which line number column to use:
|
|
125
134
|
- **"NEW"** (default): Correct for ADDED lines and CONTEXT lines (unchanged lines in both versions)
|
|
@@ -97,6 +97,15 @@ Use "Consider...", "Worth noting..." - guidance not mandates.
|
|
|
97
97
|
"summary": "Key findings as if from single reviewer (no mention of levels/orchestration)"
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
### GitHub Suggestion Syntax
|
|
101
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
102
|
+
|
|
103
|
+
```suggestion
|
|
104
|
+
replacement content here
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
108
|
+
|
|
100
109
|
## old_or_new
|
|
101
110
|
"NEW" (default): added [+] and context lines. "OLD": deleted [-] only. Preserve from input.
|
|
102
111
|
|
|
@@ -250,6 +250,15 @@ Output JSON with this structure:
|
|
|
250
250
|
"summary": "Brief summary of the key findings and their significance to the reviewer. Focus on WHAT was found, not HOW it was found. Do NOT mention 'orchestration', 'levels', 'merged from Level 1/2/3' etc. Write as if a single reviewer produced this analysis."
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
### GitHub Suggestion Syntax
|
|
254
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
255
|
+
|
|
256
|
+
```suggestion
|
|
257
|
+
replacement content here
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
261
|
+
|
|
253
262
|
## Line Number Reference (old_or_new field)
|
|
254
263
|
The "old_or_new" field indicates which line number column to use:
|
|
255
264
|
- **"NEW"** (default): Use the NEW column number. This is correct for:
|
package/public/css/pr.css
CHANGED
|
@@ -4846,6 +4846,16 @@ tr.line-range-start .d2h-code-line-ctn {
|
|
|
4846
4846
|
opacity: 0.7;
|
|
4847
4847
|
}
|
|
4848
4848
|
|
|
4849
|
+
/* Assisted-by footer toggle (extends .remember-toggle) */
|
|
4850
|
+
.assisted-by-toggle {
|
|
4851
|
+
margin-top: 8px;
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4854
|
+
.assisted-by-toggle.disabled {
|
|
4855
|
+
opacity: 0.5;
|
|
4856
|
+
pointer-events: none;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4849
4859
|
.review-type-options {
|
|
4850
4860
|
display: flex;
|
|
4851
4861
|
flex-direction: column;
|
|
@@ -2827,7 +2827,7 @@ class ChatPanel {
|
|
|
2827
2827
|
_handleDismissCommentClick() {
|
|
2828
2828
|
if (this.isStreaming || !this._contextItemId) return;
|
|
2829
2829
|
this._pendingActionContext = { type: 'dismiss-comment', itemId: this._contextItemId };
|
|
2830
|
-
this.inputEl.value = 'Please
|
|
2830
|
+
this.inputEl.value = 'Please dismiss this comment.';
|
|
2831
2831
|
this.sendMessage();
|
|
2832
2832
|
}
|
|
2833
2833
|
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
* Review Submission Modal Component
|
|
4
4
|
* Allows users to submit their review with comments to GitHub
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
const ASSISTED_BY_STORAGE_KEY = 'pair-review-assisted-by';
|
|
8
|
+
const DEFAULT_ASSISTED_BY_URL = 'https://github.com/in-the-loop-labs/pair-review';
|
|
9
|
+
|
|
6
10
|
class ReviewModal {
|
|
7
11
|
constructor() {
|
|
8
12
|
this.modal = null;
|
|
9
13
|
this.isVisible = false;
|
|
10
14
|
this.isSubmitting = false;
|
|
15
|
+
this.assistedByUrl = DEFAULT_ASSISTED_BY_URL;
|
|
16
|
+
fetch('/api/config')
|
|
17
|
+
.then(res => res.ok ? res.json() : null)
|
|
18
|
+
.then(data => {
|
|
19
|
+
if (data?.assisted_by_url) {
|
|
20
|
+
this.assistedByUrl = data.assisted_by_url;
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {}); // Use default on failure
|
|
11
24
|
this.createModal();
|
|
12
25
|
this.setupEventListeners();
|
|
13
26
|
}
|
|
@@ -69,6 +82,11 @@ class ReviewModal {
|
|
|
69
82
|
placeholder="Leave a comment about this pull request..."
|
|
70
83
|
rows="2"
|
|
71
84
|
></textarea>
|
|
85
|
+
<label class="remember-toggle assisted-by-toggle" id="assisted-by-toggle">
|
|
86
|
+
<input type="checkbox" id="assisted-by-checkbox" />
|
|
87
|
+
<span class="toggle-switch"></span>
|
|
88
|
+
<span class="toggle-label">Append pair-review footer</span>
|
|
89
|
+
</label>
|
|
72
90
|
</div>
|
|
73
91
|
|
|
74
92
|
<div class="review-type-section">
|
|
@@ -183,6 +201,9 @@ class ReviewModal {
|
|
|
183
201
|
if (e.target.matches('input[name="review-event"]')) {
|
|
184
202
|
window.reviewModal?.updateTextareaState();
|
|
185
203
|
}
|
|
204
|
+
if (e.target.matches('#assisted-by-checkbox')) {
|
|
205
|
+
window.reviewModal?.handleAssistedByToggle();
|
|
206
|
+
}
|
|
186
207
|
});
|
|
187
208
|
}
|
|
188
209
|
|
|
@@ -194,6 +215,7 @@ class ReviewModal {
|
|
|
194
215
|
updateTextareaState() {
|
|
195
216
|
const textarea = this.modal?.querySelector('#review-body-modal');
|
|
196
217
|
const selectedOption = this.modal?.querySelector('input[name="review-event"]:checked');
|
|
218
|
+
const toggle = this.modal?.querySelector('#assisted-by-toggle');
|
|
197
219
|
|
|
198
220
|
if (!textarea || !selectedOption) return;
|
|
199
221
|
|
|
@@ -204,9 +226,15 @@ class ReviewModal {
|
|
|
204
226
|
if (isDraft) {
|
|
205
227
|
textarea.title = 'Review summary is not included with draft reviews';
|
|
206
228
|
textarea.classList.add('disabled-textarea');
|
|
229
|
+
if (toggle) {
|
|
230
|
+
toggle.classList.add('disabled');
|
|
231
|
+
}
|
|
207
232
|
} else {
|
|
208
233
|
textarea.title = '';
|
|
209
234
|
textarea.classList.remove('disabled-textarea');
|
|
235
|
+
if (toggle) {
|
|
236
|
+
toggle.classList.remove('disabled');
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
239
|
}
|
|
212
240
|
|
|
@@ -235,6 +263,9 @@ class ReviewModal {
|
|
|
235
263
|
// Update textarea state (ensures it's enabled since COMMENT is selected by default)
|
|
236
264
|
this.updateTextareaState();
|
|
237
265
|
|
|
266
|
+
// Restore assisted-by toggle from localStorage
|
|
267
|
+
this.restoreAssistedByToggle();
|
|
268
|
+
|
|
238
269
|
// Clear any errors or warnings
|
|
239
270
|
this.hideError();
|
|
240
271
|
this.updateLargeReviewWarning(0);
|
|
@@ -431,6 +462,10 @@ class ReviewModal {
|
|
|
431
462
|
if (this.isSubmitting) return;
|
|
432
463
|
|
|
433
464
|
const reviewBody = this.modal.querySelector('#review-body-modal').value.trim();
|
|
465
|
+
const assistedByCheckbox = this.modal.querySelector('#assisted-by-checkbox');
|
|
466
|
+
const finalBody = assistedByCheckbox?.checked
|
|
467
|
+
? reviewBody + this.getAssistedByFooter()
|
|
468
|
+
: reviewBody;
|
|
434
469
|
const selectedOption = this.modal.querySelector('input[name="review-event"]:checked');
|
|
435
470
|
const reviewEvent = selectedOption ? selectedOption.value : 'COMMENT';
|
|
436
471
|
// Count BOTH line-level (.user-comment-row) and file-level (.file-comment-card.user-comment) comments
|
|
@@ -480,7 +515,7 @@ class ReviewModal {
|
|
|
480
515
|
},
|
|
481
516
|
body: JSON.stringify({
|
|
482
517
|
event: reviewEvent,
|
|
483
|
-
body:
|
|
518
|
+
body: finalBody
|
|
484
519
|
})
|
|
485
520
|
});
|
|
486
521
|
|
|
@@ -590,7 +625,6 @@ class ReviewModal {
|
|
|
590
625
|
const textarea = this.modal?.querySelector('#review-body-modal');
|
|
591
626
|
if (!textarea) return;
|
|
592
627
|
|
|
593
|
-
// Get AI summary from the AI panel
|
|
594
628
|
const summary = window.aiPanel?.getSummary?.();
|
|
595
629
|
if (!summary) {
|
|
596
630
|
if (window.toast) {
|
|
@@ -613,6 +647,35 @@ class ReviewModal {
|
|
|
613
647
|
}
|
|
614
648
|
}
|
|
615
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Get the "assisted by" footer string
|
|
652
|
+
*/
|
|
653
|
+
getAssistedByFooter() {
|
|
654
|
+
const url = this.assistedByUrl || DEFAULT_ASSISTED_BY_URL;
|
|
655
|
+
return `\n\n---\n_Review assisted by [pair-review](${url})_`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Restore the assisted-by toggle state from localStorage
|
|
660
|
+
*/
|
|
661
|
+
restoreAssistedByToggle() {
|
|
662
|
+
const checkbox = this.modal?.querySelector('#assisted-by-checkbox');
|
|
663
|
+
if (!checkbox) return;
|
|
664
|
+
|
|
665
|
+
const stored = localStorage.getItem(ASSISTED_BY_STORAGE_KEY);
|
|
666
|
+
checkbox.checked = stored !== 'false';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Handle the assisted-by checkbox toggle
|
|
671
|
+
*/
|
|
672
|
+
handleAssistedByToggle() {
|
|
673
|
+
const checkbox = this.modal?.querySelector('#assisted-by-checkbox');
|
|
674
|
+
if (!checkbox) return;
|
|
675
|
+
|
|
676
|
+
localStorage.setItem(ASSISTED_BY_STORAGE_KEY, String(checkbox.checked));
|
|
677
|
+
}
|
|
678
|
+
|
|
616
679
|
}
|
|
617
680
|
|
|
618
681
|
// Initialize when DOM is ready if not already initialized
|
package/src/ai/analyzer.js
CHANGED
|
@@ -3505,8 +3505,12 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3505
3505
|
continue;
|
|
3506
3506
|
}
|
|
3507
3507
|
|
|
3508
|
+
const suggestionText = suggestion.suggestion;
|
|
3509
|
+
const hasSuggestionBlock = suggestionText?.trimStart().startsWith('```suggestion');
|
|
3508
3510
|
const body = suggestion.description +
|
|
3509
|
-
(
|
|
3511
|
+
(suggestionText
|
|
3512
|
+
? (hasSuggestionBlock ? '\n\n' + suggestionText : '\n\n**Suggestion:** ' + suggestionText)
|
|
3513
|
+
: '');
|
|
3510
3514
|
|
|
3511
3515
|
const isFileLevel = suggestion.is_file_level === true || suggestion.line_start === null ? 1 : 0;
|
|
3512
3516
|
const side = suggestion.old_or_new === 'OLD' ? 'LEFT' : 'RIGHT';
|
|
@@ -118,6 +118,15 @@ Output JSON with this structure:
|
|
|
118
118
|
}],
|
|
119
119
|
"summary": "Brief consolidation summary. Write as if a single reviewer produced this analysis — do NOT mention 'consolidation', 'merging', or 'multiple reviewers'."
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
### GitHub Suggestion Syntax
|
|
123
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
124
|
+
|
|
125
|
+
\`\`\`suggestion
|
|
126
|
+
replacement content here
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
121
130
|
</section>
|
|
122
131
|
|
|
123
132
|
<section name="diff-instructions" required="true">
|
|
@@ -89,6 +89,15 @@ Merge suggestions from multiple AI reviewers. Deduplicate. Resolve conflicts. Ke
|
|
|
89
89
|
}],
|
|
90
90
|
"summary": "Key findings as if from single reviewer (no mention of consolidation/merging)"
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
### GitHub Suggestion Syntax
|
|
94
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
95
|
+
|
|
96
|
+
\`\`\`suggestion
|
|
97
|
+
replacement content here
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
92
101
|
</section>
|
|
93
102
|
|
|
94
103
|
<section name="diff-instructions" required="true" tier="fast">
|
|
@@ -173,6 +173,15 @@ Output JSON with this structure:
|
|
|
173
173
|
}],
|
|
174
174
|
"summary": "Brief summary of the key findings and their significance. Write as if a single reviewer produced this analysis — do NOT mention 'consolidation', 'merging', or 'multiple reviewers'."
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
### GitHub Suggestion Syntax
|
|
178
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
179
|
+
|
|
180
|
+
\`\`\`suggestion
|
|
181
|
+
replacement content here
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
176
185
|
</section>
|
|
177
186
|
|
|
178
187
|
<section name="diff-instructions" required="true" tier="thorough">
|
|
@@ -119,6 +119,15 @@ Do NOT modify files or run write commands. Analyze and report only.
|
|
|
119
119
|
}],
|
|
120
120
|
"summary": "Brief summary of findings"
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
### GitHub Suggestion Syntax
|
|
124
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
125
|
+
|
|
126
|
+
\`\`\`suggestion
|
|
127
|
+
replacement content here
|
|
128
|
+
\`\`\`
|
|
129
|
+
|
|
130
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
122
131
|
</section>
|
|
123
132
|
|
|
124
133
|
<section name="diff-instructions" required="true">
|
|
@@ -98,6 +98,15 @@ Annotated diff tool, \`cat -n\`, ls, find, grep. Do NOT modify files.
|
|
|
98
98
|
}],
|
|
99
99
|
"summary": "Brief summary"
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
### GitHub Suggestion Syntax
|
|
103
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
104
|
+
|
|
105
|
+
\`\`\`suggestion
|
|
106
|
+
replacement content here
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
101
110
|
</section>
|
|
102
111
|
|
|
103
112
|
<section name="diff-instructions" required="true" tier="fast">
|
|
@@ -161,6 +161,15 @@ Output JSON with this structure:
|
|
|
161
161
|
}],
|
|
162
162
|
"summary": "Brief summary of findings"
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
### GitHub Suggestion Syntax
|
|
166
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
167
|
+
|
|
168
|
+
\`\`\`suggestion
|
|
169
|
+
replacement content here
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
164
173
|
</section>
|
|
165
174
|
|
|
166
175
|
<section name="diff-instructions" required="true" tier="thorough">
|
|
@@ -126,6 +126,15 @@ Note: You may use parallel read-only Tasks to examine multiple files simultaneou
|
|
|
126
126
|
}],
|
|
127
127
|
"summary": "Brief summary of file context findings"
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
### GitHub Suggestion Syntax
|
|
131
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
132
|
+
|
|
133
|
+
\`\`\`suggestion
|
|
134
|
+
replacement content here
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
129
138
|
</section>
|
|
130
139
|
|
|
131
140
|
<section name="diff-instructions" required="true">
|
|
@@ -107,6 +107,15 @@ Annotated diff tool (preferred), \`cat -n <file>\`, ls, find, grep. Do NOT modif
|
|
|
107
107
|
}],
|
|
108
108
|
"summary": "Brief summary"
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
### GitHub Suggestion Syntax
|
|
112
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
113
|
+
|
|
114
|
+
\`\`\`suggestion
|
|
115
|
+
replacement content here
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
110
119
|
</section>
|
|
111
120
|
|
|
112
121
|
<section name="diff-instructions" required="true" tier="fast">
|
|
@@ -201,6 +201,15 @@ Output JSON with this structure:
|
|
|
201
201
|
}],
|
|
202
202
|
"summary": "Brief summary of file context findings"
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
### GitHub Suggestion Syntax
|
|
206
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
207
|
+
|
|
208
|
+
\`\`\`suggestion
|
|
209
|
+
replacement content here
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
204
213
|
</section>
|
|
205
214
|
|
|
206
215
|
<section name="diff-instructions" required="true" tier="thorough">
|
|
@@ -150,6 +150,15 @@ Output JSON with this structure:
|
|
|
150
150
|
}],
|
|
151
151
|
"summary": "Brief summary of how these changes connect to and impact the codebase"
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
### GitHub Suggestion Syntax
|
|
155
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
156
|
+
|
|
157
|
+
\`\`\`suggestion
|
|
158
|
+
replacement content here
|
|
159
|
+
\`\`\`
|
|
160
|
+
|
|
161
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
153
162
|
</section>
|
|
154
163
|
|
|
155
164
|
<section name="diff-instructions" required="true">
|
|
@@ -124,6 +124,15 @@ Output JSON with this structure:
|
|
|
124
124
|
}],
|
|
125
125
|
"summary": "Brief summary of how these changes connect to and impact the codebase"
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
### GitHub Suggestion Syntax
|
|
129
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
130
|
+
|
|
131
|
+
\`\`\`suggestion
|
|
132
|
+
replacement content here
|
|
133
|
+
\`\`\`
|
|
134
|
+
|
|
135
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
127
136
|
</section>
|
|
128
137
|
|
|
129
138
|
<section name="diff-instructions" required="true" tier="fast">
|
|
@@ -268,6 +268,15 @@ Output JSON with this structure:
|
|
|
268
268
|
}],
|
|
269
269
|
"summary": "Brief summary of how these changes connect to and impact the codebase"
|
|
270
270
|
}
|
|
271
|
+
|
|
272
|
+
### GitHub Suggestion Syntax
|
|
273
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
274
|
+
|
|
275
|
+
\`\`\`suggestion
|
|
276
|
+
replacement content here
|
|
277
|
+
\`\`\`
|
|
278
|
+
|
|
279
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
271
280
|
</section>
|
|
272
281
|
|
|
273
282
|
<section name="diff-instructions" required="true" tier="thorough">
|
|
@@ -140,6 +140,15 @@ Output JSON with this structure:
|
|
|
140
140
|
}],
|
|
141
141
|
"summary": "Brief summary of the key findings and their significance to the reviewer. Focus on WHAT was found, not HOW it was found. Do NOT mention 'orchestration', 'levels', 'merged from Level 1/2/3' etc. Write as if a single reviewer produced this analysis."
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
### GitHub Suggestion Syntax
|
|
145
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
146
|
+
|
|
147
|
+
\`\`\`suggestion
|
|
148
|
+
replacement content here
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
143
152
|
</section>
|
|
144
153
|
|
|
145
154
|
<section name="diff-instructions" required="true">
|
|
@@ -120,6 +120,15 @@ Use "Consider...", "Worth noting..." - guidance not mandates.
|
|
|
120
120
|
}],
|
|
121
121
|
"summary": "Key findings as if from single reviewer (no mention of levels/orchestration)"
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
### GitHub Suggestion Syntax
|
|
125
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
126
|
+
|
|
127
|
+
\`\`\`suggestion
|
|
128
|
+
replacement content here
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
123
132
|
</section>
|
|
124
133
|
|
|
125
134
|
<section name="diff-instructions" required="true" tier="fast">
|
|
@@ -283,6 +283,15 @@ Output JSON with this structure:
|
|
|
283
283
|
}],
|
|
284
284
|
"summary": "Brief summary of the key findings and their significance to the reviewer. Focus on WHAT was found, not HOW it was found. Do NOT mention 'orchestration', 'levels', 'merged from Level 1/2/3' etc. Write as if a single reviewer produced this analysis."
|
|
285
285
|
}
|
|
286
|
+
|
|
287
|
+
### GitHub Suggestion Syntax
|
|
288
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
289
|
+
|
|
290
|
+
\`\`\`suggestion
|
|
291
|
+
replacement content here
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.
|
|
286
295
|
</section>
|
|
287
296
|
|
|
288
297
|
<section name="diff-instructions" required="true" tier="thorough">
|
|
@@ -148,7 +148,16 @@ function buildOutputSchemaSection(schema) {
|
|
|
148
148
|
Output ONLY valid JSON with no additional text, explanations, or markdown code blocks. Do not wrap the JSON in \`\`\`json blocks. The response must start with { and end with }.
|
|
149
149
|
|
|
150
150
|
Output JSON with this structure:
|
|
151
|
-
${JSON.stringify(schema, null, 2)}
|
|
151
|
+
${JSON.stringify(schema, null, 2)}
|
|
152
|
+
|
|
153
|
+
### GitHub Suggestion Syntax
|
|
154
|
+
When suggesting a specific change, **embed** a GitHub suggestion block within the "suggestion" field:
|
|
155
|
+
|
|
156
|
+
\`\`\`suggestion
|
|
157
|
+
replacement content here
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
The content inside the block is the complete replacement for the commented line(s). Do not include explanation inside the block — any explanation should appear as plain text outside it. For non-specific suggestions, use plain text only.`
|
|
152
161
|
};
|
|
153
162
|
}
|
|
154
163
|
|
|
@@ -44,6 +44,7 @@ function buildChatPrompt({ review, prData, skillPath, chatInstructions }) {
|
|
|
44
44
|
'- **Comments** are human-curated review findings (created by the reviewer).',
|
|
45
45
|
'- **Suggestions** are AI-generated findings from analysis runs.',
|
|
46
46
|
'- **Workflow**: AI generates suggestions → reviewer triages (adopt, edit, or dismiss) → adopted suggestions become comments.',
|
|
47
|
+
'- **IMPORTANT**: Do NOT adopt, dismiss, or modify suggestions or comments unless the user explicitly asks you to. Your role is to discuss and explain — the reviewer decides what action to take.',
|
|
47
48
|
'- **Analysis runs** are the process that produces suggestions. Each run has a provider, model, tier, and status.',
|
|
48
49
|
'- **Review ID** is a stable integer identifying this review session, used in all API calls.'
|
|
49
50
|
];
|
|
@@ -69,7 +70,11 @@ function buildChatPrompt({ review, prData, skillPath, chatInstructions }) {
|
|
|
69
70
|
'These become clickable links in the UI. Do NOT use backtick code spans for file references you want to be clickable.\n\n' +
|
|
70
71
|
'Files in the diff can be referenced freely. Files outside the diff can also be referenced; ' +
|
|
71
72
|
'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
|
+
'Add context files judiciously — only when directly relevant, with focused line ranges.\n\n' +
|
|
74
|
+
'When the user asks to see code (e.g. "Show me…", "Where is…", "What does X look like?"), ' +
|
|
75
|
+
'add the relevant file as a context file with a focused line range so it appears in the diff panel. ' +
|
|
76
|
+
'When explaining code, prefer referencing specific line ranges (e.g. [[file:path.js:10-25]]) ' +
|
|
77
|
+
'over quoting large blocks in your response. Short inline snippets (a few lines) are fine when they clarify the explanation.'
|
|
73
78
|
);
|
|
74
79
|
|
|
75
80
|
// Tool usage discipline — avoid unnecessary Task delegation
|
package/src/config.js
CHANGED
|
@@ -24,7 +24,8 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
enable_chat: true, // When true, enables the chat panel feature (requires Pi AI provider)
|
|
25
25
|
chat: { enable_shortcuts: true }, // Chat panel settings (enable_shortcuts: show action shortcut buttons)
|
|
26
26
|
providers: {}, // Custom provider configurations (overrides built-in defaults)
|
|
27
|
-
monorepos: {} // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
|
|
27
|
+
monorepos: {}, // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
|
|
28
|
+
assisted_by_url: "https://github.com/in-the-loop-labs/pair-review" // URL for "Review assisted by" footer link
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
/**
|
package/src/database.js
CHANGED
|
@@ -2193,8 +2193,12 @@ class CommentRepository {
|
|
|
2193
2193
|
}
|
|
2194
2194
|
|
|
2195
2195
|
for (const suggestion of normalized) {
|
|
2196
|
+
const suggestionText = suggestion.suggestion;
|
|
2197
|
+
const hasSuggestionBlock = suggestionText?.trimStart().startsWith('```suggestion');
|
|
2196
2198
|
const body = suggestion.description +
|
|
2197
|
-
(
|
|
2199
|
+
(suggestionText
|
|
2200
|
+
? (hasSuggestionBlock ? '\n\n' + suggestionText : '\n\n**Suggestion:** ' + suggestionText)
|
|
2201
|
+
: '');
|
|
2198
2202
|
|
|
2199
2203
|
// File-level suggestions have is_file_level=true or have null line_start
|
|
2200
2204
|
const isFileLevel = suggestion.is_file_level === true || suggestion.line_start === null ? 1 : 0;
|
package/src/github/parser.js
CHANGED
|
@@ -30,7 +30,7 @@ class PRArgumentParser {
|
|
|
30
30
|
// Check if input is a PR number
|
|
31
31
|
const prNumber = parseInt(input);
|
|
32
32
|
if (isNaN(prNumber) || prNumber <= 0) {
|
|
33
|
-
throw new Error('Invalid input format. Expected: PR number, GitHub URL (https://github.com/owner/repo/pull/number),
|
|
33
|
+
throw new Error('Invalid input format. Expected: PR number, GitHub URL (https://github.com/owner/repo/pull/number), Graphite URL (https://app.graphite.com/github/pr/owner/repo/number), or pair-review:// URL');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Parse repository from current directory's git remote
|
|
@@ -59,6 +59,15 @@ class PRArgumentParser {
|
|
|
59
59
|
normalizedUrl = 'https://' + normalizedUrl;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Check if input is a pair-review:// protocol URL
|
|
63
|
+
if (normalizedUrl.startsWith('pair-review://')) {
|
|
64
|
+
try {
|
|
65
|
+
return this.parseProtocolURL(normalizedUrl);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
// Check if input is a GitHub URL
|
|
63
72
|
if (normalizedUrl.startsWith('https://github.com/')) {
|
|
64
73
|
try {
|
|
@@ -114,6 +123,22 @@ class PRArgumentParser {
|
|
|
114
123
|
return this._createPRInfo(owner, repo, numberStr, 'Graphite');
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Parse pair-review:// protocol URL to extract owner, repo, and PR number
|
|
128
|
+
* @param {string} url - Protocol URL (e.g., pair-review://pr/owner/repo/123)
|
|
129
|
+
* @returns {Object} Parsed information { owner, repo, number }
|
|
130
|
+
*/
|
|
131
|
+
parseProtocolURL(url) {
|
|
132
|
+
const match = url.match(/^pair-review:\/\/pr\/([^\/]+)\/([^\/]+)\/(\d+)(?:\/.*)?$/);
|
|
133
|
+
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error('Invalid pair-review:// URL format. Expected: pair-review://pr/owner/repo/number');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const [, owner, repo, numberStr] = match;
|
|
139
|
+
return this._createPRInfo(owner, repo, numberStr, 'pair-review://');
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/**
|
|
118
143
|
* Create and validate PR info object from parsed components
|
|
119
144
|
* @param {string} owner - Repository owner
|
|
@@ -129,7 +154,9 @@ class PRArgumentParser {
|
|
|
129
154
|
if (isNaN(number) || number <= 0) {
|
|
130
155
|
const exampleUrl = source === 'GitHub'
|
|
131
156
|
? 'https://github.com/owner/repo/pull/number'
|
|
132
|
-
: '
|
|
157
|
+
: source === 'pair-review://'
|
|
158
|
+
? 'pair-review://pr/owner/repo/number'
|
|
159
|
+
: 'https://app.graphite.com/github/pr/owner/repo/number';
|
|
133
160
|
throw new Error(`Invalid ${source} URL format. Expected: ${exampleUrl}`);
|
|
134
161
|
}
|
|
135
162
|
|
package/src/main.js
CHANGED
|
@@ -16,6 +16,7 @@ const simpleGit = require('simple-git');
|
|
|
16
16
|
const { getGeneratedFilePatterns } = require('./git/gitattributes');
|
|
17
17
|
const { getEmoji: getCategoryEmoji } = require('./utils/category-emoji');
|
|
18
18
|
const open = (...args) => import('open').then(({default: open}) => open(...args));
|
|
19
|
+
const { registerProtocolHandler, unregisterProtocolHandler } = require('./protocol-handler');
|
|
19
20
|
|
|
20
21
|
let db = null;
|
|
21
22
|
|
|
@@ -117,6 +118,9 @@ OPTIONS:
|
|
|
117
118
|
(automatic in GitHub Actions)
|
|
118
119
|
--yolo Allow AI providers full system access (skip read-only
|
|
119
120
|
restrictions). Analogous to --dangerously-skip-permissions
|
|
121
|
+
--register [--command <cmd>] Register pair-review:// URL scheme handler (macOS)
|
|
122
|
+
Default command: npx @in-the-loop-labs/pair-review
|
|
123
|
+
--unregister Unregister pair-review:// URL scheme handler (macOS)
|
|
120
124
|
-v, --version Show version number and exit
|
|
121
125
|
|
|
122
126
|
EXAMPLES:
|
|
@@ -125,6 +129,8 @@ EXAMPLES:
|
|
|
125
129
|
pair-review --local # Review uncommitted local changes
|
|
126
130
|
pair-review 123 --ai # Auto-run AI analysis
|
|
127
131
|
pair-review --ai-review # CI mode: auto-detect PR, submit review
|
|
132
|
+
pair-review --register # Register URL scheme handler
|
|
133
|
+
pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
128
134
|
|
|
129
135
|
ENVIRONMENT VARIABLES:
|
|
130
136
|
GITHUB_TOKEN GitHub Personal Access Token (takes precedence over config file)
|
|
@@ -188,6 +194,9 @@ const KNOWN_FLAGS = new Set([
|
|
|
188
194
|
'-l', '--local',
|
|
189
195
|
'--mcp',
|
|
190
196
|
'--model',
|
|
197
|
+
'--register',
|
|
198
|
+
'--unregister',
|
|
199
|
+
'--command',
|
|
191
200
|
'--use-checkout',
|
|
192
201
|
'--yolo',
|
|
193
202
|
'-v', '--version'
|
|
@@ -243,9 +252,14 @@ function parseArgs(args) {
|
|
|
243
252
|
i++; // Skip next argument since we consumed it
|
|
244
253
|
}
|
|
245
254
|
// localPath will be resolved to cwd if not provided
|
|
246
|
-
} else if (arg === '--configure' || arg === '-h' || arg === '--help' || arg === '--mcp' || arg === '-v' || arg === '--version') {
|
|
255
|
+
} else if (arg === '--configure' || arg === '-h' || arg === '--help' || arg === '--mcp' || arg === '-v' || arg === '--version' || arg === '--register' || arg === '--unregister') {
|
|
247
256
|
// Skip flags that are handled earlier in main()
|
|
248
257
|
continue;
|
|
258
|
+
} else if (arg === '--command') {
|
|
259
|
+
// --command flag consumed by --register handler, skip it and its value
|
|
260
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
261
|
+
i++; // Skip the next argument (the command value)
|
|
262
|
+
}
|
|
249
263
|
} else if (arg.startsWith('-')) {
|
|
250
264
|
// Unknown flag - collect for error reporting
|
|
251
265
|
unknownFlags.push(arg);
|
|
@@ -346,6 +360,26 @@ AI PROVIDERS:
|
|
|
346
360
|
process.exit(0);
|
|
347
361
|
}
|
|
348
362
|
|
|
363
|
+
// Handle protocol handler registration
|
|
364
|
+
if (args.includes('--register')) {
|
|
365
|
+
const cmdIdx = args.indexOf('--command');
|
|
366
|
+
let command;
|
|
367
|
+
if (cmdIdx !== -1) {
|
|
368
|
+
if (cmdIdx + 1 < args.length && !args[cmdIdx + 1].startsWith('-')) {
|
|
369
|
+
command = args[cmdIdx + 1];
|
|
370
|
+
} else {
|
|
371
|
+
throw new Error('--command flag requires a command string');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await registerProtocolHandler({ command });
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (args.includes('--unregister')) {
|
|
379
|
+
await unregisterProtocolHandler();
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
|
|
349
383
|
// Load configuration
|
|
350
384
|
const { config, isFirstRun } = await loadConfig();
|
|
351
385
|
|
|
@@ -466,8 +500,6 @@ AI PROVIDERS:
|
|
|
466
500
|
* @param {Object} flags - Parsed command line flags
|
|
467
501
|
*/
|
|
468
502
|
async function handlePullRequest(args, config, db, flags = {}) {
|
|
469
|
-
let prInfo = null; // Declare prInfo outside try block for error handling
|
|
470
|
-
|
|
471
503
|
try {
|
|
472
504
|
// Get GitHub token (env var takes precedence over config)
|
|
473
505
|
const githubToken = getGitHubToken(config);
|
|
@@ -477,156 +509,36 @@ async function handlePullRequest(args, config, db, flags = {}) {
|
|
|
477
509
|
|
|
478
510
|
// Parse PR arguments
|
|
479
511
|
const parser = new PRArgumentParser();
|
|
480
|
-
prInfo = await parser.parsePRArguments(args);
|
|
481
|
-
|
|
482
|
-
console.log(`Processing pull request #${prInfo.number} from ${prInfo.owner}/${prInfo.repo}`);
|
|
483
|
-
|
|
484
|
-
// Create GitHub client and verify repository access
|
|
485
|
-
const githubClient = new GitHubClient(githubToken);
|
|
486
|
-
const repoExists = await githubClient.repositoryExists(prInfo.owner, prInfo.repo);
|
|
487
|
-
if (!repoExists) {
|
|
488
|
-
throw new Error(`Repository ${prInfo.owner}/${prInfo.repo} not found or not accessible`);
|
|
489
|
-
}
|
|
512
|
+
const prInfo = await parser.parsePRArguments(args);
|
|
490
513
|
|
|
491
|
-
//
|
|
492
|
-
console.log('Fetching pull request data from GitHub...');
|
|
493
|
-
const prData = await githubClient.fetchPullRequest(prInfo.owner, prInfo.repo, prInfo.number);
|
|
494
|
-
|
|
495
|
-
// Determine repository path: only use cwd if it matches the target repo
|
|
514
|
+
// Register cwd as known repo path if it matches the target repo
|
|
496
515
|
const currentDir = parser.getCurrentDirectory();
|
|
497
516
|
const isMatchingRepo = await parser.isMatchingRepository(currentDir, prInfo.owner, prInfo.repo);
|
|
498
|
-
const repository = normalizeRepository(prInfo.owner, prInfo.repo);
|
|
499
|
-
|
|
500
|
-
let repositoryPath;
|
|
501
|
-
let worktreeSourcePath;
|
|
502
|
-
let checkoutScript;
|
|
503
|
-
let checkoutTimeout;
|
|
504
|
-
let worktreeConfig = null;
|
|
505
517
|
if (isMatchingRepo) {
|
|
506
|
-
// Current directory is a checkout of the target repository
|
|
507
|
-
repositoryPath = currentDir;
|
|
508
|
-
// Register the known repository location for future web UI usage
|
|
509
518
|
await registerRepositoryLocation(db, currentDir, prInfo.owner, prInfo.repo);
|
|
510
|
-
|
|
511
|
-
// Resolve monorepo config options (checkout_script, worktree_directory, worktree_name_template)
|
|
512
|
-
// even when running from inside the target repo, so they are not silently ignored.
|
|
513
|
-
const resolved = resolveMonorepoOptions(config, repository);
|
|
514
|
-
checkoutScript = resolved.checkoutScript;
|
|
515
|
-
checkoutTimeout = resolved.checkoutTimeout;
|
|
516
|
-
worktreeConfig = resolved.worktreeConfig;
|
|
517
|
-
} else {
|
|
518
|
-
// Current directory is not the target repository - find or clone it
|
|
519
|
-
console.log(`Current directory is not a checkout of ${prInfo.owner}/${prInfo.repo}, locating repository...`);
|
|
520
|
-
const result = await findRepositoryPath({
|
|
521
|
-
db,
|
|
522
|
-
owner: prInfo.owner,
|
|
523
|
-
repo: prInfo.repo,
|
|
524
|
-
repository,
|
|
525
|
-
prNumber: prInfo.number,
|
|
526
|
-
config,
|
|
527
|
-
onProgress: (progress) => {
|
|
528
|
-
if (progress.message) {
|
|
529
|
-
console.log(progress.message);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
repositoryPath = result.repositoryPath;
|
|
534
|
-
worktreeSourcePath = result.worktreeSourcePath;
|
|
535
|
-
checkoutScript = result.checkoutScript;
|
|
536
|
-
checkoutTimeout = result.checkoutTimeout;
|
|
537
|
-
worktreeConfig = result.worktreeConfig;
|
|
538
519
|
}
|
|
539
520
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
worktreeSourcePath,
|
|
545
|
-
checkoutScript,
|
|
546
|
-
checkoutTimeout
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// Generate unified diff
|
|
550
|
-
console.log('Generating unified diff...');
|
|
551
|
-
const diff = await worktreeManager.generateUnifiedDiff(worktreePath, prData);
|
|
552
|
-
const changedFiles = await worktreeManager.getChangedFiles(worktreePath, prData);
|
|
521
|
+
// Set model override if provided via CLI flag
|
|
522
|
+
if (flags.model) {
|
|
523
|
+
process.env.PAIR_REVIEW_MODEL = flags.model;
|
|
524
|
+
}
|
|
553
525
|
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
await storePRData(db, prInfo, prData, diff, changedFiles, worktreePath);
|
|
526
|
+
// Start server and open browser to setup page
|
|
527
|
+
const port = await startServer(db);
|
|
557
528
|
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
const port = await startServerWithPRContext(config, prInfo, flags);
|
|
529
|
+
// Async cleanup of stale worktrees (don't block startup)
|
|
530
|
+
cleanupStaleWorktreesAsync(config);
|
|
561
531
|
|
|
562
|
-
|
|
532
|
+
let url = `http://localhost:${port}/pr/${prInfo.owner}/${prInfo.repo}/${prInfo.number}`;
|
|
563
533
|
if (flags.ai) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
// Wait for server to be ready with retry logic
|
|
567
|
-
const maxRetries = 5;
|
|
568
|
-
const retryDelay = 200; // ms
|
|
569
|
-
let lastError;
|
|
570
|
-
|
|
571
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
572
|
-
try {
|
|
573
|
-
// Add small delay to ensure server is fully initialized
|
|
574
|
-
if (attempt > 1) {
|
|
575
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const response = await fetch(`http://localhost:${port}/api/pr/${prInfo.owner}/${prInfo.repo}/${prInfo.number}/analyses`, {
|
|
579
|
-
method: 'POST',
|
|
580
|
-
headers: { 'Content-Type': 'application/json' }
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
if (response.ok) {
|
|
584
|
-
const result = await response.json();
|
|
585
|
-
console.log(`AI analysis started (ID: ${result.analysisId})`);
|
|
586
|
-
break; // Success, exit retry loop
|
|
587
|
-
} else {
|
|
588
|
-
lastError = `Server responded with ${response.status}: ${await response.text()}`;
|
|
589
|
-
if (attempt === maxRetries) {
|
|
590
|
-
console.warn('Failed to start AI analysis:', lastError);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
} catch (error) {
|
|
594
|
-
lastError = error.message;
|
|
595
|
-
if (attempt === maxRetries) {
|
|
596
|
-
console.warn('Could not start AI analysis after', maxRetries, 'attempts:', lastError);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
534
|
+
url += '?analyze=true';
|
|
600
535
|
}
|
|
601
536
|
|
|
602
|
-
// Open browser to PR view
|
|
603
|
-
const url = `http://localhost:${port}/pr/${prInfo.owner}/${prInfo.repo}/${prInfo.number}`;
|
|
604
|
-
|
|
605
537
|
console.log(`Opening browser to: ${url}`);
|
|
606
538
|
await open(url);
|
|
607
539
|
|
|
608
540
|
} catch (error) {
|
|
609
|
-
|
|
610
|
-
if (error.message && error.message.includes('not found in repository')) {
|
|
611
|
-
if (prInfo) {
|
|
612
|
-
console.error(`\n❌ Pull request #${prInfo.number} does not exist in ${prInfo.owner}/${prInfo.repo}`);
|
|
613
|
-
} else {
|
|
614
|
-
console.error(`\n❌ ${error.message}`);
|
|
615
|
-
}
|
|
616
|
-
console.error('Please check the PR number and try again.\n');
|
|
617
|
-
} else if (error.message && error.message.includes('authentication failed')) {
|
|
618
|
-
console.error('\n❌ GitHub authentication failed');
|
|
619
|
-
console.error('Please check your token in ~/.pair-review/config.json\n');
|
|
620
|
-
} else if (error.message && error.message.includes('Repository') && error.message.includes('not found')) {
|
|
621
|
-
console.error(`\n❌ ${error.message}`);
|
|
622
|
-
console.error('Please check the repository name and your access permissions.\n');
|
|
623
|
-
} else if (error.message && error.message.includes('Network error')) {
|
|
624
|
-
console.error('\n❌ Network connection error');
|
|
625
|
-
console.error('Please check your internet connection and try again.\n');
|
|
626
|
-
} else {
|
|
627
|
-
// For other errors, show a clean message without stack trace
|
|
628
|
-
console.error(`\n❌ Error: ${error.message}\n`);
|
|
629
|
-
}
|
|
541
|
+
console.error(`\n❌ Error: ${error.message}\n`);
|
|
630
542
|
process.exit(1);
|
|
631
543
|
}
|
|
632
544
|
}
|
|
@@ -647,36 +559,6 @@ async function startServerOnly(config) {
|
|
|
647
559
|
await open(url);
|
|
648
560
|
}
|
|
649
561
|
|
|
650
|
-
/**
|
|
651
|
-
* Start server with PR context
|
|
652
|
-
* @param {Object} config - Application configuration
|
|
653
|
-
* @param {Object} prInfo - PR information
|
|
654
|
-
* @param {Object} flags - Command line flags
|
|
655
|
-
* @returns {Promise<number>} Server port
|
|
656
|
-
*/
|
|
657
|
-
async function startServerWithPRContext(config, prInfo, flags = {}) {
|
|
658
|
-
// Set environment variable for PR context
|
|
659
|
-
process.env.PAIR_REVIEW_PR = JSON.stringify(prInfo);
|
|
660
|
-
|
|
661
|
-
// Set environment variable for auto-AI flag
|
|
662
|
-
if (flags.ai) {
|
|
663
|
-
process.env.PAIR_REVIEW_AUTO_AI = 'true';
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Set environment variable for model override (CLI takes priority)
|
|
667
|
-
if (flags.model) {
|
|
668
|
-
process.env.PAIR_REVIEW_MODEL = flags.model;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const { startServer } = require('./server');
|
|
672
|
-
const actualPort = await startServer(db);
|
|
673
|
-
|
|
674
|
-
// Async cleanup of stale worktrees (don't block startup)
|
|
675
|
-
cleanupStaleWorktreesAsync(config);
|
|
676
|
-
|
|
677
|
-
// Return the actual port the server started on
|
|
678
|
-
return actualPort;
|
|
679
|
-
}
|
|
680
562
|
|
|
681
563
|
/**
|
|
682
564
|
* Format AI suggestion with emoji and category prefix
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { getConfigDir } = require('./config');
|
|
6
|
+
const logger = require('./utils/logger');
|
|
7
|
+
|
|
8
|
+
const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
|
|
9
|
+
|
|
10
|
+
// Default dependencies (overridable for testing)
|
|
11
|
+
const defaults = {
|
|
12
|
+
fs,
|
|
13
|
+
execSync,
|
|
14
|
+
getConfigDir,
|
|
15
|
+
logger,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a macOS custom URL scheme handler for pair-review://
|
|
20
|
+
* Creates an AppleScript .app that forwards URLs to the pair-review CLI.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {string} [options.command] - CLI command to invoke (default: npx @in-the-loop-labs/pair-review)
|
|
24
|
+
* @param {object} [options._deps] - Internal: dependency overrides for testing
|
|
25
|
+
*/
|
|
26
|
+
function registerProtocolHandler({ command, _deps } = {}) {
|
|
27
|
+
const deps = { ...defaults, ..._deps };
|
|
28
|
+
|
|
29
|
+
if (process.platform !== 'darwin') {
|
|
30
|
+
deps.logger.warn('Protocol handler registration is only supported on macOS');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const shell = process.env.SHELL || '/bin/zsh';
|
|
35
|
+
const resolvedCommand = command || 'npx @in-the-loop-labs/pair-review';
|
|
36
|
+
|
|
37
|
+
// Escape characters special inside AppleScript double-quoted strings
|
|
38
|
+
const escapeAS = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
39
|
+
const safeShell = escapeAS(shell);
|
|
40
|
+
const safeCommand = escapeAS(resolvedCommand);
|
|
41
|
+
|
|
42
|
+
const appleScriptSource = [
|
|
43
|
+
'on run',
|
|
44
|
+
'\t-- No-op: launched directly without a URL',
|
|
45
|
+
'end run',
|
|
46
|
+
'',
|
|
47
|
+
'on open location theURL',
|
|
48
|
+
`\tdo shell script "${safeShell} -l -c \\"${safeCommand} " & quoted form of theURL & " > /dev/null 2>&1 &\\""`,
|
|
49
|
+
'end open location',
|
|
50
|
+
].join('\n');
|
|
51
|
+
|
|
52
|
+
const appPath = path.join(deps.getConfigDir(), 'PairReview.app');
|
|
53
|
+
|
|
54
|
+
// Remove existing .app if present
|
|
55
|
+
deps.fs.rmSync(appPath, { recursive: true, force: true });
|
|
56
|
+
|
|
57
|
+
// Compile the AppleScript into an .app bundle
|
|
58
|
+
deps.execSync(`osacompile -o "${appPath}"`, { input: appleScriptSource });
|
|
59
|
+
|
|
60
|
+
// Mutate Info.plist to declare the URL scheme
|
|
61
|
+
const plistPath = path.join(appPath, 'Contents', 'Info.plist');
|
|
62
|
+
let plist = deps.fs.readFileSync(plistPath, 'utf-8');
|
|
63
|
+
|
|
64
|
+
const urlSchemeEntries = [
|
|
65
|
+
'\t<key>CFBundleIdentifier</key>',
|
|
66
|
+
'\t<string>com.pair-review.launcher</string>',
|
|
67
|
+
'\t<key>CFBundleURLTypes</key>',
|
|
68
|
+
'\t<array>',
|
|
69
|
+
'\t\t<dict>',
|
|
70
|
+
'\t\t\t<key>CFBundleURLName</key>',
|
|
71
|
+
'\t\t\t<string>pair-review URL</string>',
|
|
72
|
+
'\t\t\t<key>CFBundleURLSchemes</key>',
|
|
73
|
+
'\t\t\t<array>',
|
|
74
|
+
'\t\t\t\t<string>pair-review</string>',
|
|
75
|
+
'\t\t\t</array>',
|
|
76
|
+
'\t\t</dict>',
|
|
77
|
+
'\t</array>',
|
|
78
|
+
].join('\n');
|
|
79
|
+
|
|
80
|
+
// Insert before the closing </dict> that precedes </plist>
|
|
81
|
+
plist = plist.replace('</dict>\n</plist>', `${urlSchemeEntries}\n</dict>\n</plist>`);
|
|
82
|
+
deps.fs.writeFileSync(plistPath, plist);
|
|
83
|
+
|
|
84
|
+
deps.execSync(`"${LSREGISTER}" -R -f "${appPath}"`);
|
|
85
|
+
|
|
86
|
+
// console.log intentional: CLI output, not server-side (see CLAUDE.md logging convention)
|
|
87
|
+
console.log('Registered pair-review:// URL scheme handler');
|
|
88
|
+
console.log(`Command: ${shell} -l -c '${resolvedCommand} <url>'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unregister the macOS custom URL scheme handler for pair-review://
|
|
93
|
+
* Removes the .app bundle and deregisters from Launch Services.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @param {object} [options._deps] - Internal: dependency overrides for testing
|
|
97
|
+
*/
|
|
98
|
+
function unregisterProtocolHandler({ _deps } = {}) {
|
|
99
|
+
const deps = { ...defaults, ..._deps };
|
|
100
|
+
|
|
101
|
+
if (process.platform !== 'darwin') {
|
|
102
|
+
deps.logger.warn('Protocol handler registration is only supported on macOS');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const appPath = path.join(deps.getConfigDir(), 'PairReview.app');
|
|
107
|
+
|
|
108
|
+
// Attempt to deregister from Launch Services (may fail if not registered)
|
|
109
|
+
try {
|
|
110
|
+
deps.execSync(`"${LSREGISTER}" -u "${appPath}"`);
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore — handler may not be registered
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove the .app bundle
|
|
116
|
+
deps.fs.rmSync(appPath, { recursive: true, force: true });
|
|
117
|
+
|
|
118
|
+
// console.log intentional: CLI output, not server-side (see CLAUDE.md logging convention)
|
|
119
|
+
console.log('Unregistered pair-review:// URL scheme handler');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { registerProtocolHandler, unregisterProtocolHandler };
|
package/src/routes/config.js
CHANGED
|
@@ -40,7 +40,8 @@ router.get('/api/config', (req, res) => {
|
|
|
40
40
|
is_running_via_npx: isRunningViaNpx(),
|
|
41
41
|
enable_chat: config.enable_chat !== false,
|
|
42
42
|
chat_enable_shortcuts: config.chat?.enable_shortcuts !== false,
|
|
43
|
-
pi_available: getCachedAvailability('pi')?.available || false
|
|
43
|
+
pi_available: getCachedAvailability('pi')?.available || false,
|
|
44
|
+
assisted_by_url: config.assisted_by_url || 'https://github.com/in-the-loop-labs/pair-review'
|
|
44
45
|
});
|
|
45
46
|
});
|
|
46
47
|
|