prreviewbuddy 0.31.0 → 0.36.3
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 +165 -0
- package/README.md +7 -3
- package/dist/{delete_review-C2wax0ED.js → delete_review-Kup1LXD_.js} +277 -13
- package/dist/main.js +25 -13
- package/dist/server.js +802 -106
- package/package.json +1 -1
- package/static/chunk-B80S6zbH.js +1 -1
- package/static/chunk-DmbjXWI0.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 +1066 -419
- package/static/workspace.js +41 -41
- package/static/chunk-CIsM3PMS.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,170 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.36.3
|
|
4
|
+
|
|
5
|
+
**The reviews list is one click away from anywhere.** The strip at the top of every workspace page
|
|
6
|
+
now opens it, beside the notifications bell. Until now the only way back to the list was the rail,
|
|
7
|
+
which a narrow window hides, so from a review page it was the browser's Back button or nothing.
|
|
8
|
+
|
|
9
|
+
Every workspace place now has an icon up there, so the text links that used to appear in the strip
|
|
10
|
+
on a narrow window are gone: each of them was the same destination as the icon beside it.
|
|
11
|
+
|
|
12
|
+
## 0.36.2
|
|
13
|
+
|
|
14
|
+
The note saying where your settings are kept now ends the settings, above the Server section rather
|
|
15
|
+
than below it. Between Stop server and the foot of the page it read as a footnote about the server,
|
|
16
|
+
which is the one thing on that page it says nothing about.
|
|
17
|
+
|
|
18
|
+
## 0.36.1
|
|
19
|
+
|
|
20
|
+
**A status dot says whether the workspace server is there.** It sits beside the restart button:
|
|
21
|
+
green while the server is answering, amber while a restart is in flight, red once nothing is. It is
|
|
22
|
+
small on purpose — while you are reading a page, the page itself is the proof the server is up, so
|
|
23
|
+
the dot earns its place in the other two states.
|
|
24
|
+
|
|
25
|
+
**A restart now says it worked.** The page comes back with "Server restarted. Workspace is back
|
|
26
|
+
online." rather than simply blinking. There is still nothing to confirm beforehand: it is one click
|
|
27
|
+
when the server is idle.
|
|
28
|
+
|
|
29
|
+
If a restart does not come back, the page says so and stays saying so. It tells the two cases apart:
|
|
30
|
+
a restart the server refused before stopping anything leaves it running and says that, and only a
|
|
31
|
+
restart that actually lost the server names the command that brings it back.
|
|
32
|
+
|
|
33
|
+
## 0.36.0
|
|
34
|
+
|
|
35
|
+
**The workspace can restart its own server.** A restart button sits in the global strip on every
|
|
36
|
+
page: the running server starts its replacement, hands over the port and goes, and the page reloads
|
|
37
|
+
itself onto the new one. It needs no terminal, and because the replacement is read from disk it is
|
|
38
|
+
how you pick up a new build without leaving the browser. While the server is making a review of its
|
|
39
|
+
own the button is disabled and says why, since that work would end with the process; a review being
|
|
40
|
+
made from a terminal is a different process and never stands in the way.
|
|
41
|
+
|
|
42
|
+
If the replacement cannot be started, nothing is stopped. The server refuses before it tears
|
|
43
|
+
anything down, keeps serving, and the page says so rather than reloading into nothing.
|
|
44
|
+
|
|
45
|
+
**Stop server has moved to Settings.** It used to sit at the foot of the rail on every page, one
|
|
46
|
+
press from ending every open review, next to the links you navigate with. It is the one thing the
|
|
47
|
+
workspace cannot undo — the page it leaves behind has no server to ask — so it now lives on the
|
|
48
|
+
Settings page with the sentence that matters beside it: start it again with `prreviewbuddy open`,
|
|
49
|
+
from any directory. The rail is navigation again, and the strip offers the recoverable action
|
|
50
|
+
instead.
|
|
51
|
+
|
|
52
|
+
`prreviewbuddy open` has always worked from any directory and starts the server if it is not
|
|
53
|
+
running; it is now tested to keep doing so. The workspace is global; only reviews belong to a
|
|
54
|
+
repository.
|
|
55
|
+
|
|
56
|
+
## 0.35.0
|
|
57
|
+
|
|
58
|
+
**Notifications have a home in the top-right corner.** A bell sits in the workspace's new global
|
|
59
|
+
strip, on every page including a review, and shows a dot when something is waiting. It was reachable
|
|
60
|
+
only from the foot of the rail before, which is not where anybody looks for it. The bell opens the
|
|
61
|
+
Notifications page and marks nothing read: that still happens only by a deliberate act. How many are
|
|
62
|
+
waiting is still said in words in the rail, which is where there is room to say it; the dot is only
|
|
63
|
+
the news that there is something to look at.
|
|
64
|
+
|
|
65
|
+
The caret on a menu is ours now rather than the browser's. The native one sat hard against the
|
|
66
|
+
control's right edge in a colour used nowhere else in the product; this one is inset by the same
|
|
67
|
+
distance the text has on the other side, and drawn in the muted grey everything quiet here shares.
|
|
68
|
+
|
|
69
|
+
**Settings is in that strip too**, as an icon beside the bell, reachable from any page without
|
|
70
|
+
going to the rail for it. It still sits in the rail with its name beside it.
|
|
71
|
+
|
|
72
|
+
**The workspace has two bars instead of one.** The product's own controls and the review's identity
|
|
73
|
+
used to share a single header, which is why there was no room for the bell. They are separated now:
|
|
74
|
+
a thin strip carries the wordmark, the bell and the build, and the bar below it belongs entirely to
|
|
75
|
+
what you are looking at. On a review that is the branch, the size, the freshness and Mark reviewed,
|
|
76
|
+
exactly as before but with the product's chrome lifted out of it. On the other pages it names the
|
|
77
|
+
page and carries that page's own controls, which have moved up out of the column they act on rather
|
|
78
|
+
than being added: the reviews search and filters, the Notifications count and Mark all read, and the
|
|
79
|
+
way back out of Settings.
|
|
80
|
+
|
|
81
|
+
The build version now sits in the strip on every page, including a review, where it used to be at
|
|
82
|
+
the foot of the rail.
|
|
83
|
+
|
|
84
|
+
Two things that had never worked in a window too narrow for the rail, both found by looking at the
|
|
85
|
+
real thing. The workspace links meant to replace the hidden rail were never shown at all, so at that
|
|
86
|
+
width there was no way to Notifications, Settings or the feedback form. And the reviews list,
|
|
87
|
+
Notifications and Settings were squeezed into the width the rail had vacated, with the rest of the
|
|
88
|
+
window empty beside them: the same fault the review itself had, fixed there in 0.34.0 and missed
|
|
89
|
+
here.
|
|
90
|
+
|
|
91
|
+
## 0.34.0
|
|
92
|
+
|
|
93
|
+
**The Guide shows one theme at a time.** The workspace Guide used to stack every theme in one
|
|
94
|
+
column, so reading the second one meant scrolling past the whole of the first. The rail already
|
|
95
|
+
lists the themes, so it now drives the page: choosing one shows that theme on its own, and the
|
|
96
|
+
rail says how long each is likely to take. The theme tag on an issue or question opens its theme,
|
|
97
|
+
which it always named and never did. In a window too narrow for the rail, the same themes appear
|
|
98
|
+
above the theme itself.
|
|
99
|
+
|
|
100
|
+
A theme now reads in the order the work happens: why it matters, then what to check, then the files
|
|
101
|
+
it touches. That order is better on both surfaces, so the browser extension's Guide gets it too, and
|
|
102
|
+
is unchanged otherwise. Its parts stand apart rather than running together, and the theme itself is
|
|
103
|
+
no longer drawn as a card: with one theme on the page, the outline had nothing on the other side of
|
|
104
|
+
it. Nothing folds a theme away any more either, since there is now nothing behind it to reveal.
|
|
105
|
+
|
|
106
|
+
**The rail says what is left, in words.** A theme row gives the time it takes and, when something is
|
|
107
|
+
still open on it, how much: "~22 min, 1 of 2 issues left". That count used to be a bare digit whose
|
|
108
|
+
meaning only a screen reader was told. The file count has gone from the rail, which is what leaves
|
|
109
|
+
room to spell the rest out. Clicking the words "Review path" folds the list, not just the caret.
|
|
110
|
+
|
|
111
|
+
**The Overview's numbers read as numbers.** Each stat card leads with the figure and sets the label
|
|
112
|
+
beside it, so the four cards line up whether or not one of them carries a second line.
|
|
113
|
+
|
|
114
|
+
Two things that were wrong wherever you looked. The document pane was painted a slightly different
|
|
115
|
+
grey from the page behind it, leaving a seam down every view. And in a window too narrow for the
|
|
116
|
+
rail, the review was squeezed into the width the rail had vacated, with half the window empty beside
|
|
117
|
+
it.
|
|
118
|
+
|
|
119
|
+
The progress line no longer counts what it cannot change: completing a theme is part of guided
|
|
120
|
+
review, which the workspace does not have, so the Guide states the size of the job rather than
|
|
121
|
+
reporting nothing done.
|
|
122
|
+
|
|
123
|
+
## 0.33.0
|
|
124
|
+
|
|
125
|
+
**A type scale, a palette for the review, and fewer boxes.** Text in the workspace now comes from
|
|
126
|
+
seven named sizes rather than fifteen ad-hoc ones, so a heading, a sentence, a piece of metadata and
|
|
127
|
+
a status chip differ by size instead of by weight, and ordinary text is ordinary weight again. The
|
|
128
|
+
review itself now has its own light palette: three greys where one grey had been written several
|
|
129
|
+
ways, a single hairline where six different border shades were drawing the same line, and one purple
|
|
130
|
+
where there were five. Seven panels that were a box drawn inside another box have lost their
|
|
131
|
+
outline and sit on the page as white cards against a slightly deeper background. Purple now marks
|
|
132
|
+
what you have selected, what has keyboard focus, and the main action on a page, so hovering a row no
|
|
133
|
+
longer shouts as loudly as choosing one. The browser extension is unchanged.
|
|
134
|
+
|
|
135
|
+
## 0.32.1
|
|
136
|
+
|
|
137
|
+
**The rail on every workspace page.** The reviews list, Notifications and Settings now carry the
|
|
138
|
+
same rail a review does, so Reviews, Notifications and Settings are always one click away without
|
|
139
|
+
opening a review first. A review's rail keeps everything it had, with the workspace places beneath
|
|
140
|
+
it. The unread count sits beside Notifications wherever the rail is. In a window too narrow for the
|
|
141
|
+
rail, the same places appear in the header instead.
|
|
142
|
+
|
|
143
|
+
## 0.32.0
|
|
144
|
+
|
|
145
|
+
**Notifications.** When a review, reanalysis, update or KISS analysis finishes or fails, PR Review
|
|
146
|
+
Buddy now records it on a new Notifications page in the workspace, so you can start a review, or a
|
|
147
|
+
batch of them, get on with something else, and see what finished when you come back. The reviews
|
|
148
|
+
list shows how many are unread, and every review's rail links to the page. Opening a notification
|
|
149
|
+
takes you to its review and marks it read, and Mark all read clears the rest; visiting the review
|
|
150
|
+
some other way leaves it unread. A review you stop with Ctrl-C is not recorded as a failure.
|
|
151
|
+
Notifications are kept for thirty days, on this machine only, and nothing about them is sent
|
|
152
|
+
anywhere.
|
|
153
|
+
|
|
154
|
+
**Toasts in the workspace.** While any workspace page is open, a notification also appears for a
|
|
155
|
+
few seconds in the bottom-right corner, with Open beside it. It stays while your pointer is on it,
|
|
156
|
+
and no more than three show at once: when a batch finishes together, the last one says how many more
|
|
157
|
+
there are, with View all. Letting a toast go or dismissing it leaves the notification unread on the
|
|
158
|
+
Notifications page.
|
|
159
|
+
|
|
160
|
+
**Closing the terminal no longer strands a review.** The CLI used to say a review continues if its
|
|
161
|
+
terminal closes. It did not: the review stopped with the terminal, and running the command again
|
|
162
|
+
said it was still being made for the next half hour. The message now asks you to keep the terminal
|
|
163
|
+
open, and running the same command again picks the review up at once from the step it reached.
|
|
164
|
+
|
|
165
|
+
Also fixed: a review picked up part way through its analysis showed that step as finished while it
|
|
166
|
+
was still running.
|
|
167
|
+
|
|
3
168
|
## 0.31.0
|
|
4
169
|
|
|
5
170
|
**Review a single commit.** `prreviewbuddy review --commit <commit>` reviews the change one commit
|
package/README.md
CHANGED
|
@@ -82,6 +82,8 @@ prreviewbuddy agents which coding agents you have
|
|
|
82
82
|
prreviewbuddy config set agent <agent> remember which to use when you do not name one
|
|
83
83
|
prreviewbuddy config set telemetry-upload on|off
|
|
84
84
|
send usage telemetry, or stop. On by default
|
|
85
|
+
prreviewbuddy config get port the port the workspace runs on
|
|
86
|
+
prreviewbuddy config set port <port> move it, from the next start onwards
|
|
85
87
|
|
|
86
88
|
prreviewbuddy uninstall remove everything this put on your machine
|
|
87
89
|
```
|
|
@@ -113,9 +115,11 @@ started with.
|
|
|
113
115
|
The job is durable: closing the terminal does not stop it, and running the same command again after
|
|
114
116
|
an interruption carries on from the phase that stopped rather than starting over.
|
|
115
117
|
|
|
116
|
-
Reviews are kept in `~/.prreviewbuddy` for thirty days. A local server holds them open
|
|
117
|
-
the terminal that started it,
|
|
118
|
-
|
|
118
|
+
Reviews are kept in `~/.prreviewbuddy` for thirty days. A local server holds them open at
|
|
119
|
+
`http://localhost:7726` and outlives the terminal that started it, so a link to a review is worth
|
|
120
|
+
keeping: the address does not move, and the same link still opens after a restart. Run
|
|
121
|
+
`prreviewbuddy open` from any directory to get back to the list, and
|
|
122
|
+
`prreviewbuddy config set port <port>` if 7726 is taken on your machine.
|
|
119
123
|
|
|
120
124
|
The isolated checkout a review was made in is kept for six hours after it finishes, because that is
|
|
121
125
|
what the assistant reads when you ask it questions, and is then removed.
|
|
@@ -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,
|
|
@@ -6972,7 +6984,7 @@ async function waitForPortToFree(port) {
|
|
|
6972
6984
|
async function ensureServer() {
|
|
6973
6985
|
const running = await healthyPort();
|
|
6974
6986
|
if (running !== null) return running;
|
|
6975
|
-
if (!
|
|
6987
|
+
if (!serverEntryExists()) throw new Error(`The workspace server is not built. Expected it at ${serverEntry()}.${buildInstruction(SURFACE.client)}`);
|
|
6976
6988
|
if (!acquireSpawnLock()) {
|
|
6977
6989
|
const shared = await waitForHealthyServer();
|
|
6978
6990
|
if (shared !== null) return shared;
|
|
@@ -6991,11 +7003,7 @@ async function ensureServer() {
|
|
|
6991
7003
|
}
|
|
6992
7004
|
if (occupant === "foreign") throw new Error(portTakenMessage(port));
|
|
6993
7005
|
clearServerRecord();
|
|
6994
|
-
|
|
6995
|
-
detached: true,
|
|
6996
|
-
stdio: "ignore",
|
|
6997
|
-
cwd: STORE_ROOT
|
|
6998
|
-
}).unref();
|
|
7006
|
+
spawnServer();
|
|
6999
7007
|
const started = await waitForHealthyServer();
|
|
7000
7008
|
if (started !== null) return started;
|
|
7001
7009
|
if (await occupantOf(port) === "foreign") throw new Error(portTakenMessage(port));
|
|
@@ -7005,6 +7013,32 @@ async function ensureServer() {
|
|
|
7005
7013
|
}
|
|
7006
7014
|
}
|
|
7007
7015
|
/**
|
|
7016
|
+
* Start a detached workspace server and let go of it.
|
|
7017
|
+
*
|
|
7018
|
+
* One function rather than one per caller: the launcher starts the first server, and a server being
|
|
7019
|
+
* restarted starts its own replacement, and those must be the same process in the same place with
|
|
7020
|
+
* the same entry — a second spawn written separately is how the two quietly drift apart.
|
|
7021
|
+
*
|
|
7022
|
+
* `serverEntry()` is read at call time rather than captured, so a restart after a rebuild picks up
|
|
7023
|
+
* the new file at the same path, which is the main reason anybody wants one.
|
|
7024
|
+
*/
|
|
7025
|
+
function spawnServer() {
|
|
7026
|
+
const child = spawn(process.execPath, [serverEntry()], {
|
|
7027
|
+
detached: true,
|
|
7028
|
+
stdio: "ignore",
|
|
7029
|
+
cwd: STORE_ROOT
|
|
7030
|
+
});
|
|
7031
|
+
child.unref();
|
|
7032
|
+
return child;
|
|
7033
|
+
}
|
|
7034
|
+
/** Whether there is anything to start. The one foreseeable reason a restart cannot be attempted. */
|
|
7035
|
+
function serverEntryExists() {
|
|
7036
|
+
return existsSync(serverEntry());
|
|
7037
|
+
}
|
|
7038
|
+
function serverEntry() {
|
|
7039
|
+
return SERVER_ENTRY;
|
|
7040
|
+
}
|
|
7041
|
+
/**
|
|
7008
7042
|
* The recorded server's port, if it is actually answering *and* is this build's.
|
|
7009
7043
|
*
|
|
7010
7044
|
* A server from another version is stopped rather than left alone. Two servers cannot both hold
|
|
@@ -9493,6 +9527,229 @@ async function withUsageRecorded(workspaceId, kind, env, body, runId, requestedM
|
|
|
9493
9527
|
}
|
|
9494
9528
|
}
|
|
9495
9529
|
//#endregion
|
|
9530
|
+
//#region ../../packages/review-harness/src/workspace/notifications.ts
|
|
9531
|
+
/**
|
|
9532
|
+
* What happened while the reviewer was looking at something else, kept until they have seen it.
|
|
9533
|
+
*
|
|
9534
|
+
* **One file per notification, not one list.** Notifications are written by whichever process
|
|
9535
|
+
* drove the work to its end: the CLI for a review typed at a terminal, the workspace server for one
|
|
9536
|
+
* started from the page, the sweep for one whose driver died. A single `notifications.json` read,
|
|
9537
|
+
* modified and written by several processes loses whichever entry was written second, and the lost
|
|
9538
|
+
* one is by definition something the reviewer was not watching. `jobs/` is laid out the same way for
|
|
9539
|
+
* the same reason.
|
|
9540
|
+
*
|
|
9541
|
+
* **Created once, with `wx`.** The id is derived from the transition it records, so a second attempt
|
|
9542
|
+
* to record the same transition fails at the filesystem and returns null, and a caller that only
|
|
9543
|
+
* delivers what it created cannot deliver twice. Nothing that reads a job ever creates one: a page
|
|
9544
|
+
* reloading, a client polling or a review being reopened cannot announce anything.
|
|
9545
|
+
*
|
|
9546
|
+
* Imports only `store_root`, for the cycle `queue.ts` describes.
|
|
9547
|
+
*/
|
|
9548
|
+
var NOTIFICATIONS_DIR = join(STORE_ROOT, "notifications");
|
|
9549
|
+
/** As long as a review is kept, so the list never names a review nobody can open for that reason. */
|
|
9550
|
+
var NOTIFICATION_RETENTION_MS = 2592e6;
|
|
9551
|
+
var OPERATION_NAMES = {
|
|
9552
|
+
review: "Review",
|
|
9553
|
+
reanalysis: "Reanalysis",
|
|
9554
|
+
update: "Review update",
|
|
9555
|
+
kiss: "KISS analysis"
|
|
9556
|
+
};
|
|
9557
|
+
/** What happened, in the words the list uses: `Review ready`, `Review update failed`. */
|
|
9558
|
+
function eventName(notification) {
|
|
9559
|
+
return `${OPERATION_NAMES[notification.operation]} ${notification.outcome}`;
|
|
9560
|
+
}
|
|
9561
|
+
/** The whole line, for anything that shows one event on its own: what happened, then to what. */
|
|
9562
|
+
function headline(notification) {
|
|
9563
|
+
return `${eventName(notification)} · ${notification.subject}`;
|
|
9564
|
+
}
|
|
9565
|
+
var OPERATIONS = [
|
|
9566
|
+
"review",
|
|
9567
|
+
"reanalysis",
|
|
9568
|
+
"update",
|
|
9569
|
+
"kiss"
|
|
9570
|
+
];
|
|
9571
|
+
var OUTCOMES = ["ready", "failed"];
|
|
9572
|
+
/** An id is a file name, so it is held to a shape that cannot leave the directory. */
|
|
9573
|
+
var ID = /^[A-Za-z0-9-]{1,200}$/;
|
|
9574
|
+
function pathOf(id) {
|
|
9575
|
+
return join(NOTIFICATIONS_DIR, `${id}.json`);
|
|
9576
|
+
}
|
|
9577
|
+
/**
|
|
9578
|
+
* Record one event, or return null because it is already recorded or could not be written.
|
|
9579
|
+
*
|
|
9580
|
+
* Never throws. A notification that could not be written is a notification the reviewer does not
|
|
9581
|
+
* get, which is a smaller loss than the review job that was being announced.
|
|
9582
|
+
*/
|
|
9583
|
+
function createNotification(input, now = Date.now()) {
|
|
9584
|
+
if (!ID.test(input.id)) return null;
|
|
9585
|
+
const record = {
|
|
9586
|
+
version: 1,
|
|
9587
|
+
kind: "analysis",
|
|
9588
|
+
...input,
|
|
9589
|
+
createdAt: now
|
|
9590
|
+
};
|
|
9591
|
+
try {
|
|
9592
|
+
mkdirSync(NOTIFICATIONS_DIR, { recursive: true });
|
|
9593
|
+
writeFileSync(pathOf(input.id), JSON.stringify(record), {
|
|
9594
|
+
flag: "wx",
|
|
9595
|
+
mode: 384
|
|
9596
|
+
});
|
|
9597
|
+
return record;
|
|
9598
|
+
} catch {
|
|
9599
|
+
return null;
|
|
9600
|
+
}
|
|
9601
|
+
}
|
|
9602
|
+
/** Every notification, newest first. Unreadable and malformed files are skipped, not reported. */
|
|
9603
|
+
function listNotifications() {
|
|
9604
|
+
let names;
|
|
9605
|
+
try {
|
|
9606
|
+
names = readdirSync(NOTIFICATIONS_DIR);
|
|
9607
|
+
} catch {
|
|
9608
|
+
return [];
|
|
9609
|
+
}
|
|
9610
|
+
const found = [];
|
|
9611
|
+
for (const name of names) {
|
|
9612
|
+
if (!name.endsWith(".json")) continue;
|
|
9613
|
+
const record = readNotification(name.slice(0, -5));
|
|
9614
|
+
if (record) found.push(record);
|
|
9615
|
+
}
|
|
9616
|
+
return found.sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? -1 : 1));
|
|
9617
|
+
}
|
|
9618
|
+
function readNotification(id) {
|
|
9619
|
+
if (!ID.test(id)) return null;
|
|
9620
|
+
try {
|
|
9621
|
+
const parsed = JSON.parse(readFileSync(pathOf(id), "utf8"));
|
|
9622
|
+
return isNotification(parsed) && parsed.id === id ? parsed : null;
|
|
9623
|
+
} catch {
|
|
9624
|
+
return null;
|
|
9625
|
+
}
|
|
9626
|
+
}
|
|
9627
|
+
function unreadCount() {
|
|
9628
|
+
return listNotifications().filter((notification) => notification.readAt === void 0).length;
|
|
9629
|
+
}
|
|
9630
|
+
/**
|
|
9631
|
+
* Mark these read, and say how many changed.
|
|
9632
|
+
*
|
|
9633
|
+
* Only ever by something the reviewer did to the notification itself: opening it, or one of the two
|
|
9634
|
+
* mark buttons. Opening the review it points at is not the same act and does not come through here.
|
|
9635
|
+
*/
|
|
9636
|
+
function markRead(ids, now = Date.now()) {
|
|
9637
|
+
let changed = 0;
|
|
9638
|
+
for (const id of ids) {
|
|
9639
|
+
const record = readNotification(id);
|
|
9640
|
+
if (!record || record.readAt !== void 0) continue;
|
|
9641
|
+
try {
|
|
9642
|
+
writeFileSync(pathOf(id), JSON.stringify({
|
|
9643
|
+
...record,
|
|
9644
|
+
readAt: now
|
|
9645
|
+
}), { mode: 384 });
|
|
9646
|
+
changed += 1;
|
|
9647
|
+
} catch {}
|
|
9648
|
+
}
|
|
9649
|
+
return changed;
|
|
9650
|
+
}
|
|
9651
|
+
function markAllRead(now = Date.now()) {
|
|
9652
|
+
return markRead(listNotifications().filter((notification) => notification.readAt === void 0).map((notification) => notification.id), now);
|
|
9653
|
+
}
|
|
9654
|
+
/**
|
|
9655
|
+
* Forget what is older than the retention, and anything past the cap, oldest first.
|
|
9656
|
+
*
|
|
9657
|
+
* Files that do not parse are removed once they are as old as the retention, judged by their
|
|
9658
|
+
* modification time, so a stray file neither lingers for ever nor disappears the moment it is seen.
|
|
9659
|
+
*/
|
|
9660
|
+
function pruneNotifications(now = Date.now()) {
|
|
9661
|
+
let names;
|
|
9662
|
+
try {
|
|
9663
|
+
names = readdirSync(NOTIFICATIONS_DIR);
|
|
9664
|
+
} catch {
|
|
9665
|
+
return 0;
|
|
9666
|
+
}
|
|
9667
|
+
const kept = listNotifications();
|
|
9668
|
+
const keep = new Set(kept.filter((notification) => now - notification.createdAt < NOTIFICATION_RETENTION_MS).slice(0, 200).map((notification) => `${notification.id}.json`));
|
|
9669
|
+
const valid = new Set(kept.map((notification) => `${notification.id}.json`));
|
|
9670
|
+
let removed = 0;
|
|
9671
|
+
for (const name of names) {
|
|
9672
|
+
if (!name.endsWith(".json") || keep.has(name)) continue;
|
|
9673
|
+
const path = join(NOTIFICATIONS_DIR, name);
|
|
9674
|
+
try {
|
|
9675
|
+
if (!valid.has(name) && now - statSync(path).mtimeMs < 2592e6) continue;
|
|
9676
|
+
unlinkSync(path);
|
|
9677
|
+
removed += 1;
|
|
9678
|
+
} catch {}
|
|
9679
|
+
}
|
|
9680
|
+
return removed;
|
|
9681
|
+
}
|
|
9682
|
+
function isNotification(value) {
|
|
9683
|
+
if (!value || typeof value !== "object") return false;
|
|
9684
|
+
const record = value;
|
|
9685
|
+
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");
|
|
9686
|
+
}
|
|
9687
|
+
//#endregion
|
|
9688
|
+
//#region ../../packages/review-harness/src/workspace/announce.ts
|
|
9689
|
+
/**
|
|
9690
|
+
* Tell the reviewer a job ended, once, after the ending has been written.
|
|
9691
|
+
*
|
|
9692
|
+
* Called by the three places that write an ending: `drive` in `review_job.ts`, `updateReview`, and
|
|
9693
|
+
* the sweep for a job whose driver died. Each calls it with the record it has just saved, in the
|
|
9694
|
+
* process that saved it, so the answer never depends on who happens to be looking.
|
|
9695
|
+
*
|
|
9696
|
+
* The id is the job and the moment it ended, so two calls about the same ending make one record,
|
|
9697
|
+
* and a job picked up again that ends later is a new event with its own.
|
|
9698
|
+
*
|
|
9699
|
+
* The record is the event. The Notifications page and the workspace's toasts both read it. Any
|
|
9700
|
+
* delivery that has to be pushed rather than read belongs here, after `createNotification`, and only
|
|
9701
|
+
* for a record this call created, which carries the once-only guarantee over to it for free. Desktop
|
|
9702
|
+
* delivery is written that way and parked outside the product: see `desktop_preference.ts`.
|
|
9703
|
+
*
|
|
9704
|
+
* Never throws. This runs on the success path of a review that has already been saved, and the
|
|
9705
|
+
* accounting must not break the thing it accounts for.
|
|
9706
|
+
*/
|
|
9707
|
+
function announceJob(job) {
|
|
9708
|
+
try {
|
|
9709
|
+
if (!isTerminal(job) || job.failure?.stopped || purposeOf(job) === "ask") return;
|
|
9710
|
+
const workspace = loadWorkspace(job.workspaceId);
|
|
9711
|
+
if (!workspace) return;
|
|
9712
|
+
const branch = branchOf(workspace);
|
|
9713
|
+
createNotification({
|
|
9714
|
+
id: `${job.id}-${endedAt(job)}`,
|
|
9715
|
+
operation: operationOf(job),
|
|
9716
|
+
outcome: job.phase === "done" ? "ready" : "failed",
|
|
9717
|
+
reviewId: workspace.id,
|
|
9718
|
+
subject: subjectOf(workspace),
|
|
9719
|
+
...branch ? { branch } : {},
|
|
9720
|
+
repo: basename(workspace.repoPath)
|
|
9721
|
+
});
|
|
9722
|
+
} catch {}
|
|
9723
|
+
}
|
|
9724
|
+
function operationOf(job) {
|
|
9725
|
+
if (job.reviewType === "kiss") return "kiss";
|
|
9726
|
+
if (purposeOf(job) === "update") return "update";
|
|
9727
|
+
return job.reanalysis ? "reanalysis" : "review";
|
|
9728
|
+
}
|
|
9729
|
+
/**
|
|
9730
|
+
* What was reviewed, as the reviewer would name it, and nothing that locates it on disk.
|
|
9731
|
+
*
|
|
9732
|
+
* A pull request by its number, spelled the way its forge spells it; a commit by its short sha; a
|
|
9733
|
+
* branch by the ref it follows, which is the same rule the reviews list and the CLI use.
|
|
9734
|
+
*/
|
|
9735
|
+
function subjectOf(workspace) {
|
|
9736
|
+
const changeSet = workspace.changeSet;
|
|
9737
|
+
if (changeSet.targetType === "commit") return `commit ${changeSet.data.pullRequest.head.sha.slice(0, 7)}`;
|
|
9738
|
+
const number = changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number;
|
|
9739
|
+
if (number) return /\/merge_requests\//.test(workspace.prContext?.pullRequest.url ?? "") ? `MR !${number}` : `PR #${number}`;
|
|
9740
|
+
return followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef;
|
|
9741
|
+
}
|
|
9742
|
+
/**
|
|
9743
|
+
* The branch beside a pull request's number, which alone does not say what the change is. Nothing
|
|
9744
|
+
* for a branch review, whose subject already is the branch, or for a commit, which has none.
|
|
9745
|
+
*/
|
|
9746
|
+
function branchOf(workspace) {
|
|
9747
|
+
const changeSet = workspace.changeSet;
|
|
9748
|
+
if (changeSet.targetType === "commit") return void 0;
|
|
9749
|
+
if (!(changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number)) return void 0;
|
|
9750
|
+
return (followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef) || void 0;
|
|
9751
|
+
}
|
|
9752
|
+
//#endregion
|
|
9496
9753
|
//#region ../../packages/review-harness/src/telemetry/consent.ts
|
|
9497
9754
|
/**
|
|
9498
9755
|
* Who this machine is counted as, and what it has agreed to.
|
|
@@ -9815,6 +10072,7 @@ async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel)
|
|
|
9815
10072
|
target,
|
|
9816
10073
|
env: agent.captureEnv(),
|
|
9817
10074
|
purpose: "review",
|
|
10075
|
+
reanalysis: true,
|
|
9818
10076
|
...model ? { requestedModel: model } : {}
|
|
9819
10077
|
});
|
|
9820
10078
|
saveJob(job);
|
|
@@ -10030,6 +10288,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
10030
10288
|
else await applyGuide(job, workspaceId, payload, observedModel);
|
|
10031
10289
|
job = update(job, "done");
|
|
10032
10290
|
finished(job, workspaceId, startedAt, true);
|
|
10291
|
+
announceJob(job);
|
|
10033
10292
|
return job;
|
|
10034
10293
|
} catch (error) {
|
|
10035
10294
|
if (error instanceof AgentCancelledError || signal?.aborted) {
|
|
@@ -10043,6 +10302,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
10043
10302
|
job = recordFailure(job, explain(job, raw), driver, kind, raw);
|
|
10044
10303
|
if (job.reviewType === "kiss") failCurrentKissRun(workspaceId, job, job.failure?.message ?? raw, kind, error instanceof KissRejected ? error.answer : void 0);
|
|
10045
10304
|
finished(job, workspaceId, startedAt, false);
|
|
10305
|
+
announceJob(job);
|
|
10046
10306
|
return job;
|
|
10047
10307
|
}
|
|
10048
10308
|
}
|
|
@@ -11221,10 +11481,14 @@ async function updateReview(workspace, runners = {}) {
|
|
|
11221
11481
|
const step = (phase) => saveJob(advance(current(), phase));
|
|
11222
11482
|
try {
|
|
11223
11483
|
const outcome = await bringForward(workspace, runners, reviewedSha, held, step);
|
|
11224
|
-
|
|
11484
|
+
const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
|
|
11485
|
+
saveJob(ended);
|
|
11486
|
+
announceJob(ended);
|
|
11225
11487
|
return outcome;
|
|
11226
11488
|
} catch (error) {
|
|
11227
|
-
|
|
11489
|
+
const failed = fail(current(), error instanceof Error ? error.message : String(error));
|
|
11490
|
+
saveJob(failed);
|
|
11491
|
+
announceJob(failed);
|
|
11228
11492
|
throw error;
|
|
11229
11493
|
} finally {
|
|
11230
11494
|
held.claim.release();
|
|
@@ -11814,4 +12078,4 @@ function lineageIds(id) {
|
|
|
11814
12078
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
11815
12079
|
}
|
|
11816
12080
|
//#endregion
|
|
11817
|
-
export {
|
|
12081
|
+
export { BUILD_VERSION as $, issuesOutstanding as $t, markRead as A, lineageKeyFor as At, modelHelpLines as B, summarise as Bt, recordTelemetryUploadConsent as C, clearClaim as Ct, headline as D, groupByLineage as Dt, eventName as E, followedRefName as Et, EXPLAIN_SIMPLY_PROMPT as F, previousKissResult as Ft, ensureServer as G, queueAdd as Gt, readIndexToken as H, workspaceRevision as Ht, ReviewBeingDeletedError as I, recentWorkspaces as It, serverEntryExists as J, readQueue as Jt, readServerRecord as K, queueRemove as Kt, MANAGED_ROOT as L, reviewedCommit as Lt, readNotification as M, loadWorkspace as Mt, unreadCount as N, matchingWorkspaceIds as Nt, listNotifications as O, indexReviewGroups as Ot, withUsageRecorded as P, positionInLineage as Pt, writeServerRecord as Q, isQuestionOutstanding as Qt, readMarker as R, reviewedRepositories as Rt, record as S, purposeOf as St, announceJob as T, isClaimed as Tt, bootstrapUrl as U, workspaceStamps as Ut, relativeTime as V, touchWorkspace as Vt, clearServerRecord as W, dequeueOnComplete as Wt, stopServer as X, doneVerb as Xt, spawnServer as Y, writeQueue as Yt, workspaceUrl as Z, isIssueOutstanding as Zt, startKissJob as _, agentFor as _n, loadJob as _t, wasBlocked as a, displayRef as an, parseWorkspacePort as at, startJob as b, AgentUnavailableError as bn, phasesFor as bt, updateReview as c, resolveModel as cn, writePortPreference as ct, askCheckout as d, writeAgentPreference as dn, checkCodeFreshness as dt, questionsOutstanding as en, PACKAGE_NAME as et, UpdateAlreadyRunningError as f, CONFIG_PATH as fn, checkReview as ft, recordKissRun as g, agentById as gn, isTerminal as gt, agentEnvOf as h, DEFAULT_AGENT_ID as hn, fail as ht, liveJobsFor as i, resolveTarget as in, MIN_WORKSPACE_PORT as it, pruneNotifications as j, lineagePosition as jt, markAllRead as k, latestKissRun as kt, refreshPrContext as l, clearAgentPreference as ln, forgeResolver as lt, runningJobs as m, AGENT_IDS as mn, endedAt as mt, deleteReview as n, processDiscussion as nn, DEFAULT_WORKSPACE_PORT as nt, discardJob as o, git as on, resolveWorkspacePort as ot, liveUpdateFor as p, STORE_ROOT as pn, allJobs as pt, reviewsUrl as q, queueStamp as qt, lineageIds as r, describeAuthorship as rn, MAX_WORKSPACE_PORT as rt, RefreshUnavailableError as s, resolveAgent as sn, statedWorkspacePort as st, deleteLineage as t, reviewerDispositions as tn, feedbackUrl as tt, isUnchanged as u, readAgentPreference as un, fillFileUrlTemplate as ut, reanalyseReview as v, detectAgents as vn, saveJob as vt, telemetryUploadConsent as w, driverGone as wt, readEvents as x, progressSteps as xt, runJob as y, AgentCancelledError as yn, PHASES as yt, removeWorktree as z, saveWorkspace as zt };
|