@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.
Files changed (93) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  5. package/public/css/analysis-config.css +1807 -0
  6. package/public/css/pr.css +1029 -2169
  7. package/public/index.html +11 -0
  8. package/public/js/components/AIPanel.js +39 -23
  9. package/public/js/components/AdvancedConfigTab.js +56 -4
  10. package/public/js/components/AnalysisConfigModal.js +41 -25
  11. package/public/js/components/ChatPanel.js +163 -3
  12. package/public/js/components/KeyboardShortcuts.js +10 -26
  13. package/public/js/components/ReviewModal.js +135 -13
  14. package/public/js/components/TourBar.js +248 -0
  15. package/public/js/components/VoiceCentricConfigTab.js +36 -0
  16. package/public/js/index.js +175 -16
  17. package/public/js/local.js +64 -8
  18. package/public/js/modules/cancel-background-job.js +183 -0
  19. package/public/js/modules/hunk-summary-renderer.js +116 -0
  20. package/public/js/modules/storage-cleanup.js +16 -0
  21. package/public/js/modules/suggestion-manager.js +25 -1
  22. package/public/js/modules/tour-renderer.js +755 -0
  23. package/public/js/pr.js +1826 -56
  24. package/public/js/repo-links.js +328 -0
  25. package/public/js/utils/modal-detection.js +77 -0
  26. package/public/js/utils/provider-model.js +88 -0
  27. package/public/js/utils/storage-keys.js +50 -0
  28. package/public/local.html +24 -0
  29. package/public/pr.html +24 -0
  30. package/public/repo-settings.html +1 -0
  31. package/public/setup.html +2 -0
  32. package/src/ai/abort-signal-wiring.js +130 -0
  33. package/src/ai/analyzer.js +125 -18
  34. package/src/ai/background-queue.js +290 -0
  35. package/src/ai/claude-cli.js +1 -1
  36. package/src/ai/claude-provider.js +50 -7
  37. package/src/ai/codex-provider.js +28 -5
  38. package/src/ai/copilot-provider.js +22 -3
  39. package/src/ai/cursor-agent-provider.js +22 -6
  40. package/src/ai/executable-provider.js +4 -19
  41. package/src/ai/gemini-provider.js +22 -5
  42. package/src/ai/hunk-hashing.js +161 -0
  43. package/src/ai/index.js +2 -0
  44. package/src/ai/opencode-provider.js +21 -5
  45. package/src/ai/pi-provider.js +21 -5
  46. package/src/ai/prompts/hunk-summary.js +199 -0
  47. package/src/ai/prompts/tour.js +232 -0
  48. package/src/ai/provider.js +21 -1
  49. package/src/ai/summary-generator.js +469 -0
  50. package/src/ai/tour-generator.js +568 -0
  51. package/src/config.js +778 -10
  52. package/src/database.js +282 -1
  53. package/src/external/github-adapter.js +114 -25
  54. package/src/git/base-branch.js +11 -4
  55. package/src/github/client.js +482 -588
  56. package/src/github/errors.js +55 -0
  57. package/src/github/impl/graphql/pending-review-comments.js +230 -0
  58. package/src/github/impl/graphql/pending-review.js +153 -0
  59. package/src/github/impl/graphql/review-lifecycle.js +161 -0
  60. package/src/github/impl/graphql/stack-walker.js +210 -0
  61. package/src/github/impl/host/pending-review-comments.js +338 -0
  62. package/src/github/impl/rest/pending-review.js +251 -0
  63. package/src/github/impl/rest/review-lifecycle.js +226 -0
  64. package/src/github/impl/rest/stack-walker.js +309 -0
  65. package/src/github/operations/pending-review-comments.js +79 -0
  66. package/src/github/operations/pending-review.js +89 -0
  67. package/src/github/operations/review-lifecycle.js +126 -0
  68. package/src/github/operations/stack-walker.js +87 -0
  69. package/src/github/parser.js +230 -4
  70. package/src/github/stack-walker.js +14 -189
  71. package/src/links/repo-links.js +230 -0
  72. package/src/local-review.js +201 -172
  73. package/src/main.js +133 -30
  74. package/src/routes/analyses.js +30 -7
  75. package/src/routes/bulk-analysis-configs.js +295 -0
  76. package/src/routes/config.js +118 -3
  77. package/src/routes/context-files.js +2 -29
  78. package/src/routes/external-comments.js +20 -10
  79. package/src/routes/github-collections.js +3 -1
  80. package/src/routes/local.js +410 -13
  81. package/src/routes/mcp.js +47 -4
  82. package/src/routes/middleware/validate-review-id.js +53 -0
  83. package/src/routes/pr.js +556 -71
  84. package/src/routes/reviews.js +145 -29
  85. package/src/routes/setup.js +8 -3
  86. package/src/routes/stack-analysis.js +33 -9
  87. package/src/routes/worktrees.js +3 -2
  88. package/src/server.js +2 -0
  89. package/src/setup/pr-setup.js +37 -11
  90. package/src/setup/stack-setup.js +13 -3
  91. package/src/single-port.js +6 -3
  92. package/src/utils/diff-hunks.js +65 -0
  93. 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
+ };