@in-the-loop-labs/pair-review 3.8.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -3
- 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/pr.css +12 -60
- package/public/js/components/CouncilProgressModal.js +0 -10
- package/public/js/local.js +27 -57
- package/public/js/pr.js +0 -114
- package/public/js/utils/provider-model.js +9 -2
- package/public/local.html +0 -7
- package/public/pr.html +0 -7
- package/src/ai/claude-provider.js +56 -34
- package/src/ai/index.js +8 -0
- package/src/ai/provider.js +225 -27
- package/src/councils/headless-council.js +13 -3
- package/src/database.js +52 -0
- package/src/git/worktree.js +7 -1
- package/src/interactive-analysis-config.js +152 -0
- package/src/local-review.js +43 -21
- package/src/main.js +953 -46
- package/src/mcp-stdio.js +40 -5
- package/src/review-config.js +164 -0
- package/src/routes/bulk-analysis-configs.js +45 -15
- package/src/routes/config.js +9 -4
- package/src/routes/executable-analysis.js +10 -1
- package/src/routes/local.js +170 -109
- package/src/routes/mcp.js +28 -31
- package/src/routes/pr.js +118 -56
- package/src/single-port.js +116 -6
- package/src/utils/logger.js +25 -4
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- [AI-Guided Review](#3-ai-guided-review-when-youre-accountable)
|
|
21
21
|
- [Quick Start](#quick-start)
|
|
22
22
|
- [Command Line Interface](#command-line-interface)
|
|
23
|
+
- [Headless analysis mode](#headless-analysis-mode)
|
|
23
24
|
- [Configuration](#configuration)
|
|
24
25
|
- [Environment Variables](#environment-variables)
|
|
25
26
|
- [GitHub Token](#github-token)
|
|
@@ -192,7 +193,11 @@ pair-review --local [path]
|
|
|
192
193
|
| `<PR-URL>` | Full GitHub PR URL (e.g., `https://github.com/owner/repo/pull/123`) |
|
|
193
194
|
| `--ai` | Automatically run AI analysis when the review loads |
|
|
194
195
|
| `--ai-draft` | Run AI analysis and save suggestions as a draft review on GitHub |
|
|
195
|
-
| `--
|
|
196
|
+
| `--headless` | Run AI analysis, store it in the local database, report the results, then exit. No server, no browser, no GitHub post. Implies analysis (does not require `--ai`). Works with a `<PR-number-or-URL>` or with `--local`. See [Headless analysis mode](#headless-analysis-mode). |
|
|
197
|
+
| `--json` | With `--headless`, emit the completed run plus its consolidated suggestions as a single JSON document on a clean stdout. For any outcome of the headless run (success, zero findings, or an error raised while running it), stdout carries a JSON document with an `ok` field; failures emit an `{ "ok": false, "error": … }` envelope on stdout with a non-zero exit. (Pre-flight config/startup failures are the exception: they exit non-zero with a stderr message and no JSON envelope — branch on the exit code first.) stderr is quiet by default (only warnings/errors) — add `--debug` for verbose progress logs. Only valid together with `--headless`. |
|
|
198
|
+
| `--instructions <text>` | Per-run custom instructions for this analysis. Applies to any mode that runs analysis — `--headless`, `--ai-draft`, `--ai-review` (consumed directly), and `--ai`/`--council` (carried into the browser-triggered analysis). Rejected with a clear error if no analysis mode is selected, so it is never silently dropped. Default: none. |
|
|
199
|
+
| `--instructions-file <path>` | Read per-run custom instructions from a file (5000-character cap). Mutually exclusive with `--instructions`. |
|
|
200
|
+
| `--council <handle>` | Run analysis with a saved multi-voice council. Implies analysis. The handle resolves by council name, name-slug, id (prefix), or a partial name fragment (resolving when it matches a single council, otherwise listing the candidates). When set, `--model` is ignored (council voices use their own per-voice models). Works in headless PR (`--ai-draft`/`--ai-review`/`--headless`), interactive PR (`--ai`), and local (`--local --ai`) modes. |
|
|
196
201
|
| `--list-councils` | List saved councils with their handles, names, types, and last-used repo, then exit. Use a printed handle with `--council`. |
|
|
197
202
|
| `--configure` | Show setup instructions and configuration options |
|
|
198
203
|
| `-d`, `--debug` | Enable verbose debug logging for troubleshooting |
|
|
@@ -214,6 +219,9 @@ pair-review 123 --ai # Auto-run AI analysis
|
|
|
214
219
|
pair-review --list-councils # List saved councils and their handles
|
|
215
220
|
pair-review 123 --ai-draft --council security-review # Headless draft with a council
|
|
216
221
|
pair-review --local --ai --council security-review # Local review with a council
|
|
222
|
+
pair-review --local --headless # Analyze local changes, print a summary, exit
|
|
223
|
+
pair-review --local --headless --json # Analyze local changes, emit JSON, exit
|
|
224
|
+
pair-review 123 --headless # Analyze a PR, print a summary, exit
|
|
217
225
|
pair-review --register # Register pair-review:// URL scheme (macOS)
|
|
218
226
|
pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
219
227
|
```
|
|
@@ -225,6 +233,149 @@ pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
|
225
233
|
> otherwise lists the candidates). Run `pair-review --list-councils` to discover
|
|
226
234
|
> available handles.
|
|
227
235
|
|
|
236
|
+
> **Repo default review config:** when neither `--council` nor `--model` is
|
|
237
|
+
> given, pair-review uses the repository's configured default — its default
|
|
238
|
+
> council if one is set, otherwise its default provider/model, otherwise the
|
|
239
|
+
> global config default. This applies to `--headless`, the submit modes, and the
|
|
240
|
+
> web UI's default **Analyze** action, so `--council`/`--model` are optional when
|
|
241
|
+
> a repo default is configured.
|
|
242
|
+
|
|
243
|
+
### Headless analysis mode
|
|
244
|
+
|
|
245
|
+
`--headless` runs an AI analysis (single provider or council), stores the results
|
|
246
|
+
in the local SQLite database exactly as the web UI does, reports them, and exits.
|
|
247
|
+
It never starts the server, opens a browser, or posts to GitHub — it is the
|
|
248
|
+
analysis core without the interactive or submit steps.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Analyze uncommitted local changes, print a human-readable summary
|
|
252
|
+
pair-review --local --headless
|
|
253
|
+
|
|
254
|
+
# Analyze a PR (no review is created on GitHub)
|
|
255
|
+
pair-review 123 --headless
|
|
256
|
+
|
|
257
|
+
# Use a council and per-run instructions
|
|
258
|
+
pair-review --local --headless --council security-review \
|
|
259
|
+
--instructions "Focus on auth and input validation."
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Exit codes:** a successful analysis exits `0` regardless of how many findings it
|
|
263
|
+
surfaces (zero findings is still success, with an empty `suggestions` array).
|
|
264
|
+
Non-zero exit is reserved for operational errors — invalid flags, an unreadable
|
|
265
|
+
`--instructions-file`, or an analysis failure.
|
|
266
|
+
|
|
267
|
+
**Machine-readable output (`--json`):** add `--json` to emit the completed run
|
|
268
|
+
plus its consolidated final suggestions as a single JSON document on stdout, so
|
|
269
|
+
stdout is a clean, parseable document suitable for scripting and AI coding
|
|
270
|
+
agents:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
pair-review --local --headless --json | jq '.suggestions | length'
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Because this mode is consumed mostly by coding agents — whose shell tools capture
|
|
277
|
+
stderr alongside stdout — **stderr is quiet by default**: only genuine warnings
|
|
278
|
+
and errors are emitted, not progress narration. Add `--debug` to restore the full
|
|
279
|
+
verbose log stream on stderr when diagnosing a run.
|
|
280
|
+
|
|
281
|
+
The JSON document has this shape (fields reflect the stored run and the
|
|
282
|
+
consolidated suggestion layer the app shows by default):
|
|
283
|
+
|
|
284
|
+
```jsonc
|
|
285
|
+
{
|
|
286
|
+
"ok": true, // false on failure (see below)
|
|
287
|
+
"mode": "local", // "pr" or "local"
|
|
288
|
+
"run": {
|
|
289
|
+
"id": "…", // analysis run id
|
|
290
|
+
"review_id": 1,
|
|
291
|
+
"provider": "claude", // for a council run: "council"
|
|
292
|
+
"model": "opus", // for a council run: the council id
|
|
293
|
+
"tier": "balanced",
|
|
294
|
+
"config_type": "standard", // or "council" / "advanced"
|
|
295
|
+
"status": "completed",
|
|
296
|
+
"summary": "…",
|
|
297
|
+
"total_suggestions": 3,
|
|
298
|
+
"files_analyzed": 5,
|
|
299
|
+
"started_at": "…",
|
|
300
|
+
"completed_at": "…",
|
|
301
|
+
"head_sha": "…",
|
|
302
|
+
"parent_run_id": null,
|
|
303
|
+
"custom_instructions": null,
|
|
304
|
+
"global_instructions": null,
|
|
305
|
+
"repo_instructions": null,
|
|
306
|
+
"request_instructions": "Focus on auth…", // from --instructions[-file]
|
|
307
|
+
"levels_config": { /* parsed */ },
|
|
308
|
+
"level_outcomes": { /* parsed */ }
|
|
309
|
+
},
|
|
310
|
+
"suggestions": [
|
|
311
|
+
{
|
|
312
|
+
"id": 42,
|
|
313
|
+
"ai_run_id": "…",
|
|
314
|
+
"ai_level": null, // consolidated layer (per-level rows excluded)
|
|
315
|
+
"ai_confidence": 0.9,
|
|
316
|
+
"file": "src/app.js",
|
|
317
|
+
"line_start": 120,
|
|
318
|
+
"line_end": 124,
|
|
319
|
+
"type": "bug",
|
|
320
|
+
"title": "…",
|
|
321
|
+
"body": "…",
|
|
322
|
+
"severity": "high",
|
|
323
|
+
"status": "active", // active or adopted
|
|
324
|
+
"is_file_level": 0,
|
|
325
|
+
"reasoning": { /* parsed */ },
|
|
326
|
+
"created_at": "…"
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
"count": 1 // suggestions.length
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**On failure** (invalid flags, an unreadable `--instructions-file`, or an
|
|
334
|
+
analysis error raised while running the headless flow) `--json` emits a JSON
|
|
335
|
+
document on stdout — a compact failure envelope — and the exit code is non-zero,
|
|
336
|
+
so a consumer parses one stream and branches on `ok`:
|
|
337
|
+
|
|
338
|
+
```jsonc
|
|
339
|
+
{
|
|
340
|
+
"ok": false,
|
|
341
|
+
"mode": "local", // "pr" or "local"
|
|
342
|
+
"error": { "message": "…" }
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The envelope covers errors raised once the headless run is under way. A fatal
|
|
347
|
+
*pre-flight* failure — an unreadable or malformed `~/.pair-review/config.json`, an
|
|
348
|
+
unwritable config directory, or an invalid port — exits non-zero with a message
|
|
349
|
+
on **stderr** and does **not** emit a JSON envelope on stdout. Branch on the exit
|
|
350
|
+
code first (as the example below does), then parse stdout.
|
|
351
|
+
|
|
352
|
+
**Agent workflow.** A coding agent can run a headless analysis against its own
|
|
353
|
+
uncommitted work, parse the JSON, and act on the suggestions:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
result=$(pair-review --local --headless --json \
|
|
357
|
+
--council security-review \
|
|
358
|
+
--instructions "Review the changes I just made for security issues.")
|
|
359
|
+
|
|
360
|
+
# Bail out on failure: non-zero exit, and `ok` is false in the JSON envelope.
|
|
361
|
+
if [ $? -ne 0 ] || [ "$(echo "$result" | jq -r '.ok')" != "true" ]; then
|
|
362
|
+
echo "analysis failed: $(echo "$result" | jq -r '.error.message')" >&2
|
|
363
|
+
exit 1
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
# Act on high-severity findings
|
|
367
|
+
echo "$result" | jq -r '.suggestions[]
|
|
368
|
+
| select(.severity == "high")
|
|
369
|
+
| "\(.file):\(.line_start) [\(.type)] \(.title)"'
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Once the run starts, `--json` produces a JSON document on stdout for any outcome
|
|
373
|
+
— `{ "ok": true, … }` on success (any number of findings, including zero) and
|
|
374
|
+
`{ "ok": false, … }` with a non-zero exit on an error raised while running it —
|
|
375
|
+
so the agent reads exactly one stream and branches on `ok`. Because a pre-flight
|
|
376
|
+
config/startup failure is the one case that exits without a JSON envelope,
|
|
377
|
+
checking the exit code first (as above) covers every outcome.
|
|
378
|
+
|
|
228
379
|
## Configuration
|
|
229
380
|
|
|
230
381
|
On first run, pair-review will prompt you to configure the application.
|
|
@@ -372,11 +523,11 @@ You can override provider settings and define custom models in your config file.
|
|
|
372
523
|
"command": "opencode",
|
|
373
524
|
"extra_args": ["--verbose"],
|
|
374
525
|
"env": { "OPENCODE_TELEMETRY": "off" },
|
|
526
|
+
"default_model": "anthropic/claude-sonnet-4",
|
|
375
527
|
"models": [
|
|
376
528
|
{
|
|
377
529
|
"id": "anthropic/claude-sonnet-4",
|
|
378
530
|
"tier": "balanced",
|
|
379
|
-
"default": true,
|
|
380
531
|
"name": "Claude Sonnet 4",
|
|
381
532
|
"description": "Fast and capable for most reviews",
|
|
382
533
|
"tagline": "Best balance of speed and quality"
|
|
@@ -410,6 +561,8 @@ You can override provider settings and define custom models in your config file.
|
|
|
410
561
|
| `availability_timeout_seconds` | Seconds to allow for the startup availability probe before the provider is reported unavailable (default `10`). Raise it for providers whose check runs a slow build/compile step. Also supported per chat provider under `chat_providers.<id>`. |
|
|
411
562
|
| `availability_command` | Command run to decide availability. Executable providers default to always-available when omitted; chat providers fall back to `<command> --version` (or, for the built-in Pi, the cached AI-provider status). Pair with `availability_timeout_seconds` when the probe runs a slow build. |
|
|
412
563
|
| `models` | Array of model definitions (see below) |
|
|
564
|
+
| `default_model` | Model `id` to use as the provider's default (in the picker and when no model is specified). Preferred over the per-model `default: true` flag. If it names an unknown or disabled model, the provider falls back to automatic selection. |
|
|
565
|
+
| `disabled_models` | Array of model selectors to hide, matched by model `id` or alias. Disabled models disappear from the model picker for that provider. Works on built-in models too, so you can remove a built-in option without redefining the rest. A list that would hide *every* model is ignored. |
|
|
413
566
|
|
|
414
567
|
#### Model Configuration Fields
|
|
415
568
|
|
|
@@ -422,10 +575,29 @@ You can override provider settings and define custom models in your config file.
|
|
|
422
575
|
| `tagline` | No | Short description shown in model picker |
|
|
423
576
|
| `badge` | No | Badge text (e.g., "NEW", "BETA") |
|
|
424
577
|
| `badgeClass` | No | CSS class for badge styling |
|
|
425
|
-
| `default` | No | Set to `true` to make this the default model for the provider |
|
|
578
|
+
| `default` | No | **Deprecated** — use the provider-level `default_model` field instead. Set to `true` to make this the default model for the provider. Still honored for backward compatibility, but `default_model` takes precedence when both are present. |
|
|
426
579
|
| `extra_args` | No | Model-specific CLI arguments |
|
|
427
580
|
| `env` | No | Model-specific environment variables |
|
|
428
581
|
|
|
582
|
+
#### Choosing and Hiding Models
|
|
583
|
+
|
|
584
|
+
Use the provider-level `default_model` and `disabled_models` fields to tailor which models a provider exposes — these work on **built-in** models too, so you can adjust a provider without redefining its entire model list.
|
|
585
|
+
|
|
586
|
+
```json
|
|
587
|
+
{
|
|
588
|
+
"providers": {
|
|
589
|
+
"claude": {
|
|
590
|
+
"default_model": "sonnet-4.6",
|
|
591
|
+
"disabled_models": ["haiku", "fable"]
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
- `disabled_models` removes those models from the picker. It matches by model id or alias, so either a canonical id (`opus-4.8-xhigh`) or a convenience alias (`opus`) works. To find the built-in IDs for a provider, see the comments in `config.example.json` (e.g. Claude ships `opus-4.8-xhigh`, `sonnet-4.6`, `haiku`, …).
|
|
598
|
+
- `default_model` selects which of the *remaining* models is the default. If it points at a model that is unknown or also disabled, the provider falls back to automatic selection (the model marked `default: true`, then the first `balanced`-tier model, then the first model).
|
|
599
|
+
- Prefer `default_model` over the per-model `default: true` flag, which is deprecated. Setting `default_model` suppresses the deprecation warning.
|
|
600
|
+
|
|
429
601
|
#### Model Tiers
|
|
430
602
|
|
|
431
603
|
Models are grouped by tier to help users choose appropriately:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.1",
|
|
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": "3.
|
|
3
|
+
"version": "3.9.1",
|
|
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",
|
package/public/css/pr.css
CHANGED
|
@@ -6939,66 +6939,6 @@ body:not([data-theme="dark"]) .theme-icon-light {
|
|
|
6939
6939
|
border-color: var(--color-border-secondary);
|
|
6940
6940
|
}
|
|
6941
6941
|
|
|
6942
|
-
/* ============================================
|
|
6943
|
-
Analysis Progress Dots
|
|
6944
|
-
============================================ */
|
|
6945
|
-
|
|
6946
|
-
.analysis-progress-dots {
|
|
6947
|
-
display: flex;
|
|
6948
|
-
align-items: center;
|
|
6949
|
-
gap: 6px;
|
|
6950
|
-
margin-right: 8px;
|
|
6951
|
-
}
|
|
6952
|
-
|
|
6953
|
-
.progress-dot {
|
|
6954
|
-
width: 8px;
|
|
6955
|
-
height: 8px;
|
|
6956
|
-
border-radius: 50%;
|
|
6957
|
-
border: 1.5px solid var(--color-accent-ai);
|
|
6958
|
-
background: transparent;
|
|
6959
|
-
transition: all 0.3s ease;
|
|
6960
|
-
}
|
|
6961
|
-
|
|
6962
|
-
.progress-dot.active {
|
|
6963
|
-
animation: pulse-dot 1.2s ease-in-out infinite;
|
|
6964
|
-
}
|
|
6965
|
-
|
|
6966
|
-
.progress-dot.completed {
|
|
6967
|
-
background: var(--color-accent-ai);
|
|
6968
|
-
border-color: var(--color-accent-ai);
|
|
6969
|
-
}
|
|
6970
|
-
|
|
6971
|
-
.progress-dot.error {
|
|
6972
|
-
background: #ef4444;
|
|
6973
|
-
border-color: #ef4444;
|
|
6974
|
-
}
|
|
6975
|
-
|
|
6976
|
-
@keyframes pulse-dot {
|
|
6977
|
-
0%, 100% {
|
|
6978
|
-
transform: scale(1);
|
|
6979
|
-
opacity: 1;
|
|
6980
|
-
}
|
|
6981
|
-
50% {
|
|
6982
|
-
transform: scale(1.3);
|
|
6983
|
-
opacity: 0.9;
|
|
6984
|
-
}
|
|
6985
|
-
}
|
|
6986
|
-
|
|
6987
|
-
/* Dark theme for progress dots */
|
|
6988
|
-
[data-theme="dark"] .progress-dot {
|
|
6989
|
-
border-color: var(--color-accent-ai-dark);
|
|
6990
|
-
}
|
|
6991
|
-
|
|
6992
|
-
[data-theme="dark"] .progress-dot.completed {
|
|
6993
|
-
background: var(--color-accent-ai-dark);
|
|
6994
|
-
border-color: var(--color-accent-ai-dark);
|
|
6995
|
-
}
|
|
6996
|
-
|
|
6997
|
-
[data-theme="dark"] .progress-dot.error {
|
|
6998
|
-
background: #f87171;
|
|
6999
|
-
border-color: #f87171;
|
|
7000
|
-
}
|
|
7001
|
-
|
|
7002
6942
|
/* Magical Analyze Button - Amber AI CTA */
|
|
7003
6943
|
#analyze-btn {
|
|
7004
6944
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
@@ -12959,6 +12899,18 @@ body.resizing * {
|
|
|
12959
12899
|
animation: pulse-dot 1.2s ease-in-out infinite;
|
|
12960
12900
|
}
|
|
12961
12901
|
|
|
12902
|
+
/* Pulse used by the running/setting-up stack-progress status icons above. */
|
|
12903
|
+
@keyframes pulse-dot {
|
|
12904
|
+
0%, 100% {
|
|
12905
|
+
transform: scale(1);
|
|
12906
|
+
opacity: 1;
|
|
12907
|
+
}
|
|
12908
|
+
50% {
|
|
12909
|
+
transform: scale(1.3);
|
|
12910
|
+
opacity: 0.9;
|
|
12911
|
+
}
|
|
12912
|
+
}
|
|
12913
|
+
|
|
12962
12914
|
.status-pending .stack-progress-status-icon {
|
|
12963
12915
|
color: var(--color-text-muted, #d1d9e0);
|
|
12964
12916
|
}
|
|
@@ -304,16 +304,6 @@ class CouncilProgressModal {
|
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
// Update toolbar progress dots
|
|
308
|
-
const manager = window.prManager || window.localManager;
|
|
309
|
-
if (manager?.updateProgressDot) {
|
|
310
|
-
for (let level = 1; level <= 4; level++) {
|
|
311
|
-
if (status.levels[level]) {
|
|
312
|
-
manager.updateProgressDot(level, status.levels[level].status);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
307
|
// Terminal states
|
|
318
308
|
if (status.status === 'completed') {
|
|
319
309
|
this._handleCompletion(status);
|
package/public/js/local.js
CHANGED
|
@@ -72,20 +72,39 @@ class LocalManager {
|
|
|
72
72
|
// Auto-trigger analysis if ?analyze=true is present
|
|
73
73
|
const autoAnalyze = new URLSearchParams(window.location.search).get('analyze');
|
|
74
74
|
if (autoAnalyze === 'true' && !window.prManager.isAnalyzing) {
|
|
75
|
+
const manager = window.prManager;
|
|
75
76
|
try {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// Prefer a stashed bulk-analysis config (threaded via analysisConfigId).
|
|
78
|
+
// The CLI uses it to carry --instructions plus the resolved
|
|
79
|
+
// provider/model or council snapshot into the browser auto-analyze;
|
|
80
|
+
// mirrors PRManager._maybeAutoAnalyze.
|
|
81
|
+
const storedConfig = await manager._fetchAutoAnalysisConfigFromUrl();
|
|
82
|
+
let config;
|
|
83
|
+
if (storedConfig.requested) {
|
|
84
|
+
if (!storedConfig.config) {
|
|
85
|
+
// The stored config expired (TTL/eviction/restart). The diff has
|
|
86
|
+
// already rendered, so leave the review usable for manual analysis
|
|
87
|
+
// rather than failing; warn and bail.
|
|
88
|
+
const message = 'Could not load the selected analysis settings. Start analysis manually to choose new settings.';
|
|
89
|
+
if (window.toast) window.toast.showWarning(message);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
config = storedConfig.config;
|
|
93
|
+
} else {
|
|
94
|
+
// Fetch repo settings so we honour the repository's default provider/council
|
|
95
|
+
const [repoSettings, reviewSettings, appConfig] = await Promise.all([
|
|
96
|
+
manager.fetchRepoSettings().catch(() => null),
|
|
97
|
+
manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
|
|
98
|
+
manager._getAppConfig()
|
|
99
|
+
]);
|
|
100
|
+
config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig);
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
await this.startLocalAnalysis(null, config);
|
|
86
104
|
} finally {
|
|
87
105
|
const cleanUrl = new URL(window.location);
|
|
88
106
|
cleanUrl.searchParams.delete('analyze');
|
|
107
|
+
cleanUrl.searchParams.delete('analysisConfigId');
|
|
89
108
|
cleanUrl.searchParams.delete('council');
|
|
90
109
|
history.replaceState(null, '', cleanUrl);
|
|
91
110
|
}
|
|
@@ -1874,55 +1893,6 @@ class LocalManager {
|
|
|
1874
1893
|
};
|
|
1875
1894
|
document.addEventListener('keydown', keyHandler);
|
|
1876
1895
|
}
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* Get the progress dots container element
|
|
1880
|
-
* @returns {HTMLElement|null}
|
|
1881
|
-
*/
|
|
1882
|
-
getProgressDotsContainer() {
|
|
1883
|
-
return document.getElementById('analysis-progress-dots');
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
/**
|
|
1887
|
-
* Update a specific progress dot during analysis
|
|
1888
|
-
* Maps level numbers to phase names:
|
|
1889
|
-
* - Level 4 -> orchestration (finalization)
|
|
1890
|
-
* - Level 1 -> level1
|
|
1891
|
-
* - Level 2 -> level2
|
|
1892
|
-
* - Level 3 -> level3
|
|
1893
|
-
* @param {number} level - The level number (1, 2, 3, or 4)
|
|
1894
|
-
* @param {string} status - The status ('running', 'completed', 'failed')
|
|
1895
|
-
*/
|
|
1896
|
-
updateProgressDot(level, status) {
|
|
1897
|
-
const container = this.getProgressDotsContainer();
|
|
1898
|
-
if (!container) return;
|
|
1899
|
-
|
|
1900
|
-
// Map levels to dot phases
|
|
1901
|
-
const phaseMap = {
|
|
1902
|
-
4: 'orchestration', // Orchestration/finalization is level 4 in progress modal
|
|
1903
|
-
1: 'level1',
|
|
1904
|
-
2: 'level2',
|
|
1905
|
-
3: 'level3'
|
|
1906
|
-
};
|
|
1907
|
-
|
|
1908
|
-
const phase = phaseMap[level];
|
|
1909
|
-
if (!phase) return;
|
|
1910
|
-
|
|
1911
|
-
const dot = container.querySelector(`[data-phase="${phase}"]`);
|
|
1912
|
-
if (!dot) return;
|
|
1913
|
-
|
|
1914
|
-
// Remove existing states
|
|
1915
|
-
dot.classList.remove('active', 'completed', 'error');
|
|
1916
|
-
|
|
1917
|
-
// Apply new state
|
|
1918
|
-
if (status === 'running') {
|
|
1919
|
-
dot.classList.add('active');
|
|
1920
|
-
} else if (status === 'completed') {
|
|
1921
|
-
dot.classList.add('completed');
|
|
1922
|
-
} else if (status === 'failed') {
|
|
1923
|
-
dot.classList.add('error');
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
1896
|
}
|
|
1927
1897
|
|
|
1928
1898
|
// Initialize LocalManager when in local mode
|
package/public/js/pr.js
CHANGED
|
@@ -6588,9 +6588,6 @@ class PRManager {
|
|
|
6588
6588
|
} else {
|
|
6589
6589
|
btn.innerHTML = '<span class="analyzing-icon">✨</span> Analyzing...';
|
|
6590
6590
|
}
|
|
6591
|
-
|
|
6592
|
-
// Show progress dots
|
|
6593
|
-
this.showProgressDots();
|
|
6594
6591
|
}
|
|
6595
6592
|
|
|
6596
6593
|
/**
|
|
@@ -6615,9 +6612,6 @@ class PRManager {
|
|
|
6615
6612
|
}
|
|
6616
6613
|
btn.disabled = true;
|
|
6617
6614
|
|
|
6618
|
-
// Complete all progress dots (they'll be hidden when button resets)
|
|
6619
|
-
this.completeAllProgressDots();
|
|
6620
|
-
|
|
6621
6615
|
// Revert to normal after 2 seconds
|
|
6622
6616
|
setTimeout(() => this.resetButton(), 2000);
|
|
6623
6617
|
}
|
|
@@ -6645,114 +6639,6 @@ class PRManager {
|
|
|
6645
6639
|
} else {
|
|
6646
6640
|
btn.innerHTML = 'Analyze with AI';
|
|
6647
6641
|
}
|
|
6648
|
-
|
|
6649
|
-
// Hide progress dots when resetting
|
|
6650
|
-
this.hideProgressDots();
|
|
6651
|
-
}
|
|
6652
|
-
|
|
6653
|
-
// ============================================
|
|
6654
|
-
// Progress Dots Controller
|
|
6655
|
-
// ============================================
|
|
6656
|
-
|
|
6657
|
-
/**
|
|
6658
|
-
* Get the progress dots container
|
|
6659
|
-
* @returns {HTMLElement|null}
|
|
6660
|
-
*/
|
|
6661
|
-
getProgressDotsContainer() {
|
|
6662
|
-
return document.getElementById('analysis-progress-dots');
|
|
6663
|
-
}
|
|
6664
|
-
|
|
6665
|
-
/**
|
|
6666
|
-
* Show progress dots (called when analysis starts)
|
|
6667
|
-
*/
|
|
6668
|
-
showProgressDots() {
|
|
6669
|
-
const container = this.getProgressDotsContainer();
|
|
6670
|
-
if (!container) return;
|
|
6671
|
-
|
|
6672
|
-
container.style.display = 'flex';
|
|
6673
|
-
|
|
6674
|
-
// Reset all dots to initial state
|
|
6675
|
-
const dots = container.querySelectorAll('.progress-dot');
|
|
6676
|
-
dots.forEach(dot => {
|
|
6677
|
-
dot.classList.remove('active', 'completed', 'error');
|
|
6678
|
-
});
|
|
6679
|
-
|
|
6680
|
-
// Set first dot (orchestration) as active
|
|
6681
|
-
const firstDot = container.querySelector('[data-phase="orchestration"]');
|
|
6682
|
-
if (firstDot) {
|
|
6683
|
-
firstDot.classList.add('active');
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
|
-
|
|
6687
|
-
/**
|
|
6688
|
-
* Hide progress dots (called when analysis completes or is cancelled)
|
|
6689
|
-
*/
|
|
6690
|
-
hideProgressDots() {
|
|
6691
|
-
const container = this.getProgressDotsContainer();
|
|
6692
|
-
if (!container) return;
|
|
6693
|
-
|
|
6694
|
-
container.style.display = 'none';
|
|
6695
|
-
|
|
6696
|
-
// Reset all dots
|
|
6697
|
-
const dots = container.querySelectorAll('.progress-dot');
|
|
6698
|
-
dots.forEach(dot => {
|
|
6699
|
-
dot.classList.remove('active', 'completed', 'error');
|
|
6700
|
-
});
|
|
6701
|
-
}
|
|
6702
|
-
|
|
6703
|
-
/**
|
|
6704
|
-
* Update progress dots based on level status
|
|
6705
|
-
* Maps level numbers to phases:
|
|
6706
|
-
* - Level 4 (orchestration/finalization) -> orchestration
|
|
6707
|
-
* - Level 1 -> level1
|
|
6708
|
-
* - Level 2 -> level2
|
|
6709
|
-
* - Level 3 -> level3
|
|
6710
|
-
* @param {number} level - The level number (1, 2, 3, or 4)
|
|
6711
|
-
* @param {string} status - The status ('running', 'completed', 'failed')
|
|
6712
|
-
*/
|
|
6713
|
-
updateProgressDot(level, status) {
|
|
6714
|
-
const container = this.getProgressDotsContainer();
|
|
6715
|
-
if (!container) return;
|
|
6716
|
-
|
|
6717
|
-
// Map levels to dot phases
|
|
6718
|
-
const phaseMap = {
|
|
6719
|
-
4: 'orchestration', // Orchestration/finalization is level 4 in progress modal
|
|
6720
|
-
1: 'level1',
|
|
6721
|
-
2: 'level2',
|
|
6722
|
-
3: 'level3'
|
|
6723
|
-
};
|
|
6724
|
-
|
|
6725
|
-
const phase = phaseMap[level];
|
|
6726
|
-
if (!phase) return;
|
|
6727
|
-
|
|
6728
|
-
const dot = container.querySelector(`[data-phase="${phase}"]`);
|
|
6729
|
-
if (!dot) return;
|
|
6730
|
-
|
|
6731
|
-
// Remove existing states
|
|
6732
|
-
dot.classList.remove('active', 'completed', 'error');
|
|
6733
|
-
|
|
6734
|
-
// Apply new state
|
|
6735
|
-
if (status === 'running') {
|
|
6736
|
-
dot.classList.add('active');
|
|
6737
|
-
} else if (status === 'completed') {
|
|
6738
|
-
dot.classList.add('completed');
|
|
6739
|
-
} else if (status === 'failed') {
|
|
6740
|
-
dot.classList.add('error');
|
|
6741
|
-
}
|
|
6742
|
-
}
|
|
6743
|
-
|
|
6744
|
-
/**
|
|
6745
|
-
* Complete all progress dots (called briefly before hiding)
|
|
6746
|
-
*/
|
|
6747
|
-
completeAllProgressDots() {
|
|
6748
|
-
const container = this.getProgressDotsContainer();
|
|
6749
|
-
if (!container) return;
|
|
6750
|
-
|
|
6751
|
-
const dots = container.querySelectorAll('.progress-dot');
|
|
6752
|
-
dots.forEach(dot => {
|
|
6753
|
-
dot.classList.remove('active', 'error');
|
|
6754
|
-
dot.classList.add('completed');
|
|
6755
|
-
});
|
|
6756
6642
|
}
|
|
6757
6643
|
|
|
6758
6644
|
/**
|
|
@@ -19,13 +19,20 @@
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {Object} providerInfo - One entry from /api/providers
|
|
21
21
|
* @param {string} modelId
|
|
22
|
-
* @returns {boolean} Whether modelId is one of the provider's models
|
|
22
|
+
* @returns {boolean} Whether modelId is one of the provider's models, matched by
|
|
23
|
+
* canonical id OR any of its aliases. A persisted/configured value may name an
|
|
24
|
+
* alias (e.g. `opus`) rather than the canonical id; matching aliases keeps the
|
|
25
|
+
* user's choice instead of silently falling back to the provider default. The
|
|
26
|
+
* model cards carry their `aliases` in the /api/providers payload. Mirrors the
|
|
27
|
+
* backend `modelMatches()` helper in src/ai/provider.js.
|
|
23
28
|
*/
|
|
24
29
|
function _modelBelongsToProvider(providerInfo, modelId) {
|
|
25
30
|
return !!(
|
|
26
31
|
providerInfo &&
|
|
27
32
|
Array.isArray(providerInfo.models) &&
|
|
28
|
-
|
|
33
|
+
// Optional chaining: a model with no `aliases` short-circuits to undefined
|
|
34
|
+
// (falsy) rather than throwing, so no Array.isArray guard is needed.
|
|
35
|
+
providerInfo.models.some(m => m && (m.id === modelId || m.aliases?.includes(modelId)))
|
|
29
36
|
);
|
|
30
37
|
}
|
|
31
38
|
|
package/public/local.html
CHANGED
|
@@ -435,13 +435,6 @@
|
|
|
435
435
|
</div>
|
|
436
436
|
<div class="diff-stats" id="diff-stats"></div>
|
|
437
437
|
<div class="toolbar-actions">
|
|
438
|
-
<!-- Progress Dots (visible during analysis) -->
|
|
439
|
-
<div class="analysis-progress-dots" id="analysis-progress-dots" style="display: none;">
|
|
440
|
-
<span class="progress-dot" data-phase="level1" title="Level 1: Diff"></span>
|
|
441
|
-
<span class="progress-dot" data-phase="level2" title="Level 2: File"></span>
|
|
442
|
-
<span class="progress-dot" data-phase="level3" title="Level 3: Codebase"></span>
|
|
443
|
-
<span class="progress-dot" data-phase="orchestration" title="Consolidation"></span>
|
|
444
|
-
</div>
|
|
445
438
|
<button class="btn btn-sm btn-secondary" id="analyze-btn" title="Analyze with AI">
|
|
446
439
|
<svg class="analyze-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
447
440
|
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/>
|
package/public/pr.html
CHANGED
|
@@ -231,13 +231,6 @@
|
|
|
231
231
|
</div>
|
|
232
232
|
<div class="diff-stats" id="diff-stats"></div>
|
|
233
233
|
<div class="toolbar-actions">
|
|
234
|
-
<!-- Progress Dots (visible during analysis) -->
|
|
235
|
-
<div class="analysis-progress-dots" id="analysis-progress-dots" style="display: none;">
|
|
236
|
-
<span class="progress-dot" data-phase="level1" title="Level 1: Diff"></span>
|
|
237
|
-
<span class="progress-dot" data-phase="level2" title="Level 2: File"></span>
|
|
238
|
-
<span class="progress-dot" data-phase="level3" title="Level 3: Codebase"></span>
|
|
239
|
-
<span class="progress-dot" data-phase="orchestration" title="Consolidation"></span>
|
|
240
|
-
</div>
|
|
241
234
|
<button class="btn btn-sm btn-secondary" id="analyze-btn" title="Analyze with AI">
|
|
242
235
|
<svg class="analyze-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
243
236
|
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/>
|