prreviewbuddy 0.27.2 → 0.30.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 +72 -0
- package/README.md +9 -8
- package/dist/{delete_review-B3xciJ03.js → delete_review-B_2J2Wn9.js} +217 -22
- package/dist/main.js +102 -108
- package/dist/server.js +259 -51
- package/package.json +1 -1
- package/static/reviews.js +1 -1
- package/static/shell.css +72 -1
- package/static/workspace.js +29 -29
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as fillFileUrlTemplate, $t as
|
|
1
|
+
import { $ as fillFileUrlTemplate, $t as writeAgentPreference, A as removeWorktree, At as workspaceRevision, Bt as isQuestionOutstanding, C as recordTelemetryUploadConsent, Ct as previousKissResult, D as ReviewBeingDeletedError, Dt as saveWorkspace, E as EXPLAIN_SIMPLY_PROMPT, F as clearServerRecord, Ft as queueStamp, G as DEFAULT_WORKSPACE_PORT, Gt as describeAuthorship, H as BUILD_VERSION, Ht as questionsOutstanding, It as readQueue, J as parseWorkspacePort, Jt as git, K as MAX_WORKSPACE_PORT, L as readServerRecord, Lt as writeQueue, M as relativeTime$1, Mt as dequeueOnComplete, N as readIndexToken, Nt as queueAdd, O as MANAGED_ROOT, Pt as queueRemove, Qt as readAgentPreference, Rt as doneVerb, S as record, T as withUsageRecorded, Tt as reviewedCommit, Ut as reviewerDispositions, V as writeServerRecord, Vt as issuesOutstanding, W as feedbackUrl, Wt as processDiscussion, X as statedWorkspacePort, Xt as resolveModel, Y as resolveWorkspacePort, Z as writePortPreference, Zt as clearAgentPreference, _ as startKissJob, _t as latestKissRun, a as wasBlocked, an as agentFor, at as isTerminal, bt as loadWorkspace, c as updateReview, cn as AgentUnavailableError, d as askCheckout, dt as purposeOf, et as checkCodeFreshness, f as UpdateAlreadyRunningError, gt as indexReviewGroups, h as agentEnvOf, i as liveJobsFor, it as fail, jt as workspaceStamps, k as readMarker, kt as touchWorkspace, l as refreshPrContext, lt as phasesFor, mt as followedRefName, n as deleteReview, nn as AGENT_IDS, nt as allJobs, o as discardJob, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, r as lineageIds, rt as endedAt, s as RefreshUnavailableError, sn as AgentCancelledError, st as saveJob, t as deleteLineage, tn as STORE_ROOT, tt as checkFreshness, u as isUnchanged, ut as progressSteps, w as telemetryUploadConsent, wt as recentWorkspaces, x as readEvents, y as runJob, yt as lineagePosition, zt as isIssueOutstanding } from "./delete_review-B_2J2Wn9.js";
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -229,6 +229,36 @@ function wayBackSentence(way = wayBack()) {
|
|
|
229
229
|
return way.commands.map(({ command, where }) => `${command} in ${where}`).join(", or ");
|
|
230
230
|
}
|
|
231
231
|
//#endregion
|
|
232
|
+
//#region ../../packages/review-harness/src/workspace/rail_icons.ts
|
|
233
|
+
/**
|
|
234
|
+
* The rail's icons, one per row that is not a theme.
|
|
235
|
+
*
|
|
236
|
+
* A pure module, because the client draws the Diagrams row itself when the first drawing lands and
|
|
237
|
+
* cannot import `page.ts`. Themes keep their numbers: the order is information, and an icon there
|
|
238
|
+
* would replace it with decoration.
|
|
239
|
+
*
|
|
240
|
+
* Stroked in `currentColor` on a 24-unit grid, so every icon takes the row's colour, including
|
|
241
|
+
* Stop server's red on hover.
|
|
242
|
+
*/
|
|
243
|
+
var PATHS = {
|
|
244
|
+
review: "<path d=\"M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2\"/><rect x=\"9\" y=\"3\" width=\"6\" height=\"4\" rx=\"1\"/><path d=\"m9 14 2 2 4-4\"/>",
|
|
245
|
+
kiss: "<path d=\"M12 3v4M12 17v4M3 12h4M17 12h4\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>",
|
|
246
|
+
diagrams: "<rect x=\"3\" y=\"3\" width=\"7\" height=\"6\" rx=\"1\"/><rect x=\"14\" y=\"15\" width=\"7\" height=\"6\" rx=\"1\"/><path d=\"M6.5 9v3a2 2 0 0 0 2 2h7a2 2 0 0 1 2 2v-1\"/>",
|
|
247
|
+
issues: "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/>",
|
|
248
|
+
questions: "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01\"/>",
|
|
249
|
+
files: "<path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\"/><path d=\"M14 3v5h5M9 13h6M9 17h6\"/>",
|
|
250
|
+
prcontext: "<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/>",
|
|
251
|
+
recommendations: "<path d=\"M9 18h6M10 22h4\"/><path d=\"M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.2 1 2V17h6v-.3c0-.8.4-1.5 1-2A7 7 0 0 0 12 2z\"/>",
|
|
252
|
+
reviews: "<path d=\"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\"/>",
|
|
253
|
+
settings: "<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z\"/>",
|
|
254
|
+
feedback: "<path d=\"m22 2-7 20-4-9-9-4z\"/><path d=\"M22 2 11 13\"/>",
|
|
255
|
+
stop: "<path d=\"M12 2v10\"/><path d=\"M18.4 6.6a9 9 0 1 1-12.8 0\"/>"
|
|
256
|
+
};
|
|
257
|
+
/** The icon slot for one rail row. Hidden from assistive technology: the label beside it says it. */
|
|
258
|
+
function railIcon(icon) {
|
|
259
|
+
return `<span class="prb-rail-icon" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">${PATHS[icon]}</svg></span>`;
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
232
262
|
//#region ../../packages/review-harness/src/domain/utils/file_path.ts
|
|
233
263
|
/**
|
|
234
264
|
* Splits a repo-relative path for display, so the filename can be shown prominently and the
|
|
@@ -1363,7 +1393,7 @@ function renderGuideSectionCard(section, counts, activeGuidedSectionId, status,
|
|
|
1363
1393
|
const statusBadge = isCompleted ? `<span class="pr-helper-guide-badge pr-helper-completed-badge">Completed</span>` : status === "active" ? `<span class="pr-helper-guide-badge pr-helper-start-here-badge">${escapeHtml$1(activeBadgeLabel)}</span>` : "";
|
|
1364
1394
|
const undo = isCompleted ? `<button type="button" class="pr-helper-guide-uncomplete" data-guide-uncomplete="${escapeHtml$1(section.id)}">Mark as not reviewed</button>` : "";
|
|
1365
1395
|
return `
|
|
1366
|
-
<section class="pr-helper-guide-section${isCompleted ? " pr-helper-guide-section-completed" : ""}" id="pr-helper-guide-section-${escapeHtml$1(section.id)}" data-guide-section-card="${escapeHtml$1(section.id)}">
|
|
1396
|
+
<section class="pr-helper-guide-section${isCompleted ? " pr-helper-guide-section-completed" : ""}${options?.collapsedSections ? " pr-helper-guide-section-collapsed" : ""}" id="pr-helper-guide-section-${escapeHtml$1(section.id)}" data-guide-section-card="${escapeHtml$1(section.id)}">
|
|
1367
1397
|
<button type="button" class="pr-helper-guide-section-header" data-guide-toggle="${escapeHtml$1(section.id)}">
|
|
1368
1398
|
<div class="pr-helper-guide-section-badges">
|
|
1369
1399
|
${check}
|
|
@@ -3483,7 +3513,10 @@ function renderStandardViews(view, sections) {
|
|
|
3483
3513
|
diagrams: true,
|
|
3484
3514
|
detailsInHeader: true
|
|
3485
3515
|
})}</section>
|
|
3486
|
-
<section class="prb-view" data-view="guide" hidden>${renderGuideTab(session, null, null, {
|
|
3516
|
+
<section class="prb-view" data-view="guide" hidden>${renderGuideTab(session, null, null, {
|
|
3517
|
+
diagrams: true,
|
|
3518
|
+
collapsedSections: true
|
|
3519
|
+
})}</section>
|
|
3487
3520
|
<section class="prb-view" data-view="diagram" hidden data-diagram-pane${view.pendingDiagram ? ` data-diagram-running="${escapeHtml$1(view.pendingDiagram.sectionId ?? "")}"` : ""}>
|
|
3488
3521
|
<h3 class="prb-doc-title">Diagrams</h3>
|
|
3489
3522
|
${renderDiagrams(view.diagrams ?? [], sections, view.reviewedSha ?? null)}
|
|
@@ -3652,7 +3685,7 @@ function renderHeader(view, freshness) {
|
|
|
3652
3685
|
return `<button type="button" class="prb-fresh-check" data-fresh-check data-state="${state}" aria-expanded="false" title="How this review stands against the branch as it is now">${escapeHtml$1(label)}</button>`;
|
|
3653
3686
|
})() : ""}
|
|
3654
3687
|
${view.token ? `<button type="button" class="prb-complete" data-complete aria-pressed="${view.complete === true}">${view.complete ? "Reviewed" : "Mark reviewed"}</button>` : ""}
|
|
3655
|
-
${view.id ? renderIdMenu(view.id) : ""}
|
|
3688
|
+
${view.id ? renderIdMenu(view.id, Boolean(view.token)) : ""}
|
|
3656
3689
|
${view.session.result ? "<button type=\"button\" class=\"prb-ask-toggle\" data-ask-toggle aria-expanded=\"false\">Buddy AI</button>" : ""}
|
|
3657
3690
|
</div>
|
|
3658
3691
|
</header>`;
|
|
@@ -3667,17 +3700,29 @@ function renderHeader(view, freshness) {
|
|
|
3667
3700
|
* the address bar.
|
|
3668
3701
|
*
|
|
3669
3702
|
* The same `...` and the same menu markup as the index, deliberately. One gesture, in both places
|
|
3670
|
-
* it can be wanted, behaving identically.
|
|
3671
|
-
*
|
|
3672
|
-
*
|
|
3703
|
+
* it can be wanted, behaving identically. That includes deleting, with the same confirmation in the
|
|
3704
|
+
* same panel and the same sentence. The list is where you land afterwards, since the page you were
|
|
3705
|
+
* reading no longer exists. It deletes this review only, as a history row on the list does.
|
|
3706
|
+
*
|
|
3707
|
+
* No delete where there is no server, which is the offline preview: the item would be a lie there.
|
|
3673
3708
|
*/
|
|
3674
|
-
function renderIdMenu(id) {
|
|
3709
|
+
function renderIdMenu(id, canDelete) {
|
|
3710
|
+
const safe = escapeHtml$1(id);
|
|
3675
3711
|
return `<div class="prb-menu" data-menu>
|
|
3676
3712
|
<button type="button" class="prb-menu-trigger" data-menu-trigger aria-expanded="false" aria-haspopup="true" aria-label="Actions for this review">⋯</button>
|
|
3677
3713
|
<div class="prb-menu-panel" data-menu-panel hidden>
|
|
3678
3714
|
<div data-menu-items>
|
|
3679
|
-
<button type="button" class="prb-menu-item" data-copy-id="${
|
|
3715
|
+
<button type="button" class="prb-menu-item" data-copy-id="${safe}">Copy review ID</button>
|
|
3716
|
+
${canDelete ? `<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${safe}">Delete review</button>` : ""}
|
|
3680
3717
|
</div>
|
|
3718
|
+
${canDelete ? `<div class="prb-menu-confirm" data-menu-confirm hidden>
|
|
3719
|
+
<p class="prb-menu-question">Delete this review?</p>
|
|
3720
|
+
<p class="prb-menu-note">This cannot be undone. Your branch and its commits are untouched.</p>
|
|
3721
|
+
<div class="prb-menu-buttons">
|
|
3722
|
+
<button type="button" class="prb-menu-item prb-menu-item-danger" data-confirm-delete>Delete</button>
|
|
3723
|
+
<button type="button" class="prb-menu-item" data-cancel-delete>Cancel</button>
|
|
3724
|
+
</div>
|
|
3725
|
+
</div>` : ""}
|
|
3681
3726
|
</div>
|
|
3682
3727
|
</div>`;
|
|
3683
3728
|
}
|
|
@@ -3699,22 +3744,22 @@ function renderIdMenu(id) {
|
|
|
3699
3744
|
*/
|
|
3700
3745
|
function renderRailFoot(view) {
|
|
3701
3746
|
return `<div class="prb-rail-group prb-rail-foot">
|
|
3702
|
-
${view.token ? `<a class="prb-rail-item" href="/">
|
|
3703
|
-
|
|
3747
|
+
${view.token ? `<a class="prb-rail-item prb-rail-link" href="/">
|
|
3748
|
+
${railIcon("reviews")}
|
|
3704
3749
|
<span class="prb-rail-label"><span class="prb-rail-title">All reviews</span></span>
|
|
3705
3750
|
</a>
|
|
3706
|
-
<a class="prb-rail-item" href="/settings">
|
|
3707
|
-
|
|
3751
|
+
<a class="prb-rail-item prb-rail-link" href="/settings">
|
|
3752
|
+
${railIcon("settings")}
|
|
3708
3753
|
<span class="prb-rail-label"><span class="prb-rail-title">Settings</span></span>
|
|
3709
3754
|
</a>` : ""}
|
|
3710
|
-
<a class="prb-rail-item" href="${escapeHtml$1(feedbackUrl())}" target="_blank" rel="noopener noreferrer">
|
|
3711
|
-
|
|
3755
|
+
<a class="prb-rail-item prb-rail-link" href="${escapeHtml$1(feedbackUrl())}" target="_blank" rel="noopener noreferrer">
|
|
3756
|
+
${railIcon("feedback")}
|
|
3712
3757
|
<span class="prb-rail-label"><span class="prb-rail-title">Send feedback</span></span>
|
|
3713
3758
|
</a>
|
|
3714
3759
|
</div>
|
|
3715
3760
|
<div class="prb-rail-group prb-rail-ops">
|
|
3716
|
-
${view.token ? `<button type="button" class="prb-rail-item prb-rail-danger" data-shutdown>
|
|
3717
|
-
|
|
3761
|
+
${view.token ? `<button type="button" class="prb-rail-item prb-rail-link prb-rail-danger" data-shutdown>
|
|
3762
|
+
${railIcon("stop")}
|
|
3718
3763
|
<span class="prb-rail-label"><span class="prb-rail-title">Stop server</span></span>
|
|
3719
3764
|
</button>` : ""}
|
|
3720
3765
|
<p class="prb-build">v${escapeHtml$1(BUILD_VERSION)}</p>
|
|
@@ -3766,6 +3811,7 @@ function renderRail(view, sections) {
|
|
|
3766
3811
|
${(view.session.result ? railGroup({
|
|
3767
3812
|
id: "prb-rail-review",
|
|
3768
3813
|
title: "Review",
|
|
3814
|
+
icon: "review",
|
|
3769
3815
|
view: "overview",
|
|
3770
3816
|
current: true,
|
|
3771
3817
|
badge: null,
|
|
@@ -3777,11 +3823,11 @@ function renderRail(view, sections) {
|
|
|
3777
3823
|
<div class="prb-rail-path" id="prb-rail-path">
|
|
3778
3824
|
${themes || "<p class=\"prb-rail-heading\">No themes</p>"}
|
|
3779
3825
|
</div>
|
|
3780
|
-
${(view.diagrams?.length ?? 0) > 0 ? railLink("diagram", "Diagrams", diagramsBadge(view.diagrams?.length ?? 0)) : ""}
|
|
3781
|
-
${railLink("issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)))}
|
|
3782
|
-
${railLink("questions", "Questions", remainingBadge(questionsOutstanding(questions, answered), "question", "answered"))}
|
|
3783
|
-
${railLink("files", "Files", totalBadge(view.changeSet.data.files.length, "files"))}
|
|
3784
|
-
${view.prContext ? railLink("prcontext", "PR context", totalBadge(view.prContext.processed.stats.totalThreads, "PR comment threads")) : railLink("prcontext", "PR context", null, "No pull request was connected to this review")}`
|
|
3826
|
+
${(view.diagrams?.length ?? 0) > 0 ? railLink("diagram", "diagrams", "Diagrams", diagramsBadge(view.diagrams?.length ?? 0)) : ""}
|
|
3827
|
+
${railLink("issues", "issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)))}
|
|
3828
|
+
${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered), "question", "answered"))}
|
|
3829
|
+
${railLink("files", "files", "Files", totalBadge(view.changeSet.data.files.length, "files"))}
|
|
3830
|
+
${view.prContext ? railLink("prcontext", "prcontext", "PR context", totalBadge(view.prContext.processed.stats.totalThreads, "PR comment threads")) : railLink("prcontext", "prcontext", "PR context", null, "No pull request was connected to this review")}`
|
|
3785
3831
|
}) : "") || renderRunningRail(view)}
|
|
3786
3832
|
${renderKissRail(view)}
|
|
3787
3833
|
${renderRailFoot(view)}
|
|
@@ -3807,11 +3853,12 @@ function renderRunningRail(view) {
|
|
|
3807
3853
|
return railGroup({
|
|
3808
3854
|
id: "prb-rail-review",
|
|
3809
3855
|
title: "Review",
|
|
3856
|
+
icon: "review",
|
|
3810
3857
|
view: "progress",
|
|
3811
3858
|
current: true,
|
|
3812
3859
|
badge: null,
|
|
3813
3860
|
collapses: "the review",
|
|
3814
|
-
body: view.job && changeIsRead(view.job) ? railLink("files", "Files", totalBadge(files, "files")) : ""
|
|
3861
|
+
body: view.job && changeIsRead(view.job) ? railLink("files", "files", "Files", totalBadge(files, "files")) : ""
|
|
3815
3862
|
});
|
|
3816
3863
|
}
|
|
3817
3864
|
/**
|
|
@@ -3834,11 +3881,12 @@ function renderKissRail(view) {
|
|
|
3834
3881
|
return railGroup({
|
|
3835
3882
|
id: "prb-rail-kiss",
|
|
3836
3883
|
title: "KISS",
|
|
3884
|
+
icon: "kiss",
|
|
3837
3885
|
view: "kiss",
|
|
3838
3886
|
current: view.session.result === null && !showsProgress(view),
|
|
3839
3887
|
badge: result ? recommendationsBadge(result.recommendations.length) : null,
|
|
3840
3888
|
collapses: "the KISS review",
|
|
3841
|
-
body: result ? railLink("kissrecs", "Recommendations", recommendationsBadge(result.recommendations.length)) : ""
|
|
3889
|
+
body: result ? railLink("kissrecs", "recommendations", "Recommendations", recommendationsBadge(result.recommendations.length)) : ""
|
|
3842
3890
|
});
|
|
3843
3891
|
}
|
|
3844
3892
|
/** The one chevron both twisties draw. Rotated by CSS off `aria-expanded`, never by markup. */
|
|
@@ -3858,7 +3906,7 @@ function railGroup(group) {
|
|
|
3858
3906
|
return `<div class="prb-rail-group">
|
|
3859
3907
|
<div class="prb-rail-row">
|
|
3860
3908
|
<button type="button" class="prb-rail-item prb-rail-group-title" data-view-nav="${escapeHtml$1(group.view)}"${group.current ? " aria-current=\"true\"" : ""}>
|
|
3861
|
-
|
|
3909
|
+
${railIcon(group.icon)}
|
|
3862
3910
|
<span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(group.title)}</span></span>
|
|
3863
3911
|
${renderBadge(group.badge)}
|
|
3864
3912
|
</button>
|
|
@@ -3944,9 +3992,9 @@ function renderBadge(badge) {
|
|
|
3944
3992
|
return `<span class="prb-rail-count${badge.done ? " prb-rail-count-done" : ""}" aria-label="${escapeHtml$1(badge.label)}">${escapeHtml$1(badge.text)}</span>`;
|
|
3945
3993
|
}
|
|
3946
3994
|
/** A null badge renders nothing, for the case where zero would be a claim rather than a total. */
|
|
3947
|
-
function railLink(view, label, badge, note) {
|
|
3948
|
-
return `<button type="button" class="prb-rail-item" data-view-nav="${view}"${note ? ` title="${escapeHtml$1(note)}"` : ""}>
|
|
3949
|
-
|
|
3995
|
+
function railLink(view, icon, label, badge, note) {
|
|
3996
|
+
return `<button type="button" class="prb-rail-item prb-rail-link" data-view-nav="${view}"${note ? ` title="${escapeHtml$1(note)}"` : ""}>
|
|
3997
|
+
${railIcon(icon)}
|
|
3950
3998
|
<span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(label)}</span></span>
|
|
3951
3999
|
${renderBadge(badge)}
|
|
3952
4000
|
</button>`;
|
|
@@ -4276,6 +4324,40 @@ function countLabel(reviews, branches) {
|
|
|
4276
4324
|
return `${label} · ${branches} branch${branches === 1 ? "" : "es"}`;
|
|
4277
4325
|
}
|
|
4278
4326
|
/**
|
|
4327
|
+
* Every login the author dropdown offers, once each, in alphabetical order regardless of case.
|
|
4328
|
+
* Read from the cards the page is showing, so it never lists somebody with nothing to show.
|
|
4329
|
+
*/
|
|
4330
|
+
function authorOptions(handles) {
|
|
4331
|
+
return [...new Set(handles.filter((handle) => Boolean(handle)))].sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* A card menu's status item, which sets the state it names. The bulk bar's words for the same two
|
|
4335
|
+
* actions, so a card and a selection never disagree. Here rather than in the page renderer because
|
|
4336
|
+
* the browser relabels the item after a change, and that renderer reaches the store.
|
|
4337
|
+
*/
|
|
4338
|
+
function markItemLabel(mark) {
|
|
4339
|
+
return mark === "complete" ? "Mark reviewed" : "Mark in progress";
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* What `Continue reviewing` opens, and whether it exists at all.
|
|
4343
|
+
*
|
|
4344
|
+
* One rule rather than an expression in the template, because "is there anything to continue" and
|
|
4345
|
+
* "what is first" are the same question and must not be able to answer differently -- a button that
|
|
4346
|
+
* is present but navigates nowhere is worse than no button.
|
|
4347
|
+
*/
|
|
4348
|
+
function continueTarget(queue) {
|
|
4349
|
+
return queue[0] ?? null;
|
|
4350
|
+
}
|
|
4351
|
+
/**
|
|
4352
|
+
* The Queue pill, which carries a number only once there is one.
|
|
4353
|
+
*
|
|
4354
|
+
* "Queue 0" is a count of something that is not there, and reads as a fault. Bare "Queue" reads as
|
|
4355
|
+
* the place it is -- empty, and still the thing you would click to put something in it.
|
|
4356
|
+
*/
|
|
4357
|
+
function queuePillLabel(queued) {
|
|
4358
|
+
return queued === 0 ? "Queue" : `Queue ${queued}`;
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4279
4361
|
* The words on the status chip.
|
|
4280
4362
|
*
|
|
4281
4363
|
* Here rather than in the renderer because the browser now writes this chip too: marking a
|
|
@@ -4308,6 +4390,10 @@ function renderReviewsPage(view) {
|
|
|
4308
4390
|
const now = view.now ?? Date.now();
|
|
4309
4391
|
const ordered = [...view.groups].sort((a, b) => Number(b.latest.pinned) - Number(a.latest.pinned) || touchedAt(b.latest) - touchedAt(a.latest));
|
|
4310
4392
|
const storedReviews = view.groups.reduce((total, group) => total + group.history.length, 0);
|
|
4393
|
+
const queue = view.queue ?? [];
|
|
4394
|
+
const positions = new Map(queue.map((id, position) => [id, position]));
|
|
4395
|
+
const continues = continueTarget(queue);
|
|
4396
|
+
const front = continues === null ? void 0 : findReview(view.groups, continues);
|
|
4311
4397
|
return `<!doctype html>
|
|
4312
4398
|
<html lang="en">
|
|
4313
4399
|
<head>
|
|
@@ -4329,18 +4415,19 @@ function renderReviewsPage(view) {
|
|
|
4329
4415
|
</div>
|
|
4330
4416
|
</header>
|
|
4331
4417
|
<main class="prb-index">
|
|
4332
|
-
${ordered.length === 0 ? "" : renderControls()}
|
|
4418
|
+
${ordered.length === 0 ? "" : renderControls(queue.length, authorOptions(ordered.map((group) => group.latest.authorHandle)))}
|
|
4419
|
+
${front === void 0 ? "" : renderContinue(front)}
|
|
4333
4420
|
${ordered.length === 0 ? "" : `<div class="prb-index-summary">
|
|
4334
4421
|
<p class="prb-index-count" data-count>${escapeHtml$1(countLabel(storedReviews, ordered.length))}</p>
|
|
4335
4422
|
<button type="button" class="prb-index-select" data-select-mode aria-pressed="false">Select</button>
|
|
4336
4423
|
</div>`}
|
|
4337
4424
|
<div class="prb-cards" data-cards>
|
|
4338
|
-
${ordered.map((group) => renderCard(group, now, view.analyses?.get(group.latest.id))).join("")}
|
|
4425
|
+
${ordered.map((group) => renderCard(group, now, view.analyses?.get(group.latest.id), positions.get(group.latest.id))).join("")}
|
|
4339
4426
|
</div>
|
|
4340
4427
|
${ordered.length === 0 ? `<p class="prb-index-empty">No reviews stored yet. Ask Claude Code to review a change and one will appear here.</p>` : `<p class="prb-index-none" data-no-matches hidden>Nothing matches that.</p>`}
|
|
4341
4428
|
${ordered.length > 15 ? renderPager() : ""}
|
|
4342
4429
|
${ordered.length === 0 ? "" : renderBulkBar()}
|
|
4343
|
-
<p class="prb-index-note">Reviews are kept for thirty days, on this machine only. Every review has a fixed address, so a link to one is worth keeping and still opens after a restart. Stopping the server deletes nothing; it only stops anything being served until the next review starts it again.</p>
|
|
4430
|
+
<p class="prb-index-note">Reviews are kept for thirty days, on this machine only. Saved reviews and anything in your review queue are kept for ninety days instead, counting from the last time you opened them. Every review has a fixed address, so a link to one is worth keeping and still opens after a restart. Stopping the server deletes nothing; it only stops anything being served until the next review starts it again.</p>
|
|
4344
4431
|
</main>
|
|
4345
4432
|
<script src="/assets/reviews.js" type="module"><\/script>
|
|
4346
4433
|
</body>
|
|
@@ -4355,17 +4442,51 @@ function renderReviewsPage(view) {
|
|
|
4355
4442
|
* would be four more controls for a question ("show me only the risky ones") that sorting by
|
|
4356
4443
|
* recency already answers badly and that scanning answers well.
|
|
4357
4444
|
*/
|
|
4358
|
-
function renderControls() {
|
|
4445
|
+
function renderControls(queued, authors) {
|
|
4359
4446
|
return `<div class="prb-index-controls">
|
|
4360
4447
|
<input type="search" class="prb-index-search" data-search placeholder="Search repo, branch, title, summary or file…" aria-label="Search reviews" spellcheck="false">
|
|
4448
|
+
${renderAuthorFilter(authors)}
|
|
4361
4449
|
<div class="prb-index-filters" role="group" aria-label="Filter reviews">
|
|
4362
4450
|
<button type="button" data-filter="all" aria-pressed="true">All</button>
|
|
4451
|
+
<button type="button" data-filter="queue" aria-pressed="false" data-queue-pill>${escapeHtml$1(queuePillLabel(queued))}</button>
|
|
4452
|
+
<button type="button" data-filter="saved" aria-pressed="false">Saved</button>
|
|
4363
4453
|
<button type="button" data-filter="in-progress" aria-pressed="false">In progress</button>
|
|
4364
4454
|
<button type="button" data-filter="complete" aria-pressed="false">Reviewed</button>
|
|
4365
4455
|
</div>
|
|
4366
4456
|
</div>`;
|
|
4367
4457
|
}
|
|
4368
4458
|
/**
|
|
4459
|
+
* Whose pull requests to show.
|
|
4460
|
+
*
|
|
4461
|
+
* Absent when no card carries a login, which is every page of local-branch reviews: a dropdown
|
|
4462
|
+
* whose only choice is Everyone is not a choice.
|
|
4463
|
+
*/
|
|
4464
|
+
function renderAuthorFilter(authors) {
|
|
4465
|
+
if (authors.length === 0) return "";
|
|
4466
|
+
return `<select class="prb-index-author" name="author" data-author-filter aria-label="Filter by pull request author">
|
|
4467
|
+
<option value="">Everyone</option>${authors.map((handle) => `<option value="${escapeHtml$1(handle)}">@${escapeHtml$1(handle)}</option>`).join("")}
|
|
4468
|
+
</select>`;
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* The way back into the batch, above the list.
|
|
4472
|
+
*
|
|
4473
|
+
* A link rather than a button, and carrying the review's own token, because that is what every
|
|
4474
|
+
* other way into a review on this page is -- the list is the only thing holding every credential.
|
|
4475
|
+
*
|
|
4476
|
+
* It names the front of the queue rather than saying only "Continue reviewing", so somebody
|
|
4477
|
+
* returning after an hour can tell whether it is about to open what they expect before they click.
|
|
4478
|
+
*/
|
|
4479
|
+
function renderContinue(review) {
|
|
4480
|
+
return `<a class="prb-index-continue" href="${escapeHtml$1(`/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}`)}">
|
|
4481
|
+
<span class="prb-index-continue-label">Continue reviewing</span>
|
|
4482
|
+
<span class="prb-index-continue-title">${escapeHtml$1(review.title)}</span>
|
|
4483
|
+
</a>`;
|
|
4484
|
+
}
|
|
4485
|
+
/** The review a queued id refers to. The caller has already guaranteed it is one this page shows. */
|
|
4486
|
+
function findReview(groups, id) {
|
|
4487
|
+
for (const group of groups) for (const review of group.history) if (review.id === id) return review;
|
|
4488
|
+
}
|
|
4489
|
+
/**
|
|
4369
4490
|
* The pager, with its numbers left out.
|
|
4370
4491
|
*
|
|
4371
4492
|
* How many pages there are depends on what the search matched, and matching happens in the browser
|
|
@@ -4436,15 +4557,15 @@ function renderBulkBar() {
|
|
|
4436
4557
|
* card and is shown only in selection mode, so entering that mode is a stylesheet change rather
|
|
4437
4558
|
* than a rebuild of the list.
|
|
4438
4559
|
*/
|
|
4439
|
-
function renderCard(group, now, analysis) {
|
|
4560
|
+
function renderCard(group, now, analysis, queued) {
|
|
4440
4561
|
const review = group.latest;
|
|
4441
4562
|
const status = statusOf(review);
|
|
4442
4563
|
const href = `/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}`;
|
|
4443
4564
|
const followed = followedRefName(review.branch, review.headResolvedRef) ?? review.branch;
|
|
4444
|
-
return `<article class="prb-card" data-card data-id="${escapeHtml$1(review.id)}" data-status="${status}" data-pinned="${review.pinned}" data-reviews="${group.history.length}" data-search-text="${escapeHtml$1(groupSearchText(group))}">
|
|
4565
|
+
return `<article class="prb-card" data-card data-id="${escapeHtml$1(review.id)}" data-status="${status}" data-pinned="${review.pinned}"${queued === void 0 ? "" : ` data-queue="${queued}"`}${review.authorHandle ? ` data-author="${escapeHtml$1(review.authorHandle)}"` : ""} data-reviews="${group.history.length}" data-search-text="${escapeHtml$1(groupSearchText(group))}">
|
|
4445
4566
|
<input type="checkbox" class="prb-card-select" data-select="${escapeHtml$1(review.id)}" aria-label="Select ${escapeHtml$1(followed)}">
|
|
4446
|
-
<button type="button" class="prb-card-pin" data-pin="${escapeHtml$1(review.id)}" aria-pressed="${review.pinned}" aria-label="${review.pinned ? "
|
|
4447
|
-
${renderCardMenu(group, followed)}
|
|
4567
|
+
<button type="button" class="prb-card-pin" data-pin="${escapeHtml$1(review.id)}" aria-pressed="${review.pinned}" aria-label="${review.pinned ? "Unsave" : "Save"} this review">${review.pinned ? "★" : "☆"}</button>
|
|
4568
|
+
${renderCardMenu(group, followed, queued !== void 0, status)}
|
|
4448
4569
|
<a class="prb-card-link" href="${escapeHtml$1(href)}">
|
|
4449
4570
|
<div class="prb-card-head">
|
|
4450
4571
|
<h2>${escapeHtml$1(review.title)}</h2>
|
|
@@ -4487,15 +4608,18 @@ function renderCard(group, now, analysis) {
|
|
|
4487
4608
|
*
|
|
4488
4609
|
* The rows underneath carry the per-review menus, and they only exist once there is more than one.
|
|
4489
4610
|
*/
|
|
4490
|
-
function renderCardMenu(group, followed) {
|
|
4611
|
+
function renderCardMenu(group, followed, queued, status) {
|
|
4491
4612
|
const count = group.history.length;
|
|
4613
|
+
const mark = status === "complete" ? "in-progress" : "complete";
|
|
4492
4614
|
if (count < 2) return renderMenu({
|
|
4493
4615
|
id: group.latest.id,
|
|
4494
4616
|
scope: "review",
|
|
4495
4617
|
label: `Actions for ${followed}`,
|
|
4496
4618
|
action: "Delete review",
|
|
4497
4619
|
question: "Delete this review?",
|
|
4498
|
-
copy: "Copy review ID"
|
|
4620
|
+
copy: "Copy review ID",
|
|
4621
|
+
queue: queued ? "remove" : "add",
|
|
4622
|
+
mark
|
|
4499
4623
|
});
|
|
4500
4624
|
const all = `all ${count} reviews following ${followed}`;
|
|
4501
4625
|
return renderMenu({
|
|
@@ -4504,6 +4628,8 @@ function renderCardMenu(group, followed) {
|
|
|
4504
4628
|
label: `Actions for ${followed}`,
|
|
4505
4629
|
action: `Delete ${all}`,
|
|
4506
4630
|
question: `Delete ${all}?`,
|
|
4631
|
+
queue: queued ? "remove" : "add",
|
|
4632
|
+
mark,
|
|
4507
4633
|
copy: "Copy latest review ID"
|
|
4508
4634
|
});
|
|
4509
4635
|
}
|
|
@@ -4522,10 +4648,14 @@ function renderCardMenu(group, followed) {
|
|
|
4522
4648
|
*/
|
|
4523
4649
|
function renderMenu(item) {
|
|
4524
4650
|
const id = escapeHtml$1(item.id);
|
|
4651
|
+
const markItem = item.mark === void 0 ? "" : `<button type="button" class="prb-menu-item" data-mark-status="${item.mark}" data-mark-id="${id}">${markItemLabel(item.mark)}</button>`;
|
|
4652
|
+
const queueItem = item.queue === void 0 ? "" : `<button type="button" class="prb-menu-item" data-queue-action="${item.queue}" data-queue-id="${id}">${item.queue === "remove" ? "Remove from review queue" : "Add to review queue"}</button>`;
|
|
4525
4653
|
return `<div class="prb-menu" data-menu>
|
|
4526
4654
|
<button type="button" class="prb-menu-trigger" data-menu-trigger aria-expanded="false" aria-haspopup="true" aria-label="${escapeHtml$1(item.label)}">⋯</button>
|
|
4527
4655
|
<div class="prb-menu-panel" data-menu-panel hidden>
|
|
4528
4656
|
<div data-menu-items>
|
|
4657
|
+
${markItem}
|
|
4658
|
+
${queueItem}
|
|
4529
4659
|
<button type="button" class="prb-menu-item" data-copy-id="${id}">${escapeHtml$1(item.copy)}</button>
|
|
4530
4660
|
<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${id}" data-scope="${item.scope}">${escapeHtml$1(item.action)}</button>
|
|
4531
4661
|
</div>
|
|
@@ -4928,27 +5058,31 @@ function renderReviewsSection(settings) {
|
|
|
4928
5058
|
</section>`;
|
|
4929
5059
|
}
|
|
4930
5060
|
/**
|
|
4931
|
-
* The telemetry
|
|
5061
|
+
* The telemetry setting, visible.
|
|
4932
5062
|
*
|
|
4933
5063
|
* It has always been changeable, and only from the command line, which is a poor place for the one
|
|
4934
|
-
* setting a person is most likely to want to check rather than to set.
|
|
4935
|
-
*
|
|
4936
|
-
*
|
|
5064
|
+
* setting a person is most likely to want to check rather than to set.
|
|
5065
|
+
*
|
|
5066
|
+
* Two radios over three stored states. Until 0.30.0 the third state meant nothing was being sent,
|
|
5067
|
+
* so neither radio could be checked without answering on somebody's behalf. Now it means the
|
|
5068
|
+
* default is applying and the default is on, so Send it is checked and the help line says which of
|
|
5069
|
+
* the two reasons it is on for. The storage keeps all three, because a stored no has to outrank any
|
|
5070
|
+
* future change of default.
|
|
4937
5071
|
*/
|
|
4938
5072
|
function renderPrivacySection(settings) {
|
|
4939
|
-
const on = settings.usageCounts === true;
|
|
4940
5073
|
const off = settings.usageCounts === false;
|
|
4941
5074
|
return `<section class="prb-settings-section">
|
|
4942
5075
|
<h2>Privacy</h2>
|
|
4943
5076
|
<div class="prb-field prb-settings-field" role="group" aria-labelledby="prb-usage-label" aria-describedby="prb-usage-help">
|
|
4944
|
-
<span id="prb-usage-label">
|
|
5077
|
+
<span id="prb-usage-label">Non-identifying usage telemetry</span>
|
|
4945
5078
|
<div class="prb-settings-choice">
|
|
4946
|
-
<label><input type="radio" name="prb-usage" value="on" data-settings-usage${
|
|
4947
|
-
<label><input type="radio" name="prb-usage" value="off" data-settings-usage${off ? " checked" : ""}> Do not send
|
|
5079
|
+
<label><input type="radio" name="prb-usage" value="on" data-settings-usage${off ? "" : " checked"}> Send it</label>
|
|
5080
|
+
<label><input type="radio" name="prb-usage" value="off" data-settings-usage${off ? " checked" : ""}> Do not send it</label>
|
|
4948
5081
|
</div>
|
|
4949
5082
|
</div>
|
|
4950
|
-
<p class="prb-settings-help" id="prb-usage-help">
|
|
4951
|
-
|
|
5083
|
+
<p class="prb-settings-help" id="prb-usage-help">When a review starts and finishes, with the
|
|
5084
|
+
version, platform, agent, whether it succeeded and how long it took. Never your code, your diffs,
|
|
5085
|
+
your questions or the names of anything you review.${settings.usageCounts === null ? " It is on by default; you have not changed it." : ""}</p>
|
|
4952
5086
|
</section>`;
|
|
4953
5087
|
}
|
|
4954
5088
|
//#endregion
|
|
@@ -5075,6 +5209,7 @@ function reviewsRevision(live = analysisStates()) {
|
|
|
5075
5209
|
const state = live.get(id);
|
|
5076
5210
|
hash.update(`${id}@${state.purpose}:${state.phase}${state.stopped ? ":stopped" : ""}\n`);
|
|
5077
5211
|
}
|
|
5212
|
+
hash.update(`queue:${queueStamp()}\n`);
|
|
5078
5213
|
return hash.digest("base64url");
|
|
5079
5214
|
}
|
|
5080
5215
|
//#endregion
|
|
@@ -5903,6 +6038,7 @@ async function handle(req, res, options) {
|
|
|
5903
6038
|
if (url.pathname === "/open") return handleOpen(req, res, url.searchParams, options.indexToken);
|
|
5904
6039
|
if (url.pathname === "/settings") return void handleSettings(req, res, url.searchParams, options.indexToken);
|
|
5905
6040
|
if (url.pathname === "/pin") return handlePin(req, res, options.indexToken);
|
|
6041
|
+
if (url.pathname === "/queue") return handleQueue(req, res, options.indexToken);
|
|
5906
6042
|
if (url.pathname === "/delete") return handleDelete(req, res, options.indexToken);
|
|
5907
6043
|
if (url.pathname === "/status") return handleStatus(req, res, options.indexToken);
|
|
5908
6044
|
if (url.pathname === "/revision") return handleRevision(req, res, options.indexToken);
|
|
@@ -5928,6 +6064,7 @@ async function handle(req, res, options) {
|
|
|
5928
6064
|
if (action === "/stop") return handleStop(req, res, workspace);
|
|
5929
6065
|
if (action === "/kiss") return handleKiss(req, res, workspace);
|
|
5930
6066
|
if (action === "/state") return handleState(req, res, workspace);
|
|
6067
|
+
if (action === "/delete") return handleDeleteOwn(req, res, workspace);
|
|
5931
6068
|
if (action === "/event") return handleEvent(req, res, workspace);
|
|
5932
6069
|
if (action === "/prefs") return handlePrefs(req, res);
|
|
5933
6070
|
if (action === "/prompts") return handlePrompts(req, res);
|
|
@@ -5990,8 +6127,14 @@ function handleIndex(req, res, params, indexToken) {
|
|
|
5990
6127
|
const cookie = indexCookieHeader(indexToken);
|
|
5991
6128
|
if (onward) return redirect(res, onward, cookie);
|
|
5992
6129
|
const states = analysisStates();
|
|
6130
|
+
const queue = readQueue();
|
|
6131
|
+
const kept = new Set(queue);
|
|
6132
|
+
for (const review of recentWorkspaces(Number.MAX_SAFE_INTEGER)) if (review.pinned) kept.add(review.id);
|
|
6133
|
+
const groups = indexReviewGroups(MAX_LISTED_BRANCHES, MAX_KEPT_BRANCHES, kept);
|
|
6134
|
+
const rendered = new Set(groups.flatMap((group) => group.history.map((review) => review.id)));
|
|
5993
6135
|
sendHtml(res, 200, renderReviewsPage({
|
|
5994
|
-
groups
|
|
6136
|
+
groups,
|
|
6137
|
+
queue: queue.filter((id) => rendered.has(id)),
|
|
5995
6138
|
analyses: states,
|
|
5996
6139
|
revision: reviewsRevision(states)
|
|
5997
6140
|
}), cookie);
|
|
@@ -6144,6 +6287,7 @@ async function handleStatus(req, res, indexToken) {
|
|
|
6144
6287
|
workspace.completedAt = complete ? now : void 0;
|
|
6145
6288
|
if (!complete) workspace.lastViewedAt = workspace.lastViewedAt ?? now;
|
|
6146
6289
|
saveWorkspace(workspace);
|
|
6290
|
+
dequeueOnComplete(id, complete);
|
|
6147
6291
|
changed += 1;
|
|
6148
6292
|
}
|
|
6149
6293
|
send(res, 200, {
|
|
@@ -6197,6 +6341,32 @@ async function handlePin(req, res, indexToken) {
|
|
|
6197
6341
|
send(res, 200, { ok: true });
|
|
6198
6342
|
}
|
|
6199
6343
|
/**
|
|
6344
|
+
* Adding to and taking out of the working set.
|
|
6345
|
+
*
|
|
6346
|
+
* The index cookie for the same reason as `handlePin` above: this is the list acting on itself, and
|
|
6347
|
+
* a review's own token must not be able to rearrange the other reviews around it.
|
|
6348
|
+
*
|
|
6349
|
+
* The answer carries the queue **as stored**, which is not always the queue the page asked for --
|
|
6350
|
+
* the cap applies, duplicates collapse, and a review somebody deleted in another tab drops out on
|
|
6351
|
+
* the next write. The browser paints from this rather than from its own optimism, which is the one
|
|
6352
|
+
* thing that keeps a pill claiming "Queue 4" honest.
|
|
6353
|
+
*
|
|
6354
|
+
* An unrecognised action is a 400 rather than a no-op 200. A route that silently accepts a word it
|
|
6355
|
+
* does not implement is how a later `top` appears to work while doing nothing.
|
|
6356
|
+
*/
|
|
6357
|
+
async function handleQueue(req, res, indexToken) {
|
|
6358
|
+
if (!indexToken || !secretMatches(indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
|
|
6359
|
+
const body = await readJson(req);
|
|
6360
|
+
const id = typeof body?.id === "string" ? body.id : null;
|
|
6361
|
+
const action = body?.action;
|
|
6362
|
+
if (action !== "add" && action !== "remove") return send(res, 400, { error: "Unknown queue action." });
|
|
6363
|
+
if (!id || !loadWorkspace(id)) return send(res, 404, { error: "No such review" });
|
|
6364
|
+
const stored = action === "add" ? queueAdd(id) : queueRemove(id);
|
|
6365
|
+
if (stored === null) return send(res, 500, { error: "The queue could not be saved." });
|
|
6366
|
+
const live = stored.filter((queued) => loadWorkspace(queued) !== null);
|
|
6367
|
+
send(res, 200, { queue: live.length === stored.length ? stored : writeQueue(live) ?? stored });
|
|
6368
|
+
}
|
|
6369
|
+
/**
|
|
6200
6370
|
* Delete one review, or everything on one card.
|
|
6201
6371
|
*
|
|
6202
6372
|
* Authorised the same way pinning is, and for the same reason: this is the list acting on itself,
|
|
@@ -6224,7 +6394,35 @@ async function handleDelete(req, res, indexToken) {
|
|
|
6224
6394
|
const live = (wholeCard ? lineageIds(id) : [id]).flatMap((each) => liveJobsFor(each));
|
|
6225
6395
|
if (live.length > 0) return send(res, 409, { error: stillRunning(live.map((one) => one.job.target.branch)) });
|
|
6226
6396
|
const report = wholeCard ? await deleteLineage(id) : await deleteReview(id);
|
|
6227
|
-
if (wasBlocked(report)) return send(res, 409, { error:
|
|
6397
|
+
if (wasBlocked(report)) return send(res, 409, { error: blockedSentence(report) });
|
|
6398
|
+
send(res, 200, {
|
|
6399
|
+
ok: true,
|
|
6400
|
+
deleted: report.deleted.length,
|
|
6401
|
+
refusals: report.refusals
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
/** Why a deletion that was allowed to start still removed nothing. */
|
|
6405
|
+
function blockedSentence(report) {
|
|
6406
|
+
return report.live.length > 0 ? stillRunning(report.live.map((one) => one.job.target.branch)) : "This review is being worked on right now, so nothing was deleted. Try again in a moment.";
|
|
6407
|
+
}
|
|
6408
|
+
/**
|
|
6409
|
+
* Delete the review a workspace page is showing, from that page.
|
|
6410
|
+
*
|
|
6411
|
+
* The review's own token rather than the index credential. A page opened from a terminal link has
|
|
6412
|
+
* never been shown the list and so holds no index cookie, and this token already writes the review's
|
|
6413
|
+
* state. Deleting it is the same authority over the same one record, and it reaches no other review.
|
|
6414
|
+
*
|
|
6415
|
+
* Only this review, never its card. A page showing one analysis does not get to decide the fate of
|
|
6416
|
+
* the others in its lineage; the list is where a whole branch is deleted.
|
|
6417
|
+
*
|
|
6418
|
+
* POST only, because it is the one route under `/api/` that destroys something.
|
|
6419
|
+
*/
|
|
6420
|
+
async function handleDeleteOwn(req, res, workspace) {
|
|
6421
|
+
if (req.method !== "POST") return send(res, 405, { error: "Deleting a review needs a POST." });
|
|
6422
|
+
const live = liveJobsFor(workspace.id);
|
|
6423
|
+
if (live.length > 0) return send(res, 409, { error: stillRunning(live.map((one) => one.job.target.branch)) });
|
|
6424
|
+
const report = await deleteReview(workspace.id);
|
|
6425
|
+
if (wasBlocked(report)) return send(res, 409, { error: blockedSentence(report) });
|
|
6228
6426
|
send(res, 200, {
|
|
6229
6427
|
ok: true,
|
|
6230
6428
|
deleted: report.deleted.length,
|
|
@@ -6263,7 +6461,7 @@ async function deleteCards(ids) {
|
|
|
6263
6461
|
if (wasBlocked(report)) {
|
|
6264
6462
|
blocked.push({
|
|
6265
6463
|
id,
|
|
6266
|
-
error:
|
|
6464
|
+
error: blockedSentence(report)
|
|
6267
6465
|
});
|
|
6268
6466
|
continue;
|
|
6269
6467
|
}
|
|
@@ -6316,6 +6514,15 @@ function indexCookie(req) {
|
|
|
6316
6514
|
/** Branches, not reviews: a branch reviewed five times is one card and must not cost five slots. */
|
|
6317
6515
|
var MAX_LISTED_BRANCHES = 50;
|
|
6318
6516
|
/**
|
|
6517
|
+
* How many branches the reviewer's own working set may add on top of the recency window.
|
|
6518
|
+
*
|
|
6519
|
+
* Its own constant rather than a second use of the one above, because the two bound different
|
|
6520
|
+
* things and sharing a name would imply a ceiling the page does not have: what it lists is at most
|
|
6521
|
+
* these two added together. Saved and Queue exist to defeat recency, so they cannot be made to
|
|
6522
|
+
* compete for its slots -- saving eleven reviews must not push eleven recent ones off the page.
|
|
6523
|
+
*/
|
|
6524
|
+
var MAX_KEPT_BRANCHES = 50;
|
|
6525
|
+
/**
|
|
6319
6526
|
* A finished review's workspace was served.
|
|
6320
6527
|
*
|
|
6321
6528
|
* **This counts page serves, not people and not sessions.** A reload, a restored tab, the CLI
|
|
@@ -6904,6 +7111,7 @@ async function handleState(req, res, workspace) {
|
|
|
6904
7111
|
}
|
|
6905
7112
|
if (typeof body?.complete === "boolean") {
|
|
6906
7113
|
workspace.completedAt = body.complete ? Date.now() : void 0;
|
|
7114
|
+
dequeueOnComplete(workspace.id, body.complete);
|
|
6907
7115
|
changed = true;
|
|
6908
7116
|
}
|
|
6909
7117
|
if (changed) saveWorkspace(workspace);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://prreviewbuddy.com",
|