prreviewbuddy 0.27.2 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-DUCSC5Bc.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>
@@ -5075,6 +5205,7 @@ function reviewsRevision(live = analysisStates()) {
5075
5205
  const state = live.get(id);
5076
5206
  hash.update(`${id}@${state.purpose}:${state.phase}${state.stopped ? ":stopped" : ""}\n`);
5077
5207
  }
5208
+ hash.update(`queue:${queueStamp()}\n`);
5078
5209
  return hash.digest("base64url");
5079
5210
  }
5080
5211
  //#endregion
@@ -5903,6 +6034,7 @@ async function handle(req, res, options) {
5903
6034
  if (url.pathname === "/open") return handleOpen(req, res, url.searchParams, options.indexToken);
5904
6035
  if (url.pathname === "/settings") return void handleSettings(req, res, url.searchParams, options.indexToken);
5905
6036
  if (url.pathname === "/pin") return handlePin(req, res, options.indexToken);
6037
+ if (url.pathname === "/queue") return handleQueue(req, res, options.indexToken);
5906
6038
  if (url.pathname === "/delete") return handleDelete(req, res, options.indexToken);
5907
6039
  if (url.pathname === "/status") return handleStatus(req, res, options.indexToken);
5908
6040
  if (url.pathname === "/revision") return handleRevision(req, res, options.indexToken);
@@ -5928,6 +6060,7 @@ async function handle(req, res, options) {
5928
6060
  if (action === "/stop") return handleStop(req, res, workspace);
5929
6061
  if (action === "/kiss") return handleKiss(req, res, workspace);
5930
6062
  if (action === "/state") return handleState(req, res, workspace);
6063
+ if (action === "/delete") return handleDeleteOwn(req, res, workspace);
5931
6064
  if (action === "/event") return handleEvent(req, res, workspace);
5932
6065
  if (action === "/prefs") return handlePrefs(req, res);
5933
6066
  if (action === "/prompts") return handlePrompts(req, res);
@@ -5990,8 +6123,14 @@ function handleIndex(req, res, params, indexToken) {
5990
6123
  const cookie = indexCookieHeader(indexToken);
5991
6124
  if (onward) return redirect(res, onward, cookie);
5992
6125
  const states = analysisStates();
6126
+ const queue = readQueue();
6127
+ const kept = new Set(queue);
6128
+ for (const review of recentWorkspaces(Number.MAX_SAFE_INTEGER)) if (review.pinned) kept.add(review.id);
6129
+ const groups = indexReviewGroups(MAX_LISTED_BRANCHES, MAX_KEPT_BRANCHES, kept);
6130
+ const rendered = new Set(groups.flatMap((group) => group.history.map((review) => review.id)));
5993
6131
  sendHtml(res, 200, renderReviewsPage({
5994
- groups: recentReviewGroups(MAX_LISTED_BRANCHES),
6132
+ groups,
6133
+ queue: queue.filter((id) => rendered.has(id)),
5995
6134
  analyses: states,
5996
6135
  revision: reviewsRevision(states)
5997
6136
  }), cookie);
@@ -6144,6 +6283,7 @@ async function handleStatus(req, res, indexToken) {
6144
6283
  workspace.completedAt = complete ? now : void 0;
6145
6284
  if (!complete) workspace.lastViewedAt = workspace.lastViewedAt ?? now;
6146
6285
  saveWorkspace(workspace);
6286
+ dequeueOnComplete(id, complete);
6147
6287
  changed += 1;
6148
6288
  }
6149
6289
  send(res, 200, {
@@ -6197,6 +6337,32 @@ async function handlePin(req, res, indexToken) {
6197
6337
  send(res, 200, { ok: true });
6198
6338
  }
6199
6339
  /**
6340
+ * Adding to and taking out of the working set.
6341
+ *
6342
+ * The index cookie for the same reason as `handlePin` above: this is the list acting on itself, and
6343
+ * a review's own token must not be able to rearrange the other reviews around it.
6344
+ *
6345
+ * The answer carries the queue **as stored**, which is not always the queue the page asked for --
6346
+ * the cap applies, duplicates collapse, and a review somebody deleted in another tab drops out on
6347
+ * the next write. The browser paints from this rather than from its own optimism, which is the one
6348
+ * thing that keeps a pill claiming "Queue 4" honest.
6349
+ *
6350
+ * An unrecognised action is a 400 rather than a no-op 200. A route that silently accepts a word it
6351
+ * does not implement is how a later `top` appears to work while doing nothing.
6352
+ */
6353
+ async function handleQueue(req, res, indexToken) {
6354
+ if (!indexToken || !secretMatches(indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
6355
+ const body = await readJson(req);
6356
+ const id = typeof body?.id === "string" ? body.id : null;
6357
+ const action = body?.action;
6358
+ if (action !== "add" && action !== "remove") return send(res, 400, { error: "Unknown queue action." });
6359
+ if (!id || !loadWorkspace(id)) return send(res, 404, { error: "No such review" });
6360
+ const stored = action === "add" ? queueAdd(id) : queueRemove(id);
6361
+ if (stored === null) return send(res, 500, { error: "The queue could not be saved." });
6362
+ const live = stored.filter((queued) => loadWorkspace(queued) !== null);
6363
+ send(res, 200, { queue: live.length === stored.length ? stored : writeQueue(live) ?? stored });
6364
+ }
6365
+ /**
6200
6366
  * Delete one review, or everything on one card.
6201
6367
  *
6202
6368
  * Authorised the same way pinning is, and for the same reason: this is the list acting on itself,
@@ -6224,7 +6390,35 @@ async function handleDelete(req, res, indexToken) {
6224
6390
  const live = (wholeCard ? lineageIds(id) : [id]).flatMap((each) => liveJobsFor(each));
6225
6391
  if (live.length > 0) return send(res, 409, { error: stillRunning(live.map((one) => one.job.target.branch)) });
6226
6392
  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." });
6393
+ if (wasBlocked(report)) return send(res, 409, { error: blockedSentence(report) });
6394
+ send(res, 200, {
6395
+ ok: true,
6396
+ deleted: report.deleted.length,
6397
+ refusals: report.refusals
6398
+ });
6399
+ }
6400
+ /** Why a deletion that was allowed to start still removed nothing. */
6401
+ function blockedSentence(report) {
6402
+ 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.";
6403
+ }
6404
+ /**
6405
+ * Delete the review a workspace page is showing, from that page.
6406
+ *
6407
+ * The review's own token rather than the index credential. A page opened from a terminal link has
6408
+ * never been shown the list and so holds no index cookie, and this token already writes the review's
6409
+ * state. Deleting it is the same authority over the same one record, and it reaches no other review.
6410
+ *
6411
+ * Only this review, never its card. A page showing one analysis does not get to decide the fate of
6412
+ * the others in its lineage; the list is where a whole branch is deleted.
6413
+ *
6414
+ * POST only, because it is the one route under `/api/` that destroys something.
6415
+ */
6416
+ async function handleDeleteOwn(req, res, workspace) {
6417
+ if (req.method !== "POST") return send(res, 405, { error: "Deleting a review needs a POST." });
6418
+ const live = liveJobsFor(workspace.id);
6419
+ if (live.length > 0) return send(res, 409, { error: stillRunning(live.map((one) => one.job.target.branch)) });
6420
+ const report = await deleteReview(workspace.id);
6421
+ if (wasBlocked(report)) return send(res, 409, { error: blockedSentence(report) });
6228
6422
  send(res, 200, {
6229
6423
  ok: true,
6230
6424
  deleted: report.deleted.length,
@@ -6263,7 +6457,7 @@ async function deleteCards(ids) {
6263
6457
  if (wasBlocked(report)) {
6264
6458
  blocked.push({
6265
6459
  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."
6460
+ error: blockedSentence(report)
6267
6461
  });
6268
6462
  continue;
6269
6463
  }
@@ -6316,6 +6510,15 @@ function indexCookie(req) {
6316
6510
  /** Branches, not reviews: a branch reviewed five times is one card and must not cost five slots. */
6317
6511
  var MAX_LISTED_BRANCHES = 50;
6318
6512
  /**
6513
+ * How many branches the reviewer's own working set may add on top of the recency window.
6514
+ *
6515
+ * Its own constant rather than a second use of the one above, because the two bound different
6516
+ * things and sharing a name would imply a ceiling the page does not have: what it lists is at most
6517
+ * these two added together. Saved and Queue exist to defeat recency, so they cannot be made to
6518
+ * compete for its slots -- saving eleven reviews must not push eleven recent ones off the page.
6519
+ */
6520
+ var MAX_KEPT_BRANCHES = 50;
6521
+ /**
6319
6522
  * A finished review's workspace was served.
6320
6523
  *
6321
6524
  * **This counts page serves, not people and not sessions.** A reload, a restored tab, the CLI
@@ -6904,6 +7107,7 @@ async function handleState(req, res, workspace) {
6904
7107
  }
6905
7108
  if (typeof body?.complete === "boolean") {
6906
7109
  workspace.completedAt = body.complete ? Date.now() : void 0;
7110
+ dequeueOnComplete(workspace.id, body.complete);
6907
7111
  changed = true;
6908
7112
  }
6909
7113
  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.29.1",
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",
package/static/reviews.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./chunk-CIsM3PMS.js";import{n as t,t as n}from"./chunk-gBDI1-Cy.js";var r=[`all`,`in-progress`,`complete`];function i(e,t){let n=`${e} review${e===1?``:`s`}`;return e===t?n:`${n} · ${t} branch${t===1?``:`es`}`}function a(e,t){let n=Math.max(1,Math.ceil(e/15)),r=Math.min(Math.max(1,Math.floor(t)||1),n),i=(r-1)*15;return{page:r,pages:n,from:i,to:Math.min(i+15,e)}}function o(e){let t=new URLSearchParams(e.replace(/^#/,``)),n=t.get(`f`)??``,i=Number(t.get(`page`)??``);return{query:t.get(`q`)??``,filter:r.includes(n)?n:`all`,page:Number.isInteger(i)&&i>=1?i:1}}function s(e){let t=new URLSearchParams;e.query!==``&&t.set(`q`,e.query),e.filter!==`all`&&t.set(`f`,e.filter),e.page>1&&t.set(`page`,String(e.page));let n=t.toString();return n===``?``:`#${n}`}function c(e){return e===`complete`?`Reviewed`:e===`in-progress`?`In progress`:`New`}function l(e,t){if(e===0)return`Nothing selected`;let n=`${e} selected`;return t>0?`${n} · ${t} not shown`:n}function u(e){return e===0?`Delete`:`Delete ${e} review${e===1?``:`s`}`}function d(e){return`${u(e)}?`}function f(e,t,n){return e===``||t===``||t===e?`wait`:n?`hold`:`reload`}var p=[...document.querySelectorAll(`[data-card]`)],m=document.querySelector(`.prb-index`),h=document.querySelector(`[data-search]`),g=document.querySelector(`[data-no-matches]`),_=document.querySelector(`[data-count]`),v=document.querySelector(`[data-pager]`),y=document.querySelector(`[data-pages]`),b=document.querySelector(`[data-bulk]`),x=document.querySelector(`[data-bulk-count]`),S=document.querySelector(`[data-bulk-delete]`),C=document.querySelector(`[data-bulk-question]`),w=document.querySelector(`[data-bulk-confirm]`),T=document.querySelector(`[data-bulk-actions]`),E=document.querySelector(`[data-bulk-error]`),D=o(window.location.hash),O=new Set,k=!1;function A(){let e=D.query.trim().toLowerCase(),t=p.filter(t=>{let n=D.filter===`all`||t.dataset.status===D.filter,r=!e||(t.dataset.searchText??``).includes(e);return n&&r}),n=a(t.length,D.page);D.page=n.page;let r=new Set(t.slice(n.from,n.to));for(let e of p)e.hidden=!r.has(e);h&&h.value!==D.query&&(h.value=D.query);for(let e of document.querySelectorAll(`[data-filter]`))e.setAttribute(`aria-pressed`,String(e.dataset.filter===D.filter));_&&(_.textContent=i(t.reduce((e,t)=>e+Number(t.dataset.reviews??1),0),t.length)),g&&(g.hidden=t.length>0||p.length===0),j(n),B(t)}function j(e){if(!(!v||!y)&&(v.hidden=e.pages<2,y.textContent=``,!v.hidden)){for(let t=1;t<=e.pages;t+=1){let n=document.createElement(`button`);n.type=`button`,n.className=`prb-index-page`,n.dataset.page=String(t),n.textContent=String(t),n.setAttribute(`aria-label`,`Page ${t}`),t===e.page&&n.setAttribute(`aria-current`,`page`),y.append(n)}for(let t of v.querySelectorAll(`[data-page-step]`)){let n=e.page+Number(t.dataset.pageStep);t.disabled=n<1||n>e.pages}}}function M(e,t){D={...D,...e},A();let n=`${window.location.pathname}${window.location.search}${s(D)}`;t===`push`?window.history.pushState(null,``,n):window.history.replaceState(null,``,n)}function N(){document.documentElement.scrollTop=0,document.body.scrollTop=0}h?.addEventListener(`input`,()=>M({query:h.value,page:1},`replace`)),h?.addEventListener(`keydown`,e=>{e.key===`Escape`&&h.value!==``&&(e.preventDefault(),M({query:``,page:1},`replace`))});for(let e of document.querySelectorAll(`[data-filter]`))e.addEventListener(`click`,()=>{M({filter:e.dataset.filter??`all`,page:1},`push`)});v?.addEventListener(`click`,e=>{let t=e.target,n=t.closest(`[data-page]`),r=t.closest(`[data-page-step]`);!n&&!r||(M({page:n?Number(n.dataset.page):D.page+Number(r?.dataset.pageStep??0)},`push`),N())});for(let e of[`popstate`,`hashchange`])window.addEventListener(e,()=>{D=o(window.location.hash),A()});document.addEventListener(`click`,e=>{let t=e.target.closest(`[data-pin]`);if(!t?.dataset.pin)return;e.preventDefault(),e.stopPropagation();let n=t.getAttribute(`aria-pressed`)!==`true`;t.setAttribute(`aria-pressed`,String(n)),t.innerHTML=n?`&#9733;`:`&#9734;`,t.setAttribute(`aria-label`,`${n?`Unpin`:`Pin`} this review`),t.title=n?`Unpin`:`Pin to the top`;let r=t.closest(`[data-card]`);r&&(r.dataset.pinned=String(n)),fetch(`/pin`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.dataset.pin,pinned:n})}).catch(()=>{})}),document.addEventListener(`click`,e=>{let t=e.target.closest(`[data-history-toggle]`);if(!t)return;e.preventDefault(),e.stopPropagation();let n=t.parentElement?.querySelector(`[data-history-list]`);if(!n)return;let r=n.hidden;n.hidden=!r,t.setAttribute(`aria-expanded`,String(r))}),A();function P(e){for(let t of document.querySelectorAll(`[data-menu]`)){if(t===e)continue;let n=t.querySelector(`[data-menu-panel]`),r=t.querySelector(`[data-menu-trigger]`);n&&(n.hidden=!0),r?.setAttribute(`aria-expanded`,`false`),F(t)}}function F(e){let t=e.querySelector(`[data-menu-items]`),n=e.querySelector(`[data-menu-confirm]`);t&&(t.hidden=!1),n&&(n.hidden=!0),e.querySelector(`[data-menu-error]`)?.remove(),e.querySelector(`[data-id-field]`)?.remove()}document.addEventListener(`keydown`,e=>{e.key===`Escape`&&P()}),document.addEventListener(`click`,e=>{let n=e.target,r=n.closest(`[data-menu]`);if(!r)return void P();let i=n.closest(`[data-menu-trigger]`);if(i){e.preventDefault(),e.stopPropagation();let t=r.querySelector(`[data-menu-panel]`);if(!t)return;let n=t.hidden;P(r),F(r),t.hidden=!n,i.setAttribute(`aria-expanded`,String(n));return}let a=n.closest(`[data-copy-id]`);if(a?.dataset.copyId){e.preventDefault(),e.stopPropagation(),t(a,a.dataset.copyId);return}if(n.closest(`[data-delete]`)){e.preventDefault(),e.stopPropagation();let t=r.querySelector(`[data-menu-items]`),n=r.querySelector(`[data-menu-confirm]`);t&&(t.hidden=!0),n&&(n.hidden=!1);return}if(n.closest(`[data-cancel-delete]`)){e.preventDefault(),e.stopPropagation(),F(r);return}if(n.closest(`[data-confirm-delete]`)){e.preventDefault(),e.stopPropagation(),I(r);return}e.preventDefault(),e.stopPropagation()});async function I(e){let t=e.querySelector(`[data-delete]`),n=t?.dataset.delete;if(!n)return;let r=e.querySelector(`[data-menu-confirm]`),i=e.querySelector(`[data-confirm-delete]`);i&&(i.disabled=!0,i.textContent=`Deleting`);try{let r=await fetch(`/delete`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:n,scope:t?.dataset.scope??`review`})});if(r.ok)return void window.location.reload();L(e,(await r.json().catch(()=>null))?.error??`That could not be deleted.`)}catch{L(e,`The workspace server did not answer. It may have stopped.`)}i&&(i.disabled=!1,i.textContent=`Delete`),r&&(r.hidden=!0);let a=e.querySelector(`[data-menu-items]`);a&&(a.hidden=!1)}function L(e,t){e.querySelector(`[data-menu-error]`)?.remove();let n=document.createElement(`p`);n.className=`prb-menu-error`,n.setAttribute(`data-menu-error`,``),n.textContent=t,e.querySelector(`[data-menu-panel]`)?.append(n)}function R(e){k=e,e||O.clear(),m&&(e?m.dataset.selecting=``:delete m.dataset.selecting),document.querySelector(`[data-select-mode]`)?.setAttribute(`aria-pressed`,String(e));for(let t of document.querySelectorAll(`.prb-card-link`))e?t.setAttribute(`aria-disabled`,`true`):t.removeAttribute(`aria-disabled`);z(),A()}function z(){w&&(w.hidden=!0),T&&(T.hidden=!1),E&&(E.hidden=!0,E.textContent=``)}function B(e){for(let e of p){let t=O.has(e.dataset.id??``),n=e.querySelector(`[data-select]`);n&&(n.checked=t),t?e.dataset.selected=``:delete e.dataset.selected}if(!b||(b.hidden=!k,!k))return;let t=new Set(e.map(e=>e.dataset.id)),n=[...O].filter(e=>!t.has(e)).length;x&&(x.textContent=l(O.size,n)),S&&(S.textContent=u(U()));for(let e of b.querySelectorAll(`[data-bulk-mark], [data-bulk-delete]`))e.disabled=O.size===0}document.querySelector(`[data-select-mode]`)?.addEventListener(`click`,()=>{R(!k)});function V(e){O.has(e)?O.delete(e):O.add(e),z(),A()}document.addEventListener(`change`,e=>{let t=e.target.closest(`[data-select]`);t?.dataset.select&&V(t.dataset.select)}),document.addEventListener(`click`,e=>{if(!k)return;let t=e.target,n=t.closest(`[data-card]`);n?.dataset.id&&(t.closest(`[data-select], [data-pin], [data-menu], [data-history-toggle]`)||(e.preventDefault(),V(n.dataset.id)))}),document.addEventListener(`keydown`,e=>{if(!k||e.key!==`Enter`)return;let t=e.target.closest(`.prb-card-link`)?.closest(`[data-card]`);t?.dataset.id&&(e.preventDefault(),V(t.dataset.id))});function H(){return p.filter(e=>O.has(e.dataset.id??``))}function U(){return H().reduce((e,t)=>e+Number(t.dataset.reviews??1),0)}function W(e){E&&(E.textContent=e,E.hidden=!1)}function G(e){for(let t of b?.querySelectorAll(`button`)??[])t.disabled=e}async function K(e){let t=[...O];if(t.length===0)return;G(!0);try{if(!(await fetch(`/status`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ids:t,complete:e})})).ok)return G(!1),W(`That could not be saved. The workspace server refused it.`)}catch{return G(!1),W(`The workspace server did not answer. It may have stopped.`)}let n=e?`complete`:`in-progress`;for(let e of H()){e.dataset.status=n;let t=e.querySelector(`.prb-card-status`);t&&(t.dataset.status=n,t.textContent=c(n))}G(!1),R(!1)}async function q(){let e=[...O];if(e.length===0)return;G(!0);let t=null;try{let n=await fetch(`/delete`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ids:e})});if(t=n.ok?await n.json().catch(()=>null):null,!t)return G(!1),z(),W(`Nothing was deleted. The workspace server refused it.`)}catch{return G(!1),z(),W(`The workspace server did not answer. It may have stopped.`)}let n=t.blocked??[],r=new Set(n.map(e=>e.id));for(let e of H())r.has(e.dataset.id??``)||e.remove();p=p.filter(e=>e.isConnected),O.clear();for(let e of r)O.add(e);if(G(!1),z(),n.length===0)return R(!1);A(),W([...new Set(n.map(e=>e.error))].join(` `))}b?.addEventListener(`click`,e=>{let t=e.target,n=t.closest(`[data-bulk-mark]`);if(n)return void K(n.dataset.bulkMark===`complete`);if(t.closest(`[data-bulk-delete]`)){C&&(C.textContent=d(U())),T&&(T.hidden=!0),w&&(w.hidden=!1);return}if(t.closest(`[data-bulk-confirm-delete]`))return void q();if(t.closest(`[data-bulk-cancel-delete]`))return z();if(t.closest(`[data-bulk-cancel]`))return R(!1)}),e(window,()=>window.location.reload());var J=document.body.dataset.indexRevision??``,Y=document.body.dataset.live===`true`?Date.now():0,X=!1;function Z(){return O.size>0||document.activeElement===h}function Q(){X&&!Z()&&window.location.reload()}async function $(){try{let e=await fetch(`/revision`);if(e.ok){let t=await e.json();t.live&&Y===0&&(Y=Date.now()),t.live||(Y=0);let n=f(J,t.revision??``,Z());if(n===`reload`)return void window.location.reload();n===`hold`&&(X=!0)}}catch{}window.setTimeout(()=>void $(),n(Y===0?2**53-1:Date.now()-Y))}$(),h?.addEventListener(`blur`,Q),document.addEventListener(`click`,()=>window.setTimeout(Q,0));
1
+ import{t as e}from"./chunk-CIsM3PMS.js";import{n as t,t as n}from"./chunk-gBDI1-Cy.js";function r(e,t){t.forEach((t,n)=>{e.children[n]!==t&&e.insertBefore(t,e.children[n]??null)})}var i=[`all`,`queue`,`saved`,`in-progress`,`complete`];function a(e,t){let n=`${e} review${e===1?``:`s`}`;return e===t?n:`${n} · ${t} branch${t===1?``:`es`}`}function o(e,t){let n=Math.max(1,Math.ceil(e/15)),r=Math.min(Math.max(1,Math.floor(t)||1),n),i=(r-1)*15;return{page:r,pages:n,from:i,to:Math.min(i+15,e)}}function s(e){let t=new URLSearchParams(e.replace(/^#/,``)),n=t.get(`f`)??``,r=Number(t.get(`page`)??``);return{query:t.get(`q`)??``,filter:i.includes(n)?n:`all`,author:t.get(`by`)??``,page:Number.isInteger(r)&&r>=1?r:1}}function c(e){let t=new URLSearchParams;e.query!==``&&t.set(`q`,e.query),e.filter!==`all`&&t.set(`f`,e.filter),e.author!==``&&t.set(`by`,e.author),e.page>1&&t.set(`page`,String(e.page));let n=t.toString();return n===``?``:`#${n}`}function l(e,t){return t===`all`||t===`queue`?!0:t===`saved`?e.pinned===!0:e.status===t}function ee(e,t){return t===``||e===t}function u(e){return e===`complete`?`Mark reviewed`:`Mark in progress`}function te(e,t){return t===`queue`?e.filter(e=>e.queue!==void 0).sort((e,t)=>(e.queue??0)-(t.queue??0)):e}function d(e){return e===0?`Queue`:`Queue ${e}`}function ne(e){return e===`complete`?`Reviewed`:e===`in-progress`?`In progress`:`New`}function re(e,t){if(e===0)return`Nothing selected`;let n=`${e} selected`;return t>0?`${n} · ${t} not shown`:n}function f(e){return e===0?`Delete`:`Delete ${e} review${e===1?``:`s`}`}function ie(e){return`${f(e)}?`}function ae(e,t,n){return e===``||t===``||t===e?`wait`:n?`hold`:`reload`}var p=[...document.querySelectorAll(`[data-card]`)],m=document.querySelector(`[data-cards]`),h=document.querySelector(`.prb-index`);function g(e){return{id:e.dataset.id??``,queue:e.dataset.queue===void 0?void 0:Number(e.dataset.queue),status:e.dataset.status??`new`,pinned:e.dataset.pinned===`true`}}var _=document.querySelector(`[data-search]`),v=document.querySelector(`[data-author-filter]`),y=document.querySelector(`[data-no-matches]`),b=document.querySelector(`[data-count]`),x=document.querySelector(`[data-pager]`),S=document.querySelector(`[data-pages]`),C=document.querySelector(`[data-bulk]`),w=document.querySelector(`[data-bulk-count]`),T=document.querySelector(`[data-bulk-delete]`),E=document.querySelector(`[data-bulk-question]`),D=document.querySelector(`[data-bulk-confirm]`),O=document.querySelector(`[data-bulk-actions]`),k=document.querySelector(`[data-bulk-error]`),A=s(window.location.hash),j=new Set,M=!1;function N(){let e=A.query.trim().toLowerCase();A.author!==``&&![...v?.options??[]].some(e=>e.value===A.author)&&(A.author=``);let t=te(p.filter(t=>(!e||(t.dataset.searchText??``).includes(e))&&l(g(t),A.filter)&&ee(t.dataset.author,A.author)).map(e=>({...g(e),card:e})),A.filter).map(e=>e.card),n=o(t.length,A.page);A.page=n.page;let i=t.slice(n.from,n.to),s=new Set(i);for(let e of p)e.hidden=!s.has(e);m&&r(m,i),_&&_.value!==A.query&&(_.value=A.query),v&&(v.value=A.author,A.author===``?delete v.dataset.active:v.dataset.active=``);for(let e of document.querySelectorAll(`[data-filter]`))e.setAttribute(`aria-pressed`,String(e.dataset.filter===A.filter));b&&(b.textContent=a(t.reduce((e,t)=>e+Number(t.dataset.reviews??1),0),t.length)),y&&(y.hidden=t.length>0||p.length===0),P(n),V(t)}function P(e){if(!(!x||!S)&&(x.hidden=e.pages<2,S.textContent=``,!x.hidden)){for(let t=1;t<=e.pages;t+=1){let n=document.createElement(`button`);n.type=`button`,n.className=`prb-index-page`,n.dataset.page=String(t),n.textContent=String(t),n.setAttribute(`aria-label`,`Page ${t}`),t===e.page&&n.setAttribute(`aria-current`,`page`),S.append(n)}for(let t of x.querySelectorAll(`[data-page-step]`)){let n=e.page+Number(t.dataset.pageStep);t.disabled=n<1||n>e.pages}}}function F(e,t){A={...A,...e},N();let n=`${window.location.pathname}${window.location.search}${c(A)}`;t===`push`?window.history.pushState(null,``,n):window.history.replaceState(null,``,n)}function oe(){document.documentElement.scrollTop=0,document.body.scrollTop=0}_?.addEventListener(`input`,()=>F({query:_.value,page:1},`replace`)),_?.addEventListener(`keydown`,e=>{e.key===`Escape`&&_.value!==``&&(e.preventDefault(),F({query:``,page:1},`replace`))});for(let e of document.querySelectorAll(`[data-filter]`))e.addEventListener(`click`,()=>{F({filter:e.dataset.filter??`all`,page:1},`push`)});v?.addEventListener(`change`,()=>{F({author:v.value,page:1},`push`)}),x?.addEventListener(`click`,e=>{let t=e.target,n=t.closest(`[data-page]`),r=t.closest(`[data-page-step]`);!n&&!r||(F({page:n?Number(n.dataset.page):A.page+Number(r?.dataset.pageStep??0)},`push`),oe())});for(let e of[`popstate`,`hashchange`])window.addEventListener(e,()=>{A=s(window.location.hash),N()});document.addEventListener(`click`,e=>{let t=e.target.closest(`[data-pin]`);if(!t?.dataset.pin)return;e.preventDefault(),e.stopPropagation();let n=t.getAttribute(`aria-pressed`)!==`true`;t.setAttribute(`aria-pressed`,String(n)),t.innerHTML=n?`&#9733;`:`&#9734;`,t.setAttribute(`aria-label`,`${n?`Unsave`:`Save`} this review`);let r=t.closest(`[data-card]`);r&&(r.dataset.pinned=String(n)),fetch(`/pin`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.dataset.pin,pinned:n})}).catch(()=>{})});async function se(e,t,n){let r;try{let e=await fetch(`/queue`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,action:n})});if(!e.ok)return;r=(await e.json()).queue}catch{return}let i=new Map(r.map((e,t)=>[e,t]));for(let e of p){let t=i.get(e.dataset.id??``);t===void 0?delete e.dataset.queue:e.dataset.queue=String(t)}let a=i.has(t);e.dataset.queueAction=a?`remove`:`add`,e.textContent=a?`Remove from review queue`:`Add to review queue`;let o=document.querySelector(`[data-queue-pill]`);o&&(o.textContent=d(r.length)),N()}document.addEventListener(`click`,e=>{let t=e.target.closest(`[data-history-toggle]`);if(!t)return;e.preventDefault(),e.stopPropagation();let n=t.parentElement?.querySelector(`[data-history-list]`);if(!n)return;let r=n.hidden;n.hidden=!r,t.setAttribute(`aria-expanded`,String(r))}),N();function I(e){for(let t of document.querySelectorAll(`[data-menu]`)){if(t===e)continue;let n=t.querySelector(`[data-menu-panel]`),r=t.querySelector(`[data-menu-trigger]`);n&&(n.hidden=!0),r?.setAttribute(`aria-expanded`,`false`),L(t)}}function L(e){let t=e.querySelector(`[data-menu-items]`),n=e.querySelector(`[data-menu-confirm]`);t&&(t.hidden=!1),n&&(n.hidden=!0),e.querySelector(`[data-menu-error]`)?.remove(),e.querySelector(`[data-id-field]`)?.remove()}document.addEventListener(`keydown`,e=>{e.key===`Escape`&&I()}),document.addEventListener(`click`,e=>{let n=e.target,r=n.closest(`[data-menu]`);if(!r)return void I();let i=n.closest(`[data-menu-trigger]`);if(i){e.preventDefault(),e.stopPropagation();let t=r.querySelector(`[data-menu-panel]`);if(!t)return;let n=t.hidden;I(r),L(r),t.hidden=!n,i.setAttribute(`aria-expanded`,String(n));return}let a=n.closest(`[data-copy-id]`);if(a?.dataset.copyId){e.preventDefault(),e.stopPropagation(),t(a,a.dataset.copyId);return}let o=n.closest(`[data-mark-status]`);if(o?.dataset.markId){e.preventDefault(),e.stopPropagation(),I(),ue(o.dataset.markId,o.dataset.markStatus===`complete`);return}let s=n.closest(`[data-queue-action]`);if(s?.dataset.queueId){e.preventDefault(),e.stopPropagation(),I(),se(s,s.dataset.queueId,s.dataset.queueAction===`remove`?`remove`:`add`);return}if(n.closest(`[data-delete]`)){e.preventDefault(),e.stopPropagation();let t=r.querySelector(`[data-menu-items]`),n=r.querySelector(`[data-menu-confirm]`);t&&(t.hidden=!0),n&&(n.hidden=!1);return}if(n.closest(`[data-cancel-delete]`)){e.preventDefault(),e.stopPropagation(),L(r);return}if(n.closest(`[data-confirm-delete]`)){e.preventDefault(),e.stopPropagation(),ce(r);return}e.preventDefault(),e.stopPropagation()});async function ce(e){let t=e.querySelector(`[data-delete]`),n=t?.dataset.delete;if(!n)return;let r=e.querySelector(`[data-menu-confirm]`),i=e.querySelector(`[data-confirm-delete]`);i&&(i.disabled=!0,i.textContent=`Deleting`);try{let r=await fetch(`/delete`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:n,scope:t?.dataset.scope??`review`})});if(r.ok)return void window.location.reload();R(e,(await r.json().catch(()=>null))?.error??`That could not be deleted.`)}catch{R(e,`The workspace server did not answer. It may have stopped.`)}i&&(i.disabled=!1,i.textContent=`Delete`),r&&(r.hidden=!0);let a=e.querySelector(`[data-menu-items]`);a&&(a.hidden=!1)}function R(e,t){e.querySelector(`[data-menu-error]`)?.remove();let n=document.createElement(`p`);n.className=`prb-menu-error`,n.setAttribute(`data-menu-error`,``),n.textContent=t,e.querySelector(`[data-menu-panel]`)?.append(n)}function z(e){M=e,e||j.clear(),h&&(e?h.dataset.selecting=``:delete h.dataset.selecting),document.querySelector(`[data-select-mode]`)?.setAttribute(`aria-pressed`,String(e));for(let t of document.querySelectorAll(`.prb-card-link`))e?t.setAttribute(`aria-disabled`,`true`):t.removeAttribute(`aria-disabled`);B(),N()}function B(){D&&(D.hidden=!0),O&&(O.hidden=!1),k&&(k.hidden=!0,k.textContent=``)}function V(e){for(let e of p){let t=j.has(e.dataset.id??``),n=e.querySelector(`[data-select]`);n&&(n.checked=t),t?e.dataset.selected=``:delete e.dataset.selected}if(!C||(C.hidden=!M,!M))return;let t=new Set(e.map(e=>e.dataset.id)),n=[...j].filter(e=>!t.has(e)).length;w&&(w.textContent=re(j.size,n)),T&&(T.textContent=f(W()));for(let e of C.querySelectorAll(`[data-bulk-mark], [data-bulk-delete]`))e.disabled=j.size===0}document.querySelector(`[data-select-mode]`)?.addEventListener(`click`,()=>{z(!M)});function H(e){j.has(e)?j.delete(e):j.add(e),B(),N()}document.addEventListener(`change`,e=>{let t=e.target.closest(`[data-select]`);t?.dataset.select&&H(t.dataset.select)}),document.addEventListener(`click`,e=>{if(!M)return;let t=e.target,n=t.closest(`[data-card]`);n?.dataset.id&&(t.closest(`[data-select], [data-pin], [data-menu], [data-history-toggle]`)||(e.preventDefault(),H(n.dataset.id)))}),document.addEventListener(`keydown`,e=>{if(!M||e.key!==`Enter`)return;let t=e.target.closest(`.prb-card-link`)?.closest(`[data-card]`);t?.dataset.id&&(e.preventDefault(),H(t.dataset.id))});function U(){return p.filter(e=>j.has(e.dataset.id??``))}function W(){return U().reduce((e,t)=>e+Number(t.dataset.reviews??1),0)}function G(e){k&&(k.textContent=e,k.hidden=!1)}function K(e){for(let t of C?.querySelectorAll(`button`)??[])t.disabled=e}async function le(e){let t=[...j];if(t.length!==0){K(!0);try{if(!(await fetch(`/status`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ids:t,complete:e})})).ok)return K(!1),G(`That could not be saved. The workspace server refused it.`)}catch{return K(!1),G(`The workspace server did not answer. It may have stopped.`)}q(U(),e),K(!1),z(!1)}}function q(e,t){let n=t?`complete`:`in-progress`,r=t?`in-progress`:`complete`;for(let t of e){t.dataset.status=n;let e=t.querySelector(`.prb-card-status`);e&&(e.dataset.status=n,e.textContent=ne(n));let i=t.querySelector(`[data-mark-status]`);i&&(i.dataset.markStatus=r,i.textContent=u(r))}}async function ue(e,t){try{if(!(await fetch(`/status`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ids:[e],complete:t})})).ok)return}catch{return}q(p.filter(t=>t.dataset.id===e),t),N()}async function J(){let e=[...j];if(e.length===0)return;K(!0);let t=null;try{let n=await fetch(`/delete`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ids:e})});if(t=n.ok?await n.json().catch(()=>null):null,!t)return K(!1),B(),G(`Nothing was deleted. The workspace server refused it.`)}catch{return K(!1),B(),G(`The workspace server did not answer. It may have stopped.`)}let n=t.blocked??[],r=new Set(n.map(e=>e.id));for(let e of U())r.has(e.dataset.id??``)||e.remove();p=p.filter(e=>e.isConnected),j.clear();for(let e of r)j.add(e);if(K(!1),B(),n.length===0)return z(!1);N(),G([...new Set(n.map(e=>e.error))].join(` `))}C?.addEventListener(`click`,e=>{let t=e.target,n=t.closest(`[data-bulk-mark]`);if(n)return void le(n.dataset.bulkMark===`complete`);if(t.closest(`[data-bulk-delete]`)){E&&(E.textContent=ie(W())),O&&(O.hidden=!0),D&&(D.hidden=!1);return}if(t.closest(`[data-bulk-confirm-delete]`))return void J();if(t.closest(`[data-bulk-cancel-delete]`))return B();if(t.closest(`[data-bulk-cancel]`))return z(!1)}),e(window,()=>window.location.reload());var de=document.body.dataset.indexRevision??``,Y=document.body.dataset.live===`true`?Date.now():0,X=!1;function Z(){return j.size>0||document.activeElement===_}function Q(){X&&!Z()&&window.location.reload()}async function $(){try{let e=await fetch(`/revision`);if(e.ok){let t=await e.json();t.live&&Y===0&&(Y=Date.now()),t.live||(Y=0);let n=ae(de,t.revision??``,Z());if(n===`reload`)return void window.location.reload();n===`hold`&&(X=!0)}}catch{}window.setTimeout(()=>void $(),n(Y===0?2**53-1:Date.now()-Y))}$(),_?.addEventListener(`blur`,Q),document.addEventListener(`click`,()=>window.setTimeout(Q,0));