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/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as fillFileUrlTemplate, $t as agentFor, A as removeWorktree, At as workspaceRevision, C as recordTelemetryUploadConsent, Ct as recentReviewGroups, D as ReviewBeingDeletedError, Dt as saveWorkspace, E as EXPLAIN_SIMPLY_PROMPT, F as clearServerRecord, Ft as issuesOutstanding, G as DEFAULT_WORKSPACE_PORT, Gt as clearAgentPreference, H as BUILD_VERSION, Ht as git, It as questionsOutstanding, J as parseWorkspacePort, K as MAX_WORKSPACE_PORT, Kt as readAgentPreference, L as readServerRecord, Lt as reviewerDispositions, M as relativeTime$1, Mt as doneVerb, N as readIndexToken, Nt as isIssueOutstanding, O as MANAGED_ROOT, Pt as isQuestionOutstanding, Rt as processDiscussion, S as record, St as previousKissResult, T as withUsageRecorded, Tt as reviewedCommit, V as writeServerRecord, W as feedbackUrl, Wt as resolveModel, X as statedWorkspacePort, Xt as AGENT_IDS, Y as resolveWorkspacePort, Yt as STORE_ROOT, Z as writePortPreference, _ as startKissJob, a as wasBlocked, at as isTerminal, c as updateReview, d as askCheckout, dt as purposeOf, et as checkCodeFreshness, f as UpdateAlreadyRunningError, gt as latestKissRun, 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 AgentUnavailableError, nt as allJobs, o as discardJob, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, qt as writeAgentPreference, r as lineageIds, rt as endedAt, s as RefreshUnavailableError, st as saveJob, t as deleteLineage, tn as AgentCancelledError, tt as checkFreshness, u as isUnchanged, ut as progressSteps, vt as lineagePosition, w as telemetryUploadConsent, x as readEvents, y as runJob, yt as loadWorkspace, zt as describeAuthorship } from "./delete_review-B3xciJ03.js";
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, { diagrams: true })}</section>
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. Copy alone and no delete: removing the review you are
3671
- * currently reading is a different act needing somewhere to send you afterwards, and the list is
3672
- * one click away in the rail.
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">&#8943;</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="${escapeHtml$1(id)}">Copy review ID</button>
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
- <span class="prb-rail-order">&lsaquo;</span>
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
- <span class="prb-rail-order">&#9881;</span>
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
- <span class="prb-rail-order"></span>
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
- <span class="prb-rail-order"></span>
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
- <span class="prb-rail-order"></span>
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
- <span class="prb-rail-order"></span>
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 ? "Unpin" : "Pin"} this review" title="${review.pinned ? "Unpin" : "Pin to the top"}">${review.pinned ? "&#9733;" : "&#9734;"}</button>
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 ? "&#9733;" : "&#9734;"}</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)}">&#8943;</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 answer, visible.
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. Three states rather than
4935
- * two: never having answered is not the same as having said no, and a page that showed it as "off"
4936
- * would be answering a question on somebody's behalf.
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">Anonymous usage counts</span>
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${on ? " checked" : ""}> Send them</label>
4947
- <label><input type="radio" name="prb-usage" value="off" data-settings-usage${off ? " checked" : ""}> Do not send them</label>
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">Counts only, never your code, your diffs, your
4951
- questions or the names of anything you review.${settings.usageCounts === null ? " You have not answered this yet, so nothing is being sent." : ""}</p>
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: recentReviewGroups(MAX_LISTED_BRANCHES),
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: 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." });
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: 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."
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.27.2",
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",