prreviewbuddy 0.31.0 → 0.34.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/CHANGELOG.md +77 -0
- package/dist/{delete_review-C2wax0ED.js → delete_review-TNIFNct3.js} +249 -7
- package/dist/main.js +25 -13
- package/dist/server.js +466 -75
- package/package.json +1 -1
- package/static/chunk-B80S6zbH.js +4 -4
- package/static/chunk-CBxYYqda.js +1 -0
- package/static/chunk-CUFGKuJi.js +1 -0
- package/static/chunk-X60fsCZA.js +1 -0
- package/static/notifications.js +1 -0
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +848 -412
- package/static/workspace.js +41 -41
- package/static/chunk-CIsM3PMS.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.34.0
|
|
4
|
+
|
|
5
|
+
**The Guide shows one theme at a time.** The workspace Guide used to stack every theme in one
|
|
6
|
+
column, so reading the second one meant scrolling past the whole of the first. The rail already
|
|
7
|
+
lists the themes, so it now drives the page: choosing one shows that theme on its own, and the
|
|
8
|
+
rail says how long each is likely to take. The theme tag on an issue or question opens its theme,
|
|
9
|
+
which it always named and never did. In a window too narrow for the rail, the same themes appear
|
|
10
|
+
above the theme itself.
|
|
11
|
+
|
|
12
|
+
A theme now reads in the order the work happens: why it matters, then what to check, then the files
|
|
13
|
+
it touches. That order is better on both surfaces, so the browser extension's Guide gets it too, and
|
|
14
|
+
is unchanged otherwise. Its parts stand apart rather than running together, and the theme itself is
|
|
15
|
+
no longer drawn as a card: with one theme on the page, the outline had nothing on the other side of
|
|
16
|
+
it. Nothing folds a theme away any more either, since there is now nothing behind it to reveal.
|
|
17
|
+
|
|
18
|
+
**The rail says what is left, in words.** A theme row gives the time it takes and, when something is
|
|
19
|
+
still open on it, how much: "~22 min, 1 of 2 issues left". That count used to be a bare digit whose
|
|
20
|
+
meaning only a screen reader was told. The file count has gone from the rail, which is what leaves
|
|
21
|
+
room to spell the rest out. Clicking the words "Review path" folds the list, not just the caret.
|
|
22
|
+
|
|
23
|
+
**The Overview's numbers read as numbers.** Each stat card leads with the figure and sets the label
|
|
24
|
+
beside it, so the four cards line up whether or not one of them carries a second line.
|
|
25
|
+
|
|
26
|
+
Two things that were wrong wherever you looked. The document pane was painted a slightly different
|
|
27
|
+
grey from the page behind it, leaving a seam down every view. And in a window too narrow for the
|
|
28
|
+
rail, the review was squeezed into the width the rail had vacated, with half the window empty beside
|
|
29
|
+
it.
|
|
30
|
+
|
|
31
|
+
The progress line no longer counts what it cannot change: completing a theme is part of guided
|
|
32
|
+
review, which the workspace does not have, so the Guide states the size of the job rather than
|
|
33
|
+
reporting nothing done.
|
|
34
|
+
|
|
35
|
+
## 0.33.0
|
|
36
|
+
|
|
37
|
+
**A type scale, a palette for the review, and fewer boxes.** Text in the workspace now comes from
|
|
38
|
+
seven named sizes rather than fifteen ad-hoc ones, so a heading, a sentence, a piece of metadata and
|
|
39
|
+
a status chip differ by size instead of by weight, and ordinary text is ordinary weight again. The
|
|
40
|
+
review itself now has its own light palette: three greys where one grey had been written several
|
|
41
|
+
ways, a single hairline where six different border shades were drawing the same line, and one purple
|
|
42
|
+
where there were five. Seven panels that were a box drawn inside another box have lost their
|
|
43
|
+
outline and sit on the page as white cards against a slightly deeper background. Purple now marks
|
|
44
|
+
what you have selected, what has keyboard focus, and the main action on a page, so hovering a row no
|
|
45
|
+
longer shouts as loudly as choosing one. The browser extension is unchanged.
|
|
46
|
+
|
|
47
|
+
## 0.32.1
|
|
48
|
+
|
|
49
|
+
**The rail on every workspace page.** The reviews list, Notifications and Settings now carry the
|
|
50
|
+
same rail a review does, so Reviews, Notifications and Settings are always one click away without
|
|
51
|
+
opening a review first. A review's rail keeps everything it had, with the workspace places beneath
|
|
52
|
+
it. The unread count sits beside Notifications wherever the rail is. In a window too narrow for the
|
|
53
|
+
rail, the same places appear in the header instead.
|
|
54
|
+
|
|
55
|
+
## 0.32.0
|
|
56
|
+
|
|
57
|
+
**Notifications.** When a review, reanalysis, update or KISS analysis finishes or fails, PR Review
|
|
58
|
+
Buddy now records it on a new Notifications page in the workspace, so you can start a review, or a
|
|
59
|
+
batch of them, get on with something else, and see what finished when you come back. The reviews
|
|
60
|
+
list shows how many are unread, and every review's rail links to the page. Opening a notification
|
|
61
|
+
takes you to its review and marks it read, and Mark all read clears the rest; visiting the review
|
|
62
|
+
some other way leaves it unread. A review you stop with Ctrl-C is not recorded as a failure.
|
|
63
|
+
Notifications are kept for thirty days, on this machine only, and nothing about them is sent
|
|
64
|
+
anywhere.
|
|
65
|
+
|
|
66
|
+
**Toasts in the workspace.** While any workspace page is open, a notification also appears for a
|
|
67
|
+
few seconds in the bottom-right corner, with Open beside it. It stays while your pointer is on it,
|
|
68
|
+
and no more than three show at once: when a batch finishes together, the last one says how many more
|
|
69
|
+
there are, with View all. Letting a toast go or dismissing it leaves the notification unread on the
|
|
70
|
+
Notifications page.
|
|
71
|
+
|
|
72
|
+
**Closing the terminal no longer strands a review.** The CLI used to say a review continues if its
|
|
73
|
+
terminal closes. It did not: the review stopped with the terminal, and running the command again
|
|
74
|
+
said it was still being made for the next half hour. The message now asks you to keep the terminal
|
|
75
|
+
open, and running the same command again picks the review up at once from the step it reached.
|
|
76
|
+
|
|
77
|
+
Also fixed: a review picked up part way through its analysis showed that step as finished while it
|
|
78
|
+
was still running.
|
|
79
|
+
|
|
3
80
|
## 0.31.0
|
|
4
81
|
|
|
5
82
|
**Review a single commit.** `prreviewbuddy review --commit <commit>` reviews the change one commit
|
|
@@ -2759,7 +2759,7 @@ async function resolveTarget(input) {
|
|
|
2759
2759
|
...named
|
|
2760
2760
|
};
|
|
2761
2761
|
}
|
|
2762
|
-
const branch = await branchOf(root);
|
|
2762
|
+
const branch = await branchOf$1(root);
|
|
2763
2763
|
let sha;
|
|
2764
2764
|
try {
|
|
2765
2765
|
sha = (await git(root, ["rev-parse", "HEAD"])).trim();
|
|
@@ -2941,7 +2941,7 @@ async function refuseEmptyTarget(target) {
|
|
|
2941
2941
|
async function uncommittedIn(root, target) {
|
|
2942
2942
|
if (target.pullRequest) return 0;
|
|
2943
2943
|
try {
|
|
2944
|
-
if ((await git(root, ["rev-parse", "HEAD"])).trim() !== target.sha || await branchOf(root) !== target.branch) return 0;
|
|
2944
|
+
if ((await git(root, ["rev-parse", "HEAD"])).trim() !== target.sha || await branchOf$1(root) !== target.branch) return 0;
|
|
2945
2945
|
return (await git(root, ["status", "--porcelain"])).split("\n").filter((line) => line.trim()).length;
|
|
2946
2946
|
} catch {
|
|
2947
2947
|
return 0;
|
|
@@ -3023,7 +3023,7 @@ async function repositoryRoot(repoPath) {
|
|
|
3023
3023
|
} catch {}
|
|
3024
3024
|
throw new TargetError(`${repoPath} is not inside a git repository.`);
|
|
3025
3025
|
}
|
|
3026
|
-
async function branchOf(root) {
|
|
3026
|
+
async function branchOf$1(root) {
|
|
3027
3027
|
let branch = "";
|
|
3028
3028
|
try {
|
|
3029
3029
|
branch = (await git(root, [
|
|
@@ -4766,6 +4766,18 @@ function isClaimed(jobId, now = Date.now()) {
|
|
|
4766
4766
|
return liveClaim(jobId, now) !== null;
|
|
4767
4767
|
}
|
|
4768
4768
|
/**
|
|
4769
|
+
* Whether a driver took this job and is no longer there to finish it.
|
|
4770
|
+
*
|
|
4771
|
+
* The claim file is the evidence. A job minted a moment ago and not yet driven has no claim at all,
|
|
4772
|
+
* and a driver that finished released its claim on the way out, so a claim that is still on disk
|
|
4773
|
+
* with nobody live behind it can only belong to a run that ended without finishing: a terminal that
|
|
4774
|
+
* closed, a process that was killed. Answered at once rather than after the sweep's half hour,
|
|
4775
|
+
* because the reviewer asking is usually the one who just closed that terminal.
|
|
4776
|
+
*/
|
|
4777
|
+
function driverGone(jobId, now = Date.now()) {
|
|
4778
|
+
return readClaim(jobId) !== null && liveClaim(jobId, now) === null;
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4769
4781
|
* Take the claim on a job, or return null because another driver holds it.
|
|
4770
4782
|
*
|
|
4771
4783
|
* Two retries fired together, a retry racing the run `startJob` already kicked off, or a retry
|
|
@@ -5034,7 +5046,7 @@ function deleteJob(id) {
|
|
|
5034
5046
|
}
|
|
5035
5047
|
function advance(job, phase, progress) {
|
|
5036
5048
|
const leaving = job.phase;
|
|
5037
|
-
const completed = WORK_PHASES.includes(leaving) && !job.completed.includes(leaving) ? [...job.completed, leaving] : job.completed;
|
|
5049
|
+
const completed = WORK_PHASES.includes(leaving) && leaving !== phase && !job.completed.includes(leaving) ? [...job.completed, leaving] : job.completed;
|
|
5038
5050
|
return {
|
|
5039
5051
|
...job,
|
|
5040
5052
|
phase,
|
|
@@ -9493,6 +9505,229 @@ async function withUsageRecorded(workspaceId, kind, env, body, runId, requestedM
|
|
|
9493
9505
|
}
|
|
9494
9506
|
}
|
|
9495
9507
|
//#endregion
|
|
9508
|
+
//#region ../../packages/review-harness/src/workspace/notifications.ts
|
|
9509
|
+
/**
|
|
9510
|
+
* What happened while the reviewer was looking at something else, kept until they have seen it.
|
|
9511
|
+
*
|
|
9512
|
+
* **One file per notification, not one list.** Notifications are written by whichever process
|
|
9513
|
+
* drove the work to its end: the CLI for a review typed at a terminal, the workspace server for one
|
|
9514
|
+
* started from the page, the sweep for one whose driver died. A single `notifications.json` read,
|
|
9515
|
+
* modified and written by several processes loses whichever entry was written second, and the lost
|
|
9516
|
+
* one is by definition something the reviewer was not watching. `jobs/` is laid out the same way for
|
|
9517
|
+
* the same reason.
|
|
9518
|
+
*
|
|
9519
|
+
* **Created once, with `wx`.** The id is derived from the transition it records, so a second attempt
|
|
9520
|
+
* to record the same transition fails at the filesystem and returns null, and a caller that only
|
|
9521
|
+
* delivers what it created cannot deliver twice. Nothing that reads a job ever creates one: a page
|
|
9522
|
+
* reloading, a client polling or a review being reopened cannot announce anything.
|
|
9523
|
+
*
|
|
9524
|
+
* Imports only `store_root`, for the cycle `queue.ts` describes.
|
|
9525
|
+
*/
|
|
9526
|
+
var NOTIFICATIONS_DIR = join(STORE_ROOT, "notifications");
|
|
9527
|
+
/** As long as a review is kept, so the list never names a review nobody can open for that reason. */
|
|
9528
|
+
var NOTIFICATION_RETENTION_MS = 2592e6;
|
|
9529
|
+
var OPERATION_NAMES = {
|
|
9530
|
+
review: "Review",
|
|
9531
|
+
reanalysis: "Reanalysis",
|
|
9532
|
+
update: "Review update",
|
|
9533
|
+
kiss: "KISS analysis"
|
|
9534
|
+
};
|
|
9535
|
+
/** What happened, in the words the list uses: `Review ready`, `Review update failed`. */
|
|
9536
|
+
function eventName(notification) {
|
|
9537
|
+
return `${OPERATION_NAMES[notification.operation]} ${notification.outcome}`;
|
|
9538
|
+
}
|
|
9539
|
+
/** The whole line, for anything that shows one event on its own: what happened, then to what. */
|
|
9540
|
+
function headline(notification) {
|
|
9541
|
+
return `${eventName(notification)} · ${notification.subject}`;
|
|
9542
|
+
}
|
|
9543
|
+
var OPERATIONS = [
|
|
9544
|
+
"review",
|
|
9545
|
+
"reanalysis",
|
|
9546
|
+
"update",
|
|
9547
|
+
"kiss"
|
|
9548
|
+
];
|
|
9549
|
+
var OUTCOMES = ["ready", "failed"];
|
|
9550
|
+
/** An id is a file name, so it is held to a shape that cannot leave the directory. */
|
|
9551
|
+
var ID = /^[A-Za-z0-9-]{1,200}$/;
|
|
9552
|
+
function pathOf(id) {
|
|
9553
|
+
return join(NOTIFICATIONS_DIR, `${id}.json`);
|
|
9554
|
+
}
|
|
9555
|
+
/**
|
|
9556
|
+
* Record one event, or return null because it is already recorded or could not be written.
|
|
9557
|
+
*
|
|
9558
|
+
* Never throws. A notification that could not be written is a notification the reviewer does not
|
|
9559
|
+
* get, which is a smaller loss than the review job that was being announced.
|
|
9560
|
+
*/
|
|
9561
|
+
function createNotification(input, now = Date.now()) {
|
|
9562
|
+
if (!ID.test(input.id)) return null;
|
|
9563
|
+
const record = {
|
|
9564
|
+
version: 1,
|
|
9565
|
+
kind: "analysis",
|
|
9566
|
+
...input,
|
|
9567
|
+
createdAt: now
|
|
9568
|
+
};
|
|
9569
|
+
try {
|
|
9570
|
+
mkdirSync(NOTIFICATIONS_DIR, { recursive: true });
|
|
9571
|
+
writeFileSync(pathOf(input.id), JSON.stringify(record), {
|
|
9572
|
+
flag: "wx",
|
|
9573
|
+
mode: 384
|
|
9574
|
+
});
|
|
9575
|
+
return record;
|
|
9576
|
+
} catch {
|
|
9577
|
+
return null;
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9580
|
+
/** Every notification, newest first. Unreadable and malformed files are skipped, not reported. */
|
|
9581
|
+
function listNotifications() {
|
|
9582
|
+
let names;
|
|
9583
|
+
try {
|
|
9584
|
+
names = readdirSync(NOTIFICATIONS_DIR);
|
|
9585
|
+
} catch {
|
|
9586
|
+
return [];
|
|
9587
|
+
}
|
|
9588
|
+
const found = [];
|
|
9589
|
+
for (const name of names) {
|
|
9590
|
+
if (!name.endsWith(".json")) continue;
|
|
9591
|
+
const record = readNotification(name.slice(0, -5));
|
|
9592
|
+
if (record) found.push(record);
|
|
9593
|
+
}
|
|
9594
|
+
return found.sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? -1 : 1));
|
|
9595
|
+
}
|
|
9596
|
+
function readNotification(id) {
|
|
9597
|
+
if (!ID.test(id)) return null;
|
|
9598
|
+
try {
|
|
9599
|
+
const parsed = JSON.parse(readFileSync(pathOf(id), "utf8"));
|
|
9600
|
+
return isNotification(parsed) && parsed.id === id ? parsed : null;
|
|
9601
|
+
} catch {
|
|
9602
|
+
return null;
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
function unreadCount() {
|
|
9606
|
+
return listNotifications().filter((notification) => notification.readAt === void 0).length;
|
|
9607
|
+
}
|
|
9608
|
+
/**
|
|
9609
|
+
* Mark these read, and say how many changed.
|
|
9610
|
+
*
|
|
9611
|
+
* Only ever by something the reviewer did to the notification itself: opening it, or one of the two
|
|
9612
|
+
* mark buttons. Opening the review it points at is not the same act and does not come through here.
|
|
9613
|
+
*/
|
|
9614
|
+
function markRead(ids, now = Date.now()) {
|
|
9615
|
+
let changed = 0;
|
|
9616
|
+
for (const id of ids) {
|
|
9617
|
+
const record = readNotification(id);
|
|
9618
|
+
if (!record || record.readAt !== void 0) continue;
|
|
9619
|
+
try {
|
|
9620
|
+
writeFileSync(pathOf(id), JSON.stringify({
|
|
9621
|
+
...record,
|
|
9622
|
+
readAt: now
|
|
9623
|
+
}), { mode: 384 });
|
|
9624
|
+
changed += 1;
|
|
9625
|
+
} catch {}
|
|
9626
|
+
}
|
|
9627
|
+
return changed;
|
|
9628
|
+
}
|
|
9629
|
+
function markAllRead(now = Date.now()) {
|
|
9630
|
+
return markRead(listNotifications().filter((notification) => notification.readAt === void 0).map((notification) => notification.id), now);
|
|
9631
|
+
}
|
|
9632
|
+
/**
|
|
9633
|
+
* Forget what is older than the retention, and anything past the cap, oldest first.
|
|
9634
|
+
*
|
|
9635
|
+
* Files that do not parse are removed once they are as old as the retention, judged by their
|
|
9636
|
+
* modification time, so a stray file neither lingers for ever nor disappears the moment it is seen.
|
|
9637
|
+
*/
|
|
9638
|
+
function pruneNotifications(now = Date.now()) {
|
|
9639
|
+
let names;
|
|
9640
|
+
try {
|
|
9641
|
+
names = readdirSync(NOTIFICATIONS_DIR);
|
|
9642
|
+
} catch {
|
|
9643
|
+
return 0;
|
|
9644
|
+
}
|
|
9645
|
+
const kept = listNotifications();
|
|
9646
|
+
const keep = new Set(kept.filter((notification) => now - notification.createdAt < NOTIFICATION_RETENTION_MS).slice(0, 200).map((notification) => `${notification.id}.json`));
|
|
9647
|
+
const valid = new Set(kept.map((notification) => `${notification.id}.json`));
|
|
9648
|
+
let removed = 0;
|
|
9649
|
+
for (const name of names) {
|
|
9650
|
+
if (!name.endsWith(".json") || keep.has(name)) continue;
|
|
9651
|
+
const path = join(NOTIFICATIONS_DIR, name);
|
|
9652
|
+
try {
|
|
9653
|
+
if (!valid.has(name) && now - statSync(path).mtimeMs < 2592e6) continue;
|
|
9654
|
+
unlinkSync(path);
|
|
9655
|
+
removed += 1;
|
|
9656
|
+
} catch {}
|
|
9657
|
+
}
|
|
9658
|
+
return removed;
|
|
9659
|
+
}
|
|
9660
|
+
function isNotification(value) {
|
|
9661
|
+
if (!value || typeof value !== "object") return false;
|
|
9662
|
+
const record = value;
|
|
9663
|
+
return record.version === 1 && record.kind === "analysis" && typeof record.id === "string" && OPERATIONS.includes(record.operation) && OUTCOMES.includes(record.outcome) && typeof record.reviewId === "string" && typeof record.subject === "string" && (record.branch === void 0 || typeof record.branch === "string") && typeof record.repo === "string" && typeof record.createdAt === "number" && (record.readAt === void 0 || typeof record.readAt === "number");
|
|
9664
|
+
}
|
|
9665
|
+
//#endregion
|
|
9666
|
+
//#region ../../packages/review-harness/src/workspace/announce.ts
|
|
9667
|
+
/**
|
|
9668
|
+
* Tell the reviewer a job ended, once, after the ending has been written.
|
|
9669
|
+
*
|
|
9670
|
+
* Called by the three places that write an ending: `drive` in `review_job.ts`, `updateReview`, and
|
|
9671
|
+
* the sweep for a job whose driver died. Each calls it with the record it has just saved, in the
|
|
9672
|
+
* process that saved it, so the answer never depends on who happens to be looking.
|
|
9673
|
+
*
|
|
9674
|
+
* The id is the job and the moment it ended, so two calls about the same ending make one record,
|
|
9675
|
+
* and a job picked up again that ends later is a new event with its own.
|
|
9676
|
+
*
|
|
9677
|
+
* The record is the event. The Notifications page and the workspace's toasts both read it. Any
|
|
9678
|
+
* delivery that has to be pushed rather than read belongs here, after `createNotification`, and only
|
|
9679
|
+
* for a record this call created, which carries the once-only guarantee over to it for free. Desktop
|
|
9680
|
+
* delivery is written that way and parked outside the product: see `desktop_preference.ts`.
|
|
9681
|
+
*
|
|
9682
|
+
* Never throws. This runs on the success path of a review that has already been saved, and the
|
|
9683
|
+
* accounting must not break the thing it accounts for.
|
|
9684
|
+
*/
|
|
9685
|
+
function announceJob(job) {
|
|
9686
|
+
try {
|
|
9687
|
+
if (!isTerminal(job) || job.failure?.stopped || purposeOf(job) === "ask") return;
|
|
9688
|
+
const workspace = loadWorkspace(job.workspaceId);
|
|
9689
|
+
if (!workspace) return;
|
|
9690
|
+
const branch = branchOf(workspace);
|
|
9691
|
+
createNotification({
|
|
9692
|
+
id: `${job.id}-${endedAt(job)}`,
|
|
9693
|
+
operation: operationOf(job),
|
|
9694
|
+
outcome: job.phase === "done" ? "ready" : "failed",
|
|
9695
|
+
reviewId: workspace.id,
|
|
9696
|
+
subject: subjectOf(workspace),
|
|
9697
|
+
...branch ? { branch } : {},
|
|
9698
|
+
repo: basename(workspace.repoPath)
|
|
9699
|
+
});
|
|
9700
|
+
} catch {}
|
|
9701
|
+
}
|
|
9702
|
+
function operationOf(job) {
|
|
9703
|
+
if (job.reviewType === "kiss") return "kiss";
|
|
9704
|
+
if (purposeOf(job) === "update") return "update";
|
|
9705
|
+
return job.reanalysis ? "reanalysis" : "review";
|
|
9706
|
+
}
|
|
9707
|
+
/**
|
|
9708
|
+
* What was reviewed, as the reviewer would name it, and nothing that locates it on disk.
|
|
9709
|
+
*
|
|
9710
|
+
* A pull request by its number, spelled the way its forge spells it; a commit by its short sha; a
|
|
9711
|
+
* branch by the ref it follows, which is the same rule the reviews list and the CLI use.
|
|
9712
|
+
*/
|
|
9713
|
+
function subjectOf(workspace) {
|
|
9714
|
+
const changeSet = workspace.changeSet;
|
|
9715
|
+
if (changeSet.targetType === "commit") return `commit ${changeSet.data.pullRequest.head.sha.slice(0, 7)}`;
|
|
9716
|
+
const number = changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number;
|
|
9717
|
+
if (number) return /\/merge_requests\//.test(workspace.prContext?.pullRequest.url ?? "") ? `MR !${number}` : `PR #${number}`;
|
|
9718
|
+
return followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef;
|
|
9719
|
+
}
|
|
9720
|
+
/**
|
|
9721
|
+
* The branch beside a pull request's number, which alone does not say what the change is. Nothing
|
|
9722
|
+
* for a branch review, whose subject already is the branch, or for a commit, which has none.
|
|
9723
|
+
*/
|
|
9724
|
+
function branchOf(workspace) {
|
|
9725
|
+
const changeSet = workspace.changeSet;
|
|
9726
|
+
if (changeSet.targetType === "commit") return void 0;
|
|
9727
|
+
if (!(changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number)) return void 0;
|
|
9728
|
+
return (followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef) || void 0;
|
|
9729
|
+
}
|
|
9730
|
+
//#endregion
|
|
9496
9731
|
//#region ../../packages/review-harness/src/telemetry/consent.ts
|
|
9497
9732
|
/**
|
|
9498
9733
|
* Who this machine is counted as, and what it has agreed to.
|
|
@@ -9815,6 +10050,7 @@ async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel)
|
|
|
9815
10050
|
target,
|
|
9816
10051
|
env: agent.captureEnv(),
|
|
9817
10052
|
purpose: "review",
|
|
10053
|
+
reanalysis: true,
|
|
9818
10054
|
...model ? { requestedModel: model } : {}
|
|
9819
10055
|
});
|
|
9820
10056
|
saveJob(job);
|
|
@@ -10030,6 +10266,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
10030
10266
|
else await applyGuide(job, workspaceId, payload, observedModel);
|
|
10031
10267
|
job = update(job, "done");
|
|
10032
10268
|
finished(job, workspaceId, startedAt, true);
|
|
10269
|
+
announceJob(job);
|
|
10033
10270
|
return job;
|
|
10034
10271
|
} catch (error) {
|
|
10035
10272
|
if (error instanceof AgentCancelledError || signal?.aborted) {
|
|
@@ -10043,6 +10280,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
10043
10280
|
job = recordFailure(job, explain(job, raw), driver, kind, raw);
|
|
10044
10281
|
if (job.reviewType === "kiss") failCurrentKissRun(workspaceId, job, job.failure?.message ?? raw, kind, error instanceof KissRejected ? error.answer : void 0);
|
|
10045
10282
|
finished(job, workspaceId, startedAt, false);
|
|
10283
|
+
announceJob(job);
|
|
10046
10284
|
return job;
|
|
10047
10285
|
}
|
|
10048
10286
|
}
|
|
@@ -11221,10 +11459,14 @@ async function updateReview(workspace, runners = {}) {
|
|
|
11221
11459
|
const step = (phase) => saveJob(advance(current(), phase));
|
|
11222
11460
|
try {
|
|
11223
11461
|
const outcome = await bringForward(workspace, runners, reviewedSha, held, step);
|
|
11224
|
-
|
|
11462
|
+
const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
|
|
11463
|
+
saveJob(ended);
|
|
11464
|
+
announceJob(ended);
|
|
11225
11465
|
return outcome;
|
|
11226
11466
|
} catch (error) {
|
|
11227
|
-
|
|
11467
|
+
const failed = fail(current(), error instanceof Error ? error.message : String(error));
|
|
11468
|
+
saveJob(failed);
|
|
11469
|
+
announceJob(failed);
|
|
11228
11470
|
throw error;
|
|
11229
11471
|
} finally {
|
|
11230
11472
|
held.claim.release();
|
|
@@ -11814,4 +12056,4 @@ function lineageIds(id) {
|
|
|
11814
12056
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
11815
12057
|
}
|
|
11816
12058
|
//#endregion
|
|
11817
|
-
export {
|
|
12059
|
+
export { feedbackUrl as $, reviewerDispositions as $t, markRead as A, loadWorkspace as At, modelHelpLines as B, workspaceRevision as Bt, recordTelemetryUploadConsent as C, isClaimed as Ct, headline as D, latestKissRun as Dt, eventName as E, indexReviewGroups as Et, EXPLAIN_SIMPLY_PROMPT as F, reviewedCommit as Ft, ensureServer as G, queueStamp as Gt, readIndexToken as H, dequeueOnComplete as Ht, ReviewBeingDeletedError as I, reviewedRepositories as It, stopServer as J, doneVerb as Jt, readServerRecord as K, readQueue as Kt, MANAGED_ROOT as L, saveWorkspace as Lt, readNotification as M, positionInLineage as Mt, unreadCount as N, previousKissResult as Nt, listNotifications as O, lineageKeyFor as Ot, withUsageRecorded as P, recentWorkspaces as Pt, PACKAGE_NAME as Q, questionsOutstanding as Qt, readMarker as R, summarise as Rt, record as S, driverGone as St, announceJob as T, groupByLineage as Tt, bootstrapUrl as U, queueAdd as Ut, relativeTime as V, workspaceStamps as Vt, clearServerRecord as W, queueRemove as Wt, writeServerRecord as X, isQuestionOutstanding as Xt, workspaceUrl as Y, isIssueOutstanding as Yt, BUILD_VERSION as Z, issuesOutstanding as Zt, startKissJob as _, AgentCancelledError as _n, PHASES as _t, wasBlocked as a, resolveAgent as an, statedWorkspacePort as at, startJob as b, purposeOf as bt, updateReview as c, readAgentPreference as cn, fillFileUrlTemplate as ct, askCheckout as d, STORE_ROOT as dn, allJobs as dt, processDiscussion as en, DEFAULT_WORKSPACE_PORT as et, UpdateAlreadyRunningError as f, AGENT_IDS as fn, endedAt as ft, recordKissRun as g, detectAgents as gn, saveJob as gt, agentEnvOf as h, agentFor as hn, loadJob as ht, liveJobsFor as i, git as in, resolveWorkspacePort as it, pruneNotifications as j, matchingWorkspaceIds as jt, markAllRead as k, lineagePosition as kt, refreshPrContext as l, writeAgentPreference as ln, checkCodeFreshness as lt, runningJobs as m, agentById as mn, isTerminal as mt, deleteReview as n, resolveTarget as nn, MIN_WORKSPACE_PORT as nt, discardJob as o, resolveModel as on, writePortPreference as ot, liveUpdateFor as p, DEFAULT_AGENT_ID as pn, fail as pt, reviewsUrl as q, writeQueue as qt, lineageIds as r, displayRef as rn, parseWorkspacePort as rt, RefreshUnavailableError as s, clearAgentPreference as sn, forgeResolver as st, deleteLineage as t, describeAuthorship as tn, MAX_WORKSPACE_PORT as tt, isUnchanged as u, CONFIG_PATH as un, checkReview as ut, reanalyseReview as v, AgentUnavailableError as vn, phasesFor as vt, telemetryUploadConsent as w, followedRefName as wt, readEvents as x, clearClaim as xt, runJob as y, progressSteps as yt, removeWorktree as z, touchWorkspace as zt };
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { At as loadWorkspace, B as modelHelpLines, C as recordTelemetryUploadConsent, Ct as isClaimed, G as ensureServer, H as readIndexToken, It as reviewedRepositories, J as stopServer, L as MANAGED_ROOT, Lt as saveWorkspace, Mt as positionInLineage, Ot as lineageKeyFor, Pt as recentWorkspaces, Q as PACKAGE_NAME, R as readMarker, Rt as summarise, S as record$1, St as driverGone, Tt as groupByLineage, U as bootstrapUrl, V as relativeTime, Y as workspaceUrl, Z as BUILD_VERSION, _t as PHASES, a as wasBlocked, an as resolveAgent, at as statedWorkspacePort, b as startJob, bt as purposeOf, c as updateReview, cn as readAgentPreference, dn as STORE_ROOT, dt as allJobs, et as DEFAULT_WORKSPACE_PORT, f as UpdateAlreadyRunningError, fn as AGENT_IDS, g as recordKissRun, gn as detectAgents, ht as loadJob, i as liveJobsFor, in as git, jt as matchingWorkspaceIds, ln as writeAgentPreference, m as runningJobs, mn as agentById, mt as isTerminal, n as deleteReview$1, nn as resolveTarget, nt as MIN_WORKSPACE_PORT, on as resolveModel, ot as writePortPreference, p as liveUpdateFor, pn as DEFAULT_AGENT_ID, q as reviewsUrl, rn as displayRef, rt as parseWorkspacePort, st as forgeResolver, tt as MAX_WORKSPACE_PORT, un as CONFIG_PATH, ut as checkReview, v as reanalyseReview, w as telemetryUploadConsent, wt as followedRefName, xt as clearClaim, y as runJob, yt as progressSteps, z as removeWorktree } from "./delete_review-TNIFNct3.js";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -705,11 +705,17 @@ function madeAgainst(requested) {
|
|
|
705
705
|
const wanted = displayRef(requested);
|
|
706
706
|
return (review) => review.baseRef !== "" && displayRef(review.baseRef) === wanted;
|
|
707
707
|
}
|
|
708
|
-
/**
|
|
708
|
+
/**
|
|
709
|
+
* Whether the job behind a stored review is still working. A review with no job never was.
|
|
710
|
+
*
|
|
711
|
+
* A job whose driver has gone is not working, whatever phase its record was left at. Calling it in
|
|
712
|
+
* progress told the reviewer who had just closed a terminal that there was "nothing to do but wait",
|
|
713
|
+
* for a process that no longer existed, until the sweep called it failed half an hour later.
|
|
714
|
+
*/
|
|
709
715
|
function isRunning(jobId) {
|
|
710
716
|
if (!jobId) return false;
|
|
711
717
|
const job = loadJob(jobId);
|
|
712
|
-
return !!job && !isTerminal(job);
|
|
718
|
+
return !!job && !isTerminal(job) && !driverGone(job.id);
|
|
713
719
|
}
|
|
714
720
|
/**
|
|
715
721
|
* What the review is of, as the clause both surfaces hang off "Made two hours ago".
|
|
@@ -1264,7 +1270,7 @@ function unknownAgent(id, known) {
|
|
|
1264
1270
|
* mistake; `Open workspace now` and `Review ready` read as a beginning and an end.
|
|
1265
1271
|
*/
|
|
1266
1272
|
function startedReview(url, _target) {
|
|
1267
|
-
return [`Open workspace now ${url}`, "
|
|
1273
|
+
return [`Open workspace now ${url}`, "Keep this terminal open while it runs. If it closes, run the same command again to carry on."].join("\n");
|
|
1268
1274
|
}
|
|
1269
1275
|
/**
|
|
1270
1276
|
* The one-review-per-branch rule, said to a person rather than to a session.
|
|
@@ -1517,8 +1523,9 @@ function commitReviewCannotUpdate(sha) {
|
|
|
1517
1523
|
* Said before the update starts, so the command answers at once and the link is there to open while
|
|
1518
1524
|
* it works. The same label the creation path uses, because it is the same offer.
|
|
1519
1525
|
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1526
|
+
* Without the creation path's line about carrying on if the terminal closes. An update runs in this
|
|
1527
|
+
* process and ends with it, and running the command again starts the update over rather than
|
|
1528
|
+
* picking one up, because `liveUpdateFor` reads a dead driver as nothing running.
|
|
1522
1529
|
*/
|
|
1523
1530
|
function startedUpdate(url, acted) {
|
|
1524
1531
|
return `${subjectLine(acted)}\nOpen workspace now ${url}`;
|
|
@@ -2178,8 +2185,10 @@ function describe(deps, id) {
|
|
|
2178
2185
|
* One thing differs, and it is a property of the surface rather than of the review. An MCP tool
|
|
2179
2186
|
* returns to a session that must not sit and wait, so it fires `runJob` and returns the link at
|
|
2180
2187
|
* once. A person who typed a command is already waiting, so this awaits the job and reports what
|
|
2181
|
-
* it is doing. Either way the job is durable and
|
|
2182
|
-
*
|
|
2188
|
+
* it is doing. Either way the job is durable and the link works from the moment it is printed. The
|
|
2189
|
+
* job does not outlive this process, though: the agent shares the terminal's process group, so a
|
|
2190
|
+
* terminal closed halfway through ends both. What it loses is time, not work. The claim it leaves
|
|
2191
|
+
* behind is `driverGone`, and running the command again carries on from the phase it reached.
|
|
2183
2192
|
*
|
|
2184
2193
|
* **Two axes, and this file is where they meet.** A command names a target and an operation, and
|
|
2185
2194
|
* the operations are `update` and `reanalyse`, which were `prreviewbuddy update` until 0.15.0.
|
|
@@ -2480,15 +2489,19 @@ function makeCommandFor(command) {
|
|
|
2480
2489
|
if (command.commitRef) return `prreviewbuddy review --commit ${command.commitRef}`;
|
|
2481
2490
|
return "prreviewbuddy review";
|
|
2482
2491
|
}
|
|
2492
|
+
/** Why an unfinished review is being picked up, when nothing recorded a failure because nothing could. */
|
|
2493
|
+
var DRIVER_GONE = "the command that was running it ended before it finished, most often because its terminal closed.";
|
|
2483
2494
|
function failureOf(workspaceId) {
|
|
2484
2495
|
const workspace = loadWorkspace(workspaceId);
|
|
2485
2496
|
if (!workspace?.jobId) return null;
|
|
2486
2497
|
const job = loadJob(workspace.jobId);
|
|
2487
|
-
if (!job
|
|
2498
|
+
if (!job) return null;
|
|
2499
|
+
const abandoned = !isTerminal(job) && driverGone(job.id);
|
|
2500
|
+
if (!abandoned && (!isTerminal(job) || job.phase !== "failed")) return null;
|
|
2488
2501
|
if (isClaimed(job.id)) return null;
|
|
2489
2502
|
return {
|
|
2490
|
-
message: job.failure?.message ?? "The reason was not recorded.",
|
|
2491
|
-
stopped: job.failure?.stopped === true,
|
|
2503
|
+
message: abandoned ? DRIVER_GONE : job.failure?.message ?? "The reason was not recorded.",
|
|
2504
|
+
stopped: !abandoned && job.failure?.stopped === true,
|
|
2492
2505
|
agentId: job.env.agentId ?? DEFAULT_AGENT_ID,
|
|
2493
2506
|
...job.requestedModel ? { requestedModel: job.requestedModel } : {}
|
|
2494
2507
|
};
|
|
@@ -3202,8 +3215,7 @@ async function cleanUp(options) {
|
|
|
3202
3215
|
*
|
|
3203
3216
|
* It stops the workspace server, which the help screen says on purpose there is no command to do.
|
|
3204
3217
|
* That is not a hole in the rule but the end of it: "no command to stop the server" exists so that
|
|
3205
|
-
*
|
|
3206
|
-
* anything here.
|
|
3218
|
+
* the workspace outlives the terminal that opened it, and there are no reviews left to serve here.
|
|
3207
3219
|
*
|
|
3208
3220
|
* What it does not do is remove its own npm package. A running binary deleting the install it is
|
|
3209
3221
|
* executing from depends on which package manager put it there and fails in ways that leave a
|