@in-the-loop-labs/pair-review 3.5.2 → 3.7.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 +4 -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/public/css/analysis-config.css +1807 -0
- package/public/css/pr.css +1029 -2169
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +39 -23
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/TourBar.js +248 -0
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +64 -8
- package/public/js/modules/cancel-background-job.js +183 -0
- package/public/js/modules/hunk-summary-renderer.js +116 -0
- package/public/js/modules/storage-cleanup.js +16 -0
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +755 -0
- package/public/js/pr.js +1826 -56
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/modal-detection.js +77 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +24 -0
- package/public/pr.html +24 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/background-queue.js +290 -0
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +50 -7
- package/src/ai/codex-provider.js +28 -5
- package/src/ai/copilot-provider.js +22 -3
- package/src/ai/cursor-agent-provider.js +22 -6
- package/src/ai/executable-provider.js +4 -19
- package/src/ai/gemini-provider.js +22 -5
- package/src/ai/hunk-hashing.js +161 -0
- package/src/ai/index.js +2 -0
- package/src/ai/opencode-provider.js +21 -5
- package/src/ai/pi-provider.js +21 -5
- package/src/ai/prompts/hunk-summary.js +199 -0
- package/src/ai/prompts/tour.js +232 -0
- package/src/ai/provider.js +21 -1
- package/src/ai/summary-generator.js +469 -0
- package/src/ai/tour-generator.js +568 -0
- package/src/config.js +778 -10
- package/src/database.js +282 -1
- package/src/external/github-adapter.js +114 -25
- package/src/git/base-branch.js +11 -4
- package/src/github/client.js +482 -588
- package/src/github/errors.js +55 -0
- package/src/github/impl/graphql/pending-review-comments.js +230 -0
- package/src/github/impl/graphql/pending-review.js +153 -0
- package/src/github/impl/graphql/review-lifecycle.js +161 -0
- package/src/github/impl/graphql/stack-walker.js +210 -0
- package/src/github/impl/host/pending-review-comments.js +338 -0
- package/src/github/impl/rest/pending-review.js +251 -0
- package/src/github/impl/rest/review-lifecycle.js +226 -0
- package/src/github/impl/rest/stack-walker.js +309 -0
- package/src/github/operations/pending-review-comments.js +79 -0
- package/src/github/operations/pending-review.js +89 -0
- package/src/github/operations/review-lifecycle.js +126 -0
- package/src/github/operations/stack-walker.js +87 -0
- package/src/github/parser.js +230 -4
- package/src/github/stack-walker.js +14 -189
- package/src/links/repo-links.js +230 -0
- package/src/local-review.js +201 -172
- package/src/main.js +133 -30
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +118 -3
- package/src/routes/context-files.js +2 -29
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +410 -13
- package/src/routes/mcp.js +47 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +556 -71
- package/src/routes/reviews.js +145 -29
- package/src/routes/setup.js +8 -3
- package/src/routes/stack-analysis.js +33 -9
- package/src/routes/worktrees.js +3 -2
- package/src/server.js +2 -0
- package/src/setup/pr-setup.js +37 -11
- package/src/setup/stack-setup.js +13 -3
- package/src/single-port.js +6 -3
- package/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Repo Links Resolver
|
|
4
|
+
*
|
|
5
|
+
* Resolves per-repo header link configuration. Configuration shape:
|
|
6
|
+
*
|
|
7
|
+
* "links": {
|
|
8
|
+
* "external": { "label": "...", "url_template": "https://...", "icon": "<svg ...>...</svg>" },
|
|
9
|
+
* "github": false, // hide default GitHub link
|
|
10
|
+
* "graphite": false // hide Graphite stack link
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Public API:
|
|
14
|
+
*
|
|
15
|
+
* substituteUrlTemplate(template, context)
|
|
16
|
+
* — Replace whitelisted placeholders `{owner}`, `{repo}`, `{number}`,
|
|
17
|
+
* `{branch}`, `{base_branch}`, `{head_sha}` with URL-encoded values
|
|
18
|
+
* from `context`. Returns `null` if the resulting URL does not start
|
|
19
|
+
* with `https://` or if the template is malformed.
|
|
20
|
+
*
|
|
21
|
+
* sanitizeSvgIcon(svg)
|
|
22
|
+
* — Strip `<script>` tags, on* event-handler attributes, and
|
|
23
|
+
* `javascript:` URLs. Returns the sanitised SVG string, or null if
|
|
24
|
+
* the value is not a string that looks like SVG markup.
|
|
25
|
+
*
|
|
26
|
+
* resolveRepoLinks(config, repository)
|
|
27
|
+
* — Returns `{ external, github, graphite }` for the given
|
|
28
|
+
* `owner/repo`. The booleans are normalised: any non-false value (or
|
|
29
|
+
* absence) becomes `true`. `external` is `null` unless valid, with
|
|
30
|
+
* the icon already sanitised.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const { getRepoConfig } = require('../config');
|
|
34
|
+
const logger = require('../utils/logger');
|
|
35
|
+
|
|
36
|
+
// Whitelist of allowed `{placeholder}` names. Anything else in the template
|
|
37
|
+
// is left unsubstituted (so a malformed template surfaces visually rather
|
|
38
|
+
// than silently producing the wrong URL).
|
|
39
|
+
const ALLOWED_PLACEHOLDERS = new Set([
|
|
40
|
+
'owner', 'repo', 'number', 'branch', 'base_branch', 'head_sha'
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Matches HTML on* event-handler attributes regardless of quoting style.
|
|
44
|
+
// Used to strip dangerous attributes from user-supplied SVG icons.
|
|
45
|
+
const ON_HANDLER_RE = /\s+on[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/g;
|
|
46
|
+
|
|
47
|
+
// Matches a complete `<script ...>...</script>` block (including the open
|
|
48
|
+
// and close tags) with case-insensitive, dot-matches-newline semantics so
|
|
49
|
+
// payload bodies that span lines are removed in one pass.
|
|
50
|
+
const SCRIPT_BLOCK_RE = /<script\b[^>]*>[\s\S]*?<\/script\s*>/gi;
|
|
51
|
+
|
|
52
|
+
// Matches standalone <script ...> open tags missing a closing tag — strip
|
|
53
|
+
// them as well so a half-broken payload can't slip through.
|
|
54
|
+
const SCRIPT_OPEN_RE = /<script\b[^>]*>/gi;
|
|
55
|
+
|
|
56
|
+
// Matches values that begin with `javascript:` (anchor href, attribute
|
|
57
|
+
// values). Captures the leading quote so we can preserve attribute syntax.
|
|
58
|
+
const JS_URL_RE = /(["'=])\s*javascript:[^"'\s>]*/gi;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Substitute whitelisted `{placeholder}` tokens in a URL template.
|
|
62
|
+
*
|
|
63
|
+
* Behaviour:
|
|
64
|
+
* - Only `{owner}`, `{repo}`, `{number}`, `{branch}`, `{base_branch}`,
|
|
65
|
+
* and `{head_sha}` are substituted. Other tokens are left as-is so
|
|
66
|
+
* misconfigurations are visible.
|
|
67
|
+
* - Each substituted value is run through `encodeURIComponent()` to
|
|
68
|
+
* prevent injection of additional path segments or query params.
|
|
69
|
+
* - The result is rejected (returns `null`) unless it starts with
|
|
70
|
+
* `https://`, the same invariant `validateRepoConfig` enforces at
|
|
71
|
+
* startup. This is defence-in-depth: a template like
|
|
72
|
+
* `https://{owner}.example/foo` is technically `https://` at config
|
|
73
|
+
* time but could be subverted at substitution time if `{owner}` were
|
|
74
|
+
* unescaped — `encodeURIComponent` protects against that.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} template
|
|
77
|
+
* @param {Object} context
|
|
78
|
+
* @param {string} [context.owner]
|
|
79
|
+
* @param {string} [context.repo]
|
|
80
|
+
* @param {number|string} [context.number]
|
|
81
|
+
* @param {string} [context.branch]
|
|
82
|
+
* @param {string} [context.base_branch]
|
|
83
|
+
* @param {string} [context.head_sha]
|
|
84
|
+
* @returns {string|null} Substituted URL, or null if it fails validation
|
|
85
|
+
*/
|
|
86
|
+
function substituteUrlTemplate(template, context) {
|
|
87
|
+
if (typeof template !== 'string' || !template) return null;
|
|
88
|
+
const ctx = context || {};
|
|
89
|
+
|
|
90
|
+
const substituted = template.replace(/\{([a-zA-Z_]+)\}/g, (match, name) => {
|
|
91
|
+
if (!ALLOWED_PLACEHOLDERS.has(name)) return match;
|
|
92
|
+
const value = ctx[name];
|
|
93
|
+
if (value === undefined || value === null || value === '') return match;
|
|
94
|
+
return encodeURIComponent(String(value));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!substituted.startsWith('https://')) return null;
|
|
98
|
+
// If any unsubstituted whitelisted placeholders remain, the URL is
|
|
99
|
+
// incomplete — reject rather than producing a broken link.
|
|
100
|
+
for (const placeholder of ALLOWED_PLACEHOLDERS) {
|
|
101
|
+
if (substituted.includes(`{${placeholder}}`)) return null;
|
|
102
|
+
}
|
|
103
|
+
return substituted;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Strip dangerous content from user-supplied SVG markup.
|
|
108
|
+
*
|
|
109
|
+
* Removes:
|
|
110
|
+
* - `<script>...</script>` blocks (paired or unpaired)
|
|
111
|
+
* - All `on*=` event-handler attributes
|
|
112
|
+
* - Attribute values starting with `javascript:` (sets them to empty)
|
|
113
|
+
*
|
|
114
|
+
* This is the bare-minimum sanitisation called out in the plan. The
|
|
115
|
+
* frontend additionally re-parses the SVG via DOMParser before injecting
|
|
116
|
+
* it into the DOM, so any script-like content that slips through gets a
|
|
117
|
+
* second filter at insertion time.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} svg
|
|
120
|
+
* @returns {string|null} Sanitised SVG, or null if the input is not a
|
|
121
|
+
* string that looks like SVG markup.
|
|
122
|
+
*/
|
|
123
|
+
function sanitizeSvgIcon(svg) {
|
|
124
|
+
if (typeof svg !== 'string' || !svg.trim()) return null;
|
|
125
|
+
// Sanity check: the value should at least look like SVG markup. If it
|
|
126
|
+
// doesn't, refuse — `<svg ...>` is the only contract.
|
|
127
|
+
if (!/<svg\b/i.test(svg)) return null;
|
|
128
|
+
|
|
129
|
+
let cleaned = svg
|
|
130
|
+
.replace(SCRIPT_BLOCK_RE, '')
|
|
131
|
+
.replace(SCRIPT_OPEN_RE, '')
|
|
132
|
+
.replace(ON_HANDLER_RE, '')
|
|
133
|
+
// Replace dangerous javascript: URLs with empty strings, preserving
|
|
134
|
+
// the surrounding quote character so the attribute remains parseable.
|
|
135
|
+
.replace(JS_URL_RE, (_match, lead) => `${lead}`);
|
|
136
|
+
|
|
137
|
+
// Final guard: if any of the dangerous patterns survived (e.g.
|
|
138
|
+
// mismatched encoding tricks), reject the whole icon.
|
|
139
|
+
if (/<script\b/i.test(cleaned)) return null;
|
|
140
|
+
if (/\son[a-zA-Z]+\s*=/i.test(cleaned)) return null;
|
|
141
|
+
if (/javascript:/i.test(cleaned)) return null;
|
|
142
|
+
|
|
143
|
+
return cleaned;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve link configuration for a repo. Reads `repos[owner/repo].links`
|
|
148
|
+
* and produces a normalised object the frontend can act on without
|
|
149
|
+
* further interpretation.
|
|
150
|
+
*
|
|
151
|
+
* `links.github` and `links.graphite` are booleans that gate the default
|
|
152
|
+
* built-in links. Anything that isn't an explicit `false` (including
|
|
153
|
+
* `undefined` and `true`) leaves the link enabled — preserving current
|
|
154
|
+
* behaviour for repos that omit the `links` block entirely.
|
|
155
|
+
*
|
|
156
|
+
* For the external link, the icon is sanitised here, server-side. If
|
|
157
|
+
* sanitisation strips the icon entirely (e.g. the input was hostile or
|
|
158
|
+
* malformed), a warning is logged and `icon` becomes `null` — the link
|
|
159
|
+
* is still rendered, just without a custom icon.
|
|
160
|
+
*
|
|
161
|
+
* @param {Object} config
|
|
162
|
+
* @param {string} repository Canonical `owner/repo` identifier
|
|
163
|
+
* @returns {{ external: { label: string, url_template: string, icon: string|null }|null,
|
|
164
|
+
* github: boolean,
|
|
165
|
+
* graphite: boolean }}
|
|
166
|
+
*/
|
|
167
|
+
function resolveRepoLinks(config, repository) {
|
|
168
|
+
const result = { external: null, github: true, graphite: true };
|
|
169
|
+
if (!config || !repository) return result;
|
|
170
|
+
|
|
171
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
172
|
+
if (!repoConfig || typeof repoConfig !== 'object') return result;
|
|
173
|
+
|
|
174
|
+
const links = repoConfig.links;
|
|
175
|
+
if (!links || typeof links !== 'object') return result;
|
|
176
|
+
|
|
177
|
+
if (links.github === false) result.github = false;
|
|
178
|
+
if (links.graphite === false) result.graphite = false;
|
|
179
|
+
|
|
180
|
+
const ext = links.external;
|
|
181
|
+
if (ext && typeof ext === 'object'
|
|
182
|
+
&& typeof ext.label === 'string' && ext.label
|
|
183
|
+
&& typeof ext.url_template === 'string'
|
|
184
|
+
&& ext.url_template.startsWith('https://')) {
|
|
185
|
+
let icon = null;
|
|
186
|
+
if (ext.icon !== undefined && ext.icon !== null && ext.icon !== '') {
|
|
187
|
+
icon = sanitizeSvgIcon(ext.icon);
|
|
188
|
+
if (icon === null) {
|
|
189
|
+
logger.warn(
|
|
190
|
+
`Dropping links.external.icon for "${repository}" — failed sanitisation.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
result.external = {
|
|
195
|
+
// Optional host display name (e.g. "Meteorite"). When absent, the
|
|
196
|
+
// field is null and consumers fall back to "GitHub" via resolveHostName.
|
|
197
|
+
name: (typeof ext.name === 'string' && ext.name) ? ext.name : null,
|
|
198
|
+
label: ext.label,
|
|
199
|
+
url_template: ext.url_template,
|
|
200
|
+
icon
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the display name of the remote code host for a repo, for use in
|
|
209
|
+
* user-facing text in place of the literal "GitHub".
|
|
210
|
+
*
|
|
211
|
+
* Returns `repos[owner/repo].links.external.name` when configured, otherwise
|
|
212
|
+
* `"GitHub"`. This is the server-side counterpart to the frontend
|
|
213
|
+
* `window.RepoLinks.hostName()` accessor.
|
|
214
|
+
*
|
|
215
|
+
* @param {Object} config
|
|
216
|
+
* @param {string} repository Canonical `owner/repo` identifier
|
|
217
|
+
* @returns {string} The configured host name, or "GitHub" by default
|
|
218
|
+
*/
|
|
219
|
+
function resolveHostName(config, repository) {
|
|
220
|
+
const links = resolveRepoLinks(config, repository);
|
|
221
|
+
return (links.external && links.external.name) ? links.external.name : 'GitHub';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
substituteUrlTemplate,
|
|
226
|
+
sanitizeSvgIcon,
|
|
227
|
+
resolveRepoLinks,
|
|
228
|
+
resolveHostName,
|
|
229
|
+
ALLOWED_PLACEHOLDERS,
|
|
230
|
+
};
|