prreviewbuddy 0.48.0 → 0.50.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 { $t as mutateWorkspace, A as createPrState, An as describeAuthorship, At as allJobs, B as toneOf, Bt as purposeOf, C as readEvents, Cn as doneVerb, Ct as forgeFor, D as announceJob, Dn as questionsOutstanding, Dt as checkReview, E as telemetryUploadConsent, En as issuesOutstanding, Et as checkCodeFreshness, F as markAllRead, Ft as saveJob, G as extractJsonText, Gn as agentFor, Gt as groupByLineage, H as withUsageRecorded, Hn as AGENT_IDS, I as markRead, In as resolveModel, J as readMarker, Jn as AgentUnavailableError, Jt as latestKissRun, K as ReviewBeingDeletedError, Kt as indexKeptIds, L as pruneNotifications, Ln as clearAgentPreference, Lt as isLiveJob, M as eventName, Mt as fail, N as headline, Nn as isCommitReview, Nt as isTerminal, O as branchOf, On as reviewerDispositions, Ot as followedBranch, P as listNotifications, Pn as git, Pt as loadJob, Q as compactQueue, R as readNotification, Rn as readAgentPreference, Rt as phasesFor, Sn as readQueue, T as recordTelemetryUploadConsent, Tn as isQuestionOutstanding, Tt as fillFileUrlTemplate, U as EXPLAIN_SIMPLY_PROMPT, Ut as isClaimed, V as unreadCount$1, Vn as STORE_ROOT, W as asString, Wt as followedRefName, Xt as lineagePosition, Y as removeWorktree, Z as relativeTime$1, Zt as loadWorkspace, _ as agentEnvOf, _n as dequeueOnComplete, _t as parseWorkspacePort, a as wasBlocked, bn as queueRemove, bt as writePortPreference, c as RefreshUnavailableError, cn as workspaceStamps, d as refreshPrContext, dn as isLifecycle, dt as BUILD_VERSION, et as readIndexToken, f as isUnchanged, fn as lifecycleAction, gn as sectionOf, gt as MIN_WORKSPACE_PORT, h as liveUpdateFor, hn as parseLifecycle, ht as MAX_WORKSPACE_PORT, i as liveJobsFor, it as readServerRecord, j as createPrUpdate, jt as endedAt, k as subjectOf, kn as processDiscussion, kt as reviewedShaOf, l as mergeUpdateInto, ln as LIFECYCLES, m as UpdateAlreadyRunningError, mn as offersLifecycle, mt as DEFAULT_WORKSPACE_PORT, n as deleteReview, nn as recentWorkspaces, nt as clearServerRecord, o as discardJob, on as touchWorkspace, ot as serverEntryExists, p as askCheckout, pn as lifecycleChip, pt as feedbackUrl, q as MANAGED_ROOT, qn as AgentCancelledError, qt as indexReviewGroups, r as lineageIds, rn as reviewedCommit, s as FETCH_TIMEOUT_MS, sn as workspaceRevision, st as spawnServer, t as deleteLineage, tn as previousKissResult, u as updateReview, un as activityLabel, ut as writeServerRecord, vn as queueAdd, vt as resolveWorkspacePort, w as record, wn as isIssueOutstanding, wt as remoteHost, x as runJob, xn as queueStamp, xt as claimRequestUrl, y as startKissJob, yn as queueAddMany, yt as statedWorkspacePort, z as removeNotification, zn as writeAgentPreference, zt as progressSteps } from "./delete_review-D52wkt66.js";
1
+ import { $ as compactQueue, A as createPrState, An as processDiscussion, At as reviewedShaOf, B as toneOf, Bn as writeAgentPreference, Bt as progressSteps, C as readEvents, Cn as readQueue, D as announceJob, Dn as issuesOutstanding, Dt as checkCodeFreshness, E as telemetryUploadConsent, En as isQuestionOutstanding, Et as fillFileUrlTemplate, F as markAllRead, Fn as git, Ft as loadJob, G as extractJsonText, Gt as followedRefName, H as withUsageRecorded, Hn as STORE_ROOT, I as markRead, It as saveJob, J as readMarker, Jn as AgentCancelledError, Jt as indexReviewGroups, K as ReviewBeingDeletedError, Kn as agentFor, Kt as groupByLineage, L as pruneNotifications, Ln as resolveModel, M as eventName, Mt as endedAt, N as headline, Nt as fail, O as branchOf, On as questionsOutstanding, Ot as checkReview, P as listNotifications, Pn as isCommitReview, Pt as isTerminal, Q as activateQueued, Qt as loadWorkspace, R as readNotification, Rn as clearAgentPreference, Rt as isLiveJob, Sn as queueStamp, St as claimRequestUrl, T as recordTelemetryUploadConsent, Tn as isIssueOutstanding, Tt as remoteHost, U as EXPLAIN_SIMPLY_PROMPT, Un as AGENT_IDS, V as unreadCount$1, Vt as purposeOf, W as asString, Wt as isClaimed, Y as removeWorktree, Yn as AgentUnavailableError, Yt as latestKissRun, Z as relativeTime$1, Zt as lineagePosition, _ as agentEnvOf, _n as parseLifecycle, _t as MIN_WORKSPACE_PORT, a as wasBlocked, at as readServerRecord, bn as queueAddMany, bt as statedWorkspacePort, c as RefreshUnavailableError, cn as workspaceRevision, ct as spawnServer, d as refreshPrContext, dn as activityLabel, dt as writeServerRecord, en as mutateWorkspace, f as isUnchanged, fn as isLifecycle, ft as BUILD_VERSION, gn as offersLifecycle, gt as MAX_WORKSPACE_PORT, h as liveUpdateFor, hn as lifecyclePill, ht as DEFAULT_WORKSPACE_PORT, i as liveJobsFor, in as reviewedCommit, j as createPrUpdate, jn as describeAuthorship, jt as allJobs, k as subjectOf, kn as reviewerDispositions, kt as followedBranch, l as mergeUpdateInto, ln as workspaceStamps, m as UpdateAlreadyRunningError, mn as lifecycleChip, mt as feedbackUrl, n as deleteReview, nn as previousKissResult, o as discardJob, p as askCheckout, pn as lifecycleAction, q as MANAGED_ROOT, qt as indexKeptIds, r as lineageIds, rn as recentWorkspaces, rt as clearServerRecord, s as FETCH_TIMEOUT_MS, sn as touchWorkspace, st as serverEntryExists, t as deleteLineage, tt as readIndexToken, u as updateReview, un as LIFECYCLES, vn as sectionOf, vt as parseWorkspacePort, w as record, wn as doneVerb, wt as forgeFor, x as runJob, xn as queueRemove, xt as writePortPreference, y as startKissJob, yn as queueAdd, yt as resolveWorkspacePort, z as removeNotification, zn as readAgentPreference, zt as phasesFor } from "./delete_review-BIXqUnpx.js";
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { createHash, timingSafeEqual } from "node:crypto";
@@ -253,6 +253,14 @@ function saveItemLabel(saved) {
253
253
  return saved ? "Remove from saved" : "Save review";
254
254
  }
255
255
  /**
256
+ * The queue item, on a card and on the review's own page. "review queue" rather than "queue",
257
+ * because a menu is where somebody meets the idea for the first time. Here because the browser
258
+ * relabels it after a change, on both pages.
259
+ */
260
+ function queueItemLabel(queued) {
261
+ return queued ? "Remove from review queue" : "Add to review queue";
262
+ }
263
+ /**
256
264
  * What `Continue reviewing` opens, and whether it exists at all.
257
265
  *
258
266
  * One rule rather than an expression in the template, because "is there anything to continue" and
@@ -363,6 +371,7 @@ var PATHS = {
363
371
  reviews: "<path d=\"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\"/>",
364
372
  notifications: "<path d=\"M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.7 21a2 2 0 0 1-3.4 0\"/>",
365
373
  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\"/>",
374
+ help: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>",
366
375
  feedback: "<path d=\"m22 2-7 20-4-9-9-4z\"/><path d=\"M22 2 11 13\"/>",
367
376
  stop: "<path d=\"M12 2v10\"/><path d=\"M18.4 6.6a9 9 0 1 1-12.8 0\"/>",
368
377
  restart: "<path d=\"M21 12a9 9 0 1 1-2.64-6.36\"/><path d=\"M21 3v6h-6\"/>"
@@ -436,6 +445,12 @@ var WORKSPACE_PLACES = [
436
445
  href: "/settings",
437
446
  icon: "settings",
438
447
  label: "Settings"
448
+ },
449
+ {
450
+ place: "help",
451
+ href: "/help",
452
+ icon: "help",
453
+ label: "Help"
439
454
  }
440
455
  ];
441
456
  /**
@@ -4698,9 +4713,30 @@ function renderReviewBody(view, sections) {
4698
4713
  ${updating(view) ? renderUpdatingDoc(view) : renderReviewDoc(view, sections)}
4699
4714
  </main>
4700
4715
  ${result ? renderAsk(view, sections, updating(view)) : ""}
4716
+ ${result ? renderAskTab() : ""}
4701
4717
  </div>`;
4702
4718
  }
4703
4719
  /**
4720
+ * Buddy AI's own way in: a tab on the window's right edge, where its pane opens.
4721
+ *
4722
+ * Out of the header, which says where the review stands; the assistant is a tool beside the review,
4723
+ * and the edge it lives on is the edge this sits on. Hidden by the stylesheet whenever the pane is
4724
+ * showing, where the chevron in the pane's own header closes it again. The pane's left border is
4725
+ * not free for it: the resize handle runs its full height.
4726
+ *
4727
+ * Horizontal, and only as wide as its words: the product reads left to right everywhere else, and
4728
+ * a vertical label was a pattern it would have used exactly once.
4729
+ *
4730
+ * Only with a standard review, like the pane. A control that opens nothing is worse than none.
4731
+ */
4732
+ function renderAskTab() {
4733
+ return `<button type="button" class="prb-ask-tab" data-ask-toggle aria-controls="prb-ask" aria-expanded="false">${ASK_TAB_ICON}<span class="prb-ask-tab-label">Buddy AI</span></button>`;
4734
+ }
4735
+ /** Four-pointed, stroked like the rail's icons, so it reads as one of the product's own marks. */
4736
+ var ASK_TAB_ICON = "<svg class=\"prb-ask-tab-icon\" viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" focusable=\"false\" aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linejoin=\"round\"><path d=\"M12 3l1.8 5.6a2 2 0 0 0 1.3 1.3L21 12l-5.9 2.1a2 2 0 0 0-1.3 1.3L12 21l-1.8-5.6a2 2 0 0 0-1.3-1.3L3 12l5.9-2.1a2 2 0 0 0 1.3-1.3z\"/></svg>";
4737
+ /** Two chevrons pointing where the pane goes. */
4738
+ var ASK_HIDE_ICON = "<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" focusable=\"false\" aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 17 5-5-5-5M13 17l5-5-5-5\"/></svg>";
4739
+ /**
4704
4740
  * The document while an update runs: its progress and nothing else.
4705
4741
  *
4706
4742
  * Deliberately not a `.prb-view`. Views are what the rail and the hash switch between, and a page
@@ -4982,6 +5018,43 @@ function renderSubject(changeSet) {
4982
5018
  <span>${against}</span>`;
4983
5019
  }
4984
5020
  /**
5021
+ * Which commits this review is of, opened from the bar beside what it compares.
5022
+ *
5023
+ * Beside `.prb-change-ref` rather than inside it, because that element clips to keep the bar one
5024
+ * line and a panel inside it would be cut off. The pill's own label is a fact (how many commits),
5025
+ * so the bar gains information even closed. Facts only, and about this review: the general
5026
+ * explanation is the Help page's, linked when there is a server to answer the link.
5027
+ *
5028
+ * The count comes from the stored commit list, or from the authors' counts for a review made before
5029
+ * the list was recorded, which then says how many without listing them. Neither: no count at all,
5030
+ * never a zero.
5031
+ */
5032
+ function renderRange(changeSet, live) {
5033
+ const commits = changeSet.commits;
5034
+ const count = commits?.length ?? changeSet.authors?.reduce((sum, author) => sum + author.commits, 0);
5035
+ const counted = count !== void 0 && count > 0 ? count : void 0;
5036
+ const plural = (n) => `${n} commit${n === 1 ? "" : "s"}`;
5037
+ const sha = (value) => `<code>${escapeHtml$1(value.slice(0, 7))}</code>`;
5038
+ const lines = [];
5039
+ if (changeSet.targetType === "commit") {
5040
+ const parent = changeSet.parentSha;
5041
+ const against = !parent ? "the empty tree" : `its ${(changeSet.parentCount ?? 1) > 1 ? "first parent" : "parent"} ${sha(parent)}`;
5042
+ lines.push(`The change commit ${sha(changeSet.data.pullRequest.head.sha)} made, compared against ${against}.`);
5043
+ } else {
5044
+ lines.push(changeSet.mergeBase ? `Compared against <code>${escapeHtml$1(changeSet.baseRef)}</code>, from where this branch split off at ${sha(changeSet.mergeBase)}.` : `Compared against <code>${escapeHtml$1(changeSet.baseRef)}</code>.`);
5045
+ if (commits && commits.length > 0) {
5046
+ const more = commits.length > 10 ? ` and ${commits.length - 10} more` : "";
5047
+ lines.push(`Changes across ${plural(commits.length)}: ${commits.slice(0, 10).map(sha).join(" ")}${more}.`);
5048
+ } else if (counted) lines.push(`Changes across ${plural(counted)}.`);
5049
+ }
5050
+ lines.push(changeSet.includesUncommitted ? "This review also read uncommitted changes in the working tree." : "Uncommitted changes are never part of a review.");
5051
+ if (live) lines.push("<a href=\"/help#what-gets-reviewed\">How review ranges work</a>");
5052
+ return `<details class="prb-range" data-range-details>
5053
+ <summary class="prb-range-summary">${changeSet.targetType === "commit" ? "1 commit" : counted ? plural(counted) : "Range"}</summary>
5054
+ <div class="prb-range-panel">${lines.map((line) => `<p>${line}</p>`).join("")}</div>
5055
+ </details>`;
5056
+ }
5057
+ /**
4985
5058
  * The review's own bar, below the global strip.
4986
5059
  *
4987
5060
  * Everything here is about this review and nothing else is: the strip above carries the workspace's
@@ -5002,47 +5075,75 @@ function renderReviewBar(view, freshness) {
5002
5075
  ${changeSet.includesUncommitted ? "<span class=\"prb-file-status\">+ working tree</span>" : ""}
5003
5076
  ${renderAuthor(view)}
5004
5077
  </span>
5078
+ ${renderRange(changeSet, Boolean(view.token))}
5005
5079
  <div class="prb-header-stats">
5006
5080
  <span>${fileCount} file${fileCount === 1 ? "" : "s"}</span>
5007
5081
  <span><span class="prb-add">+${additions}</span> <span class="prb-del">&minus;${deletions}</span></span>
5008
5082
  ${freshness || view.remote ? (() => {
5009
5083
  const { label, state } = freshnessLabel(freshness ?? null, view.remote ?? null);
5010
- return `<span class="prb-fresh-status" data-fresh-status data-state="${state}">${escapeHtml$1(label)}</span>${view.token ? "<button type=\"button\" class=\"prb-fresh-check\" data-fresh-check>Check for updates</button>" : ""}`;
5084
+ return `<span class="prb-fresh-status" data-fresh-status data-state="${state}">${escapeHtml$1(label)}</span>`;
5011
5085
  })() : ""}
5012
- ${view.token ? `<button type="button" class="prb-complete" data-complete aria-pressed="${view.complete === true}">${view.complete ? "Reviewed" : "Mark reviewed"}</button>` : ""}
5013
- ${view.id ? renderIdMenu(view.id, Boolean(view.token), view.saved === true) : ""}
5014
- ${view.session.result ? "<button type=\"button\" class=\"prb-ask-toggle\" data-ask-toggle aria-expanded=\"false\">Buddy AI</button>" : ""}
5086
+ ${view.token ? renderStatePills(view) : ""}
5087
+ ${view.id ? renderIdMenu(view, freshness) : ""}
5015
5088
  </div>
5016
5089
  </div>`;
5017
5090
  }
5018
5091
  /**
5019
- * This review's ID, where somebody looking at this review would think to find it.
5092
+ * Where this review stands, as pills that say and never do.
5020
5093
  *
5021
- * In the header because the header answers "what am I looking at", and an ID is the most literal
5022
- * possible answer to that: it is the name `prreviewbuddy review --review <review> --delete` takes.
5023
- * It was on the index only, so the one page devoted to a single review was the only place that
5024
- * could not tell you which review it was, and the help text's advice amounted to reading it out of
5025
- * the address bar.
5094
+ * Two, because they answer two questions. Reviewed or In progress is what the reviewer has done;
5095
+ * Waiting or Done is what happens next with the branch. A review can be Reviewed and Waiting, so a
5096
+ * single pill would have to drop one of them. Active draws nothing, as an open pull request does
5097
+ * not: it is where every review starts, and a pill that never varies teaches nothing.
5026
5098
  *
5027
- * The same `...` and the same menu markup as the index, deliberately. One gesture, in both places
5028
- * it can be wanted, behaving identically. That includes deleting, with the same confirmation in the
5029
- * same panel and the same sentence. The list is where you land afterwards, since the page you were
5030
- * reading no longer exists. It deletes this review only, as a history row on the list does.
5099
+ * Never "New" here, because being on this page is what ends it.
5100
+ */
5101
+ function renderStatePills(view) {
5102
+ const status = view.complete ? "complete" : "in-progress";
5103
+ const pill = lifecyclePill(view.lifecycle, view.activity);
5104
+ return `<span class="prb-card-status" data-status="${status}" data-page-status>${statusLabel(status)}</span>
5105
+ <span class="prb-card-status prb-page-lifecycle" data-page-lifecycle${pill?.changed ? " data-changed" : ""}${pill ? "" : " hidden"}>${escapeHtml$1(pill?.text ?? "")}</span>`;
5106
+ }
5107
+ /**
5108
+ * Everything that can be done to this review, from the page you are reading it on.
5109
+ *
5110
+ * The header only says where the review stands; this is where that is changed. It holds what the
5111
+ * Reviews list's card menu holds, in the same words from the same registries, so nobody has to go
5112
+ * back to the list to park or queue the review they are in. Check for updates leads because it is
5113
+ * the one thing here the list does not offer, and it is here rather than in the header because the
5114
+ * page already checks by itself, every minute while visible and on coming back to the tab.
5031
5115
  *
5032
- * No delete where there is no server, which is the offline preview: the item would be a lie there.
5033
- * Save follows the same rule, since saving is a write to the store the preview does not have. It
5034
- * is the index's star, reachable from the page you are reading, so its words are the star's
5035
- * `aria-pressed` pair spelled out: what pressing it will do, never the state it is in.
5116
+ * Waiting, Done and the queue belong to the branch, so they are offered on its newest review only,
5117
+ * as on the list, where the older reviews' rows carry none. The lineage notice already points at
5118
+ * the newest one.
5119
+ *
5120
+ * Delete keeps the index's confirmation, in the same panel with the same sentence. The list is
5121
+ * where you land afterwards, since the page you were reading no longer exists.
5122
+ *
5123
+ * Nothing that writes where there is no server, which is the offline preview: each item would be a
5124
+ * lie there. Copy review ID stays, because it is the name `prreviewbuddy review --review <review>
5125
+ * --delete` takes, and the page devoted to one review should be able to say which one it is.
5036
5126
  */
5037
- function renderIdMenu(id, canWrite, saved) {
5038
- const safe = escapeHtml$1(id);
5039
- return `<div class="prb-menu" data-menu>
5127
+ function renderIdMenu(view, freshness) {
5128
+ const safe = escapeHtml$1(view.id ?? "");
5129
+ const canWrite = Boolean(view.token);
5130
+ const latest = !view.lineage || view.lineage.position >= view.lineage.total;
5131
+ const mark = view.complete ? "in-progress" : "complete";
5132
+ const branchItems = canWrite && latest;
5133
+ const items = [
5134
+ canWrite && (freshness || view.remote) ? "<button type=\"button\" class=\"prb-menu-item\" data-fresh-check>Check for updates</button>" : "",
5135
+ canWrite ? `<button type="button" class="prb-menu-item" data-mark-status="${mark}">${markItemLabel(mark)}</button>` : "",
5136
+ ...branchItems ? LIFECYCLES.map(({ id }) => `<button type="button" class="prb-menu-item" data-lifecycle-set="${id}"${offersLifecycle(id, view.lifecycle, view.activity) ? "" : " hidden"}>${escapeHtml$1(lifecycleAction(id))}</button>`) : [],
5137
+ branchItems ? `<button type="button" class="prb-menu-item" data-queue-action="${view.queued ? "remove" : "add"}">${queueItemLabel(view.queued === true)}</button>` : "",
5138
+ canWrite ? `<button type="button" class="prb-menu-item" data-save-toggle aria-pressed="${view.saved === true}">${saveItemLabel(view.saved === true)}</button>` : "",
5139
+ `<button type="button" class="prb-menu-item" data-copy-id="${safe}">Copy review ID</button>`,
5140
+ canWrite ? `<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${safe}">Delete review</button>` : ""
5141
+ ].filter(Boolean);
5142
+ return `<div class="prb-menu" data-menu data-review-menu${[view.lifecycle ? ` data-lifecycle-state="${view.lifecycle.state}" data-lifecycle-at="${view.lifecycle.at}"` : "", view.activity ? ` data-activity-at="${view.activity.at}" data-activity-kind="${view.activity.kind}"` : ""].join("")}>
5040
5143
  <button type="button" class="prb-menu-trigger" data-menu-trigger aria-expanded="false" aria-haspopup="true" aria-label="Actions for this review">&#8943;</button>
5041
5144
  <div class="prb-menu-panel" data-menu-panel hidden>
5042
5145
  <div data-menu-items>
5043
- ${canWrite ? `<button type="button" class="prb-menu-item" data-save-toggle aria-pressed="${saved}">${saveItemLabel(saved)}</button>` : ""}
5044
- <button type="button" class="prb-menu-item" data-copy-id="${safe}">Copy review ID</button>
5045
- ${canWrite ? `<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${safe}">Delete review</button>` : ""}
5146
+ ${items.join("\n ")}
5046
5147
  </div>
5047
5148
  ${canWrite ? `<div class="prb-menu-confirm" data-menu-confirm hidden>
5048
5149
  <p class="prb-menu-question">Delete this review?</p>
@@ -5059,7 +5160,7 @@ function renderIdMenu(id, canWrite, saved) {
5059
5160
  * Everything below the review: where else to go, and what to do to the server.
5060
5161
  *
5061
5162
  * These are in the rail rather than the header because they are not about this review. The header
5062
- * answers "what am I looking at" — branch, size, whether I am done with it, and Buddy AI — and
5163
+ * answers "what am I looking at" — branch, size, where the review stands — and
5063
5164
  * anything global sitting up there competed with that. Leaving the review is navigation, and it
5064
5165
  * belongs with the other navigation.
5065
5166
  *
@@ -5569,7 +5670,7 @@ function renderAsk(view, sections, paused = false) {
5569
5670
  const pending = view.pendingAsk ?? null;
5570
5671
  const pendingTurn = pending ? `<div class="prb-msg prb-msg-user">${escapeHtml$1(pending.question ?? "")}</div>
5571
5672
  <div class="prb-thinking" data-ask-pending><i></i><i></i><i></i><span>Reading the change…</span></div>` : "";
5572
- return `<aside class="prb-ask" aria-label="Ask Buddy AI">
5673
+ return `<aside class="prb-ask" id="prb-ask" aria-label="Ask Buddy AI">
5573
5674
  <div
5574
5675
  class="prb-ask-resize"
5575
5676
  data-ask-resize
@@ -5579,9 +5680,12 @@ function renderAsk(view, sections, paused = false) {
5579
5680
  tabindex="0"
5580
5681
  ></div>
5581
5682
  <div class="prb-ask-header">
5582
- <h2>Ask Buddy AI</h2>
5583
- <span class="prb-ask-scope" data-ask-scope>whole change</span>
5683
+ <hgroup class="prb-ask-heading">
5684
+ <h2>Ask Buddy AI</h2>
5685
+ <p class="prb-ask-scope">Asking about: <span data-ask-scope>the whole change</span></p>
5686
+ </hgroup>
5584
5687
  ${renderFocusToggle("buddy", "Buddy AI")}
5688
+ <button type="button" class="prb-ask-hide" data-ask-toggle aria-controls="prb-ask" aria-label="Hide Buddy AI">${ASK_HIDE_ICON}</button>
5585
5689
  </div>
5586
5690
  ${paused ? "" : renderPromptBank(view.prompts ?? [], view.promptsCollapsed === true)}
5587
5691
  <div class="prb-ask-log" data-ask-log>
@@ -7034,7 +7138,7 @@ function renderCardMenu(group, followed, queued, status, activity) {
7034
7138
  function renderMenu(item) {
7035
7139
  const id = escapeHtml$1(item.id);
7036
7140
  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>`;
7037
- 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>`;
7141
+ const queueItem = item.queue === void 0 ? "" : `<button type="button" class="prb-menu-item" data-queue-action="${item.queue}" data-queue-id="${id}">${queueItemLabel(item.queue === "remove")}</button>`;
7038
7142
  const lifecycleItems = item.lifecycle === void 0 ? "" : LIFECYCLES.map(({ id: target }) => `<button type="button" class="prb-menu-item" data-lifecycle-set="${target}" data-lifecycle-id="${id}"${offersLifecycle(target, item.lifecycle.stored, item.lifecycle.activity) ? "" : " hidden"}>${escapeHtml$1(lifecycleAction(target))}</button>`).join("\n ");
7039
7143
  return `<div class="prb-menu" data-menu>
7040
7144
  <button type="button" class="prb-menu-trigger" data-menu-trigger aria-expanded="false" aria-haspopup="true" aria-label="${escapeHtml$1(item.label)}">&#8943;</button>
@@ -7843,6 +7947,87 @@ function defaultFetchDeps() {
7843
7947
  };
7844
7948
  }
7845
7949
  //#endregion
7950
+ //#region ../../packages/review-harness/src/workspace/help_page.ts
7951
+ /** The diagrams are text on purpose: they are the same ones `prreviewbuddy --help` prints. */
7952
+ var TOPICS = [{
7953
+ id: "what-gets-reviewed",
7954
+ title: "What gets reviewed",
7955
+ body: `<p>A review is the combined change between a base and the branch you review, shown as one
7956
+ diff. It is not just the latest commit. The base determines the split point: the commit where
7957
+ your branch left it (git calls this the merge-base). PR Review Buddy reviews the changes on your
7958
+ branch since that point.</p>
7959
+ <pre class="prb-help-diagram">main: A---B---C
7960
+ \\
7961
+ feature: D---E</pre>
7962
+ <p>Reviewing <code>feature</code> against <code>main</code> covers D and E together. C is on
7963
+ main but not on your branch, so it is not part of the review, and the review does not show its
7964
+ changes as removed. Commits already in the base are never reviewed again.</p>
7965
+
7966
+ <h3>Local main and origin/main</h3>
7967
+ <p>Your local <code>main</code> can hold commits that are not on <code>origin/main</code> yet,
7968
+ such as a merge you have not pushed. When the two are different commits, the one you choose
7969
+ moves the split point.</p>
7970
+ <pre class="prb-help-diagram">origin/main: A---B
7971
+ \\
7972
+ local main: C---D
7973
+ \\
7974
+ feature: E</pre>
7975
+ <ul>
7976
+ <li>Against local <code>main</code>, the review covers E.</li>
7977
+ <li>Against <code>origin/main</code>, it covers C, D and E together.</li>
7978
+ </ul>
7979
+ <p>When nobody names a base and the two differ, PR Review Buddy does not guess. It asks which
7980
+ one you mean and says which commits each would review.</p>
7981
+
7982
+ <h3>Uncommitted changes</h3>
7983
+ <p>Never included. A review is a fixed snapshot of your commits, made in a checkout of its own,
7984
+ so work you have not committed is not part of it.</p>
7985
+
7986
+ <h3>Choosing a base</h3>
7987
+ <p>The base is chosen when the review is made, in the terminal:</p>
7988
+ <ul>
7989
+ <li><code>prreviewbuddy review --base refs/heads/main</code> for everything on your branch since it left your local main</li>
7990
+ <li><code>prreviewbuddy review --base origin/main</code> for everything on your branch not yet on origin/main</li>
7991
+ <li><code>prreviewbuddy review --commit &lt;commit&gt;</code> for one commit, against its parent</li>
7992
+ </ul>
7993
+ <p>Every review shows its own range. The commit count beside the branch name at the top of a
7994
+ review opens its split point and the commits it covers.</p>`
7995
+ }];
7996
+ function renderHelpPage(view) {
7997
+ return `<!doctype html>
7998
+ <html lang="en"${themeAttribute(view.theme)}>
7999
+ <head>
8000
+ <meta charset="utf-8">
8001
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8002
+ <title>Help · PR Review Buddy</title>
8003
+ <link rel="icon" type="image/png" href="/assets/icon128.png">
8004
+ <link rel="stylesheet" href="/assets/sidebar.css">
8005
+ <link rel="stylesheet" href="/assets/shell.css">
8006
+ </head>
8007
+ <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}">
8008
+ ${renderGlobalBar({
8009
+ current: "help",
8010
+ unread: view.unreadNotifications ?? 0,
8011
+ driving: view.driving
8012
+ })}
8013
+ ${renderContextBar("Help", "")}
8014
+ <div class="prb-shell">
8015
+ ${renderWorkspaceRail({
8016
+ current: "help",
8017
+ unread: view.unreadNotifications ?? 0
8018
+ })}
8019
+ <main class="prb-index prb-help">
8020
+ ${TOPICS.map((topic) => `<section class="prb-help-topic" id="${topic.id}">
8021
+ <h2>${escapeHtml$1(topic.title)}</h2>
8022
+ ${topic.body}
8023
+ </section>`).join("")}
8024
+ </main>
8025
+ </div>
8026
+ <script src="/assets/help.js" type="module"><\/script>
8027
+ </body>
8028
+ </html>`;
8029
+ }
8030
+ //#endregion
7846
8031
  //#region ../../packages/review-harness/src/workspace/notifications_page.ts
7847
8032
  function renderNotificationsPage(view) {
7848
8033
  const unread = view.notifications.filter((notification) => notification.readAt === void 0).length;
@@ -8193,12 +8378,25 @@ function buildPrompt$2(workspace, question, sectionId, cwd) {
8193
8378
  ...recent.length > 0 ? [
8194
8379
  "---",
8195
8380
  "EARLIER IN THIS CONVERSATION:",
8196
- recent.map((entry) => `Q: ${entry.question}\nA: ${entry.answer}`).join("\n\n")
8381
+ recent.map((entry) => `Q ${askedWhere(workspace, entry.sectionId)}: ${entry.question}\nA: ${entry.answer}`).join("\n\n")
8197
8382
  ] : [],
8198
8383
  "---",
8199
8384
  `QUESTION: ${question}`
8200
8385
  ].join("\n");
8201
8386
  }
8387
+ /**
8388
+ * Where an earlier question was asked, which the question alone does not say.
8389
+ *
8390
+ * "What is risky here?" in one theme followed by "and how does that compare?" in another reads, as
8391
+ * bare text, as two questions about the same thing. A theme that has since gone, because the
8392
+ * review was updated, is said to have gone rather than dropped: dropping it would present a
8393
+ * theme's question as one about the whole change.
8394
+ */
8395
+ function askedWhere(workspace, sectionId) {
8396
+ if (sectionId === null) return "(asked about the whole change)";
8397
+ const section = workspace.session.result?.sections.find((s) => s.id === sectionId);
8398
+ return section ? `(asked while reading the "${section.title}" theme)` : "(asked while reading a theme that is no longer in the review)";
8399
+ }
8202
8400
  //#endregion
8203
8401
  //#region ../../packages/review-harness/src/workspace/assistant_runs.ts
8204
8402
  var RUN_KINDS = ["ask", "diagram"];
@@ -9206,6 +9404,7 @@ async function handle(req, res, options) {
9206
9404
  if (url.pathname === "/open") return handleOpen(req, res, url.searchParams, options.indexToken);
9207
9405
  if (url.pathname === "/settings") return void handleSettings(req, res, url.searchParams, options.indexToken);
9208
9406
  if (url.pathname === "/notifications") return void handleNotifications(req, res, url.searchParams, options.indexToken);
9407
+ if (url.pathname === "/help") return void handleHelp(req, res, url.searchParams, options.indexToken);
9209
9408
  if (url.pathname === "/notifications/open") return handleNotificationOpen(req, res, url.searchParams, options.indexToken);
9210
9409
  if (url.pathname === "/notifications/recent") return handleRecentNotifications(req, res, url.searchParams, options.indexToken);
9211
9410
  if (url.pathname === "/pin") return handlePin(req, res, options.indexToken);
@@ -9239,6 +9438,8 @@ async function handle(req, res, options) {
9239
9438
  if (action === "/stop") return handleStop(req, res, workspace);
9240
9439
  if (action === "/kiss") return handleKiss(req, res, workspace);
9241
9440
  if (action === "/state") return handleState(req, res, workspace);
9441
+ if (action === "/lifecycle") return handleOwnLifecycle(req, res, workspace, options.establishBaseline ?? establishBaseline);
9442
+ if (action === "/queue") return handleOwnQueue(req, res, workspace);
9242
9443
  if (action === "/delete") return handleDeleteOwn(req, res, workspace);
9243
9444
  if (action === "/event") return handleEvent(req, res, workspace);
9244
9445
  if (action === "/prefs") return handlePrefs(req, res);
@@ -9413,6 +9614,18 @@ async function handleSettings(req, res, params, indexToken) {
9413
9614
  }), indexCookieHeader(indexToken));
9414
9615
  }
9415
9616
  /**
9617
+ * Help. Static, but still behind the index's credential like every other workspace place: its
9618
+ * rail and strip carry the unread count, which is this machine's state.
9619
+ */
9620
+ function handleHelp(req, res, params, indexToken) {
9621
+ if (!indexAuthorised(req, params, indexToken)) return refuseIndex(req, res);
9622
+ sendHtml(res, 200, renderHelpPage({
9623
+ unreadNotifications: unreadCount$1(),
9624
+ driving: isDriving(),
9625
+ theme: readPreferences().theme
9626
+ }), indexCookieHeader(indexToken));
9627
+ }
9628
+ /**
9416
9629
  * Whether the caller is a browser navigating, rather than something reading the API.
9417
9630
  *
9418
9631
  * `Accept` is the only signal available and browsers are consistent about it: a navigation asks
@@ -9545,7 +9758,6 @@ async function handleStatus(req, res, indexToken) {
9545
9758
  current.completedAt = complete ? now : void 0;
9546
9759
  if (!complete) current.lastViewedAt = current.lastViewedAt ?? now;
9547
9760
  })) continue;
9548
- dequeueOnComplete(id, complete);
9549
9761
  changed += 1;
9550
9762
  }
9551
9763
  send(res, 200, {
@@ -9579,28 +9791,110 @@ async function handleLifecycle(req, res, indexToken, baseline) {
9579
9791
  return send(res, 400, { error: `There is no lifecycle state called \`${String(state)}\`. Choose one of ${choices}.` });
9580
9792
  }
9581
9793
  const ids = Array.isArray(body?.ids) ? body.ids.filter((id) => typeof id === "string") : [];
9582
- const now = Date.now();
9794
+ const { changed, at } = applyLifecycle(ids, state);
9795
+ send(res, 200, {
9796
+ ok: true,
9797
+ changed,
9798
+ at,
9799
+ queue: readQueue()
9800
+ });
9801
+ baselineParked(ids, state, baseline);
9802
+ }
9803
+ /**
9804
+ * The write behind every lifecycle mark, from the list or from the review's own page. Waiting and
9805
+ * Done dequeue; see `handleLifecycle`.
9806
+ */
9807
+ function applyLifecycle(ids, state) {
9808
+ const at = Date.now();
9583
9809
  let changed = 0;
9584
9810
  for (const id of ids) {
9585
9811
  if (!mutateWorkspace(id, (current) => {
9586
9812
  current.lifecycle = state === "active" ? void 0 : {
9587
9813
  state,
9588
- at: now
9814
+ at
9589
9815
  };
9590
9816
  })) continue;
9591
9817
  if (state !== "active") queueRemove(id);
9592
9818
  changed += 1;
9593
9819
  }
9820
+ return {
9821
+ changed,
9822
+ at
9823
+ };
9824
+ }
9825
+ /** After the answer is sent, so a first look at the forge never holds up the page that asked. */
9826
+ function baselineParked(ids, state, baseline) {
9827
+ if (state !== "waiting") return;
9828
+ const unseen = unobserved(ids);
9829
+ if (unseen.size > 0) baseline(unseen);
9830
+ }
9831
+ /**
9832
+ * Lifecycle and queue, asked by the review's own page on its own token.
9833
+ *
9834
+ * Both belong to the branch, and the list only ever reads them from the newest review of it, so a
9835
+ * mark written on an older one would be a write nobody sees. The page offers neither there; this
9836
+ * says why to anything that asks anyway.
9837
+ */
9838
+ function refuseUnlessLatest(res, workspace) {
9839
+ const { position, total } = lineagePosition(workspace);
9840
+ if (position >= total) return false;
9841
+ send(res, 409, { error: "A newer review of this branch exists. Waiting, Done and the review queue are set on the newest one." });
9842
+ return true;
9843
+ }
9844
+ /**
9845
+ * What the Reviews list would say about this branch: the newest analysis's creation, a push the
9846
+ * watcher saw, or a change of pull request state. Reads every record, so it is asked only where
9847
+ * it can change what is drawn.
9848
+ */
9849
+ function branchActivity(workspace, all = recentWorkspaces(Number.MAX_SAFE_INTEGER)) {
9850
+ const group = groupByLineage(all).find((candidate) => candidate.history.some((review) => review.id === workspace.id));
9851
+ return group ? reviewActivity([group]).get(group.latest.id) : void 0;
9852
+ }
9853
+ /**
9854
+ * Where the review stands, for the page's own checks to repaint from. Another tab can mark or park
9855
+ * it, and the watcher can see the branch move while the page is open.
9856
+ *
9857
+ * Activity only for a parked review: it decides nothing for an Active one, and it costs a read of
9858
+ * every record (measured 27 ms warm, 525 ms cold, on 66 reviews), once a minute per open tab.
9859
+ */
9860
+ function pageStanding(workspace) {
9861
+ const lifecycle = parseLifecycle(workspace.lifecycle) ?? null;
9862
+ return {
9863
+ complete: workspace.completedAt !== void 0,
9864
+ lifecycle,
9865
+ queued: readQueue().includes(workspace.id),
9866
+ activity: lifecycle ? branchActivity(workspace) ?? null : null
9867
+ };
9868
+ }
9869
+ /** Where this review stands after a lifecycle or queue change, which is everything the page repaints. */
9870
+ function ownStanding(id) {
9871
+ return {
9872
+ lifecycle: parseLifecycle(loadWorkspace(id)?.lifecycle) ?? null,
9873
+ queued: readQueue().includes(id)
9874
+ };
9875
+ }
9876
+ async function handleOwnLifecycle(req, res, workspace, baseline) {
9877
+ const state = (await readJson(req))?.state;
9878
+ if (!isLifecycle(state)) {
9879
+ const choices = LIFECYCLES.map((lifecycle) => lifecycle.id).join(", ");
9880
+ return send(res, 400, { error: `There is no lifecycle state called \`${String(state)}\`. Choose one of ${choices}.` });
9881
+ }
9882
+ if (refuseUnlessLatest(res, workspace)) return;
9883
+ applyLifecycle([workspace.id], state);
9594
9884
  send(res, 200, {
9595
9885
  ok: true,
9596
- changed,
9597
- at: now,
9598
- queue: readQueue()
9886
+ ...ownStanding(workspace.id)
9599
9887
  });
9600
- if (state === "waiting") {
9601
- const unseen = unobserved(ids);
9602
- if (unseen.size > 0) baseline(unseen);
9603
- }
9888
+ baselineParked([workspace.id], state, baseline);
9889
+ }
9890
+ async function handleOwnQueue(req, res, workspace) {
9891
+ const action = (await readJson(req))?.action;
9892
+ if (action !== "add" && action !== "remove") return send(res, 400, { error: "Unknown queue action." });
9893
+ if (refuseUnlessLatest(res, workspace)) return;
9894
+ const stored = applyQueue(workspace.id, action);
9895
+ if (stored === null) return send(res, 500, { error: "The queue could not be saved." });
9896
+ if (action === "add" && !stored.includes(workspace.id)) return send(res, 409, { error: `The review queue is full. It holds 50 reviews at most.` });
9897
+ send(res, 200, ownStanding(workspace.id));
9604
9898
  }
9605
9899
  /**
9606
9900
  * Which of these reviews the watcher has never seen the head of.
@@ -9713,28 +10007,17 @@ async function handleQueue(req, res, indexToken) {
9713
10007
  return addMany(res, body.ids.filter((one) => typeof one === "string"));
9714
10008
  }
9715
10009
  if (!id || !loadWorkspace(id)) return send(res, 404, { error: "No such review" });
10010
+ const stored = applyQueue(id, action);
10011
+ if (stored === null) return send(res, 500, { error: "The queue could not be saved." });
10012
+ send(res, 200, { queue: stored });
10013
+ }
10014
+ /** One review in or out of the queue, from a card or from its own page. The queue as stored, or null. */
10015
+ function applyQueue(id, action) {
9716
10016
  if (action === "add") compactQueue(readQueue());
9717
10017
  const stored = action === "add" ? queueAdd(id) : queueRemove(id);
9718
- if (stored === null) return send(res, 500, { error: "The queue could not be saved." });
10018
+ if (stored === null) return null;
9719
10019
  if (action === "add") activateQueued([id], stored);
9720
- send(res, 200, { queue: compactQueue(stored) });
9721
- }
9722
- /**
9723
- * Queueing a review puts it back in Active.
9724
- *
9725
- * The queue is work the reviewer wants surfaced next, and Waiting and Done both say the opposite; a
9726
- * queued review folded away in Done would be a contradiction the page then has to draw. Parking
9727
- * takes a review out of the queue, and this is the same rule read the other way. Only what actually
9728
- * went in: a review refused because the queue was full keeps the lifecycle it had.
9729
- */
9730
- function activateQueued(ids, queue) {
9731
- const queued = new Set(queue);
9732
- for (const one of ids) {
9733
- if (!queued.has(one) || loadWorkspace(one)?.lifecycle === void 0) continue;
9734
- mutateWorkspace(one, (current) => {
9735
- current.lifecycle = void 0;
9736
- });
9737
- }
10020
+ return compactQueue(stored);
9738
10021
  }
9739
10022
  /**
9740
10023
  * Adding a selection, and saying which of it did not go in.
@@ -9976,6 +10259,7 @@ async function sendWorkspacePage(res, workspace) {
9976
10259
  if (notice) mutateWorkspace(workspace.id, (current) => {
9977
10260
  if (current.lastUpdate?.at === notice.at) current.lastUpdate.acknowledged = true;
9978
10261
  });
10262
+ const everyReview = recentWorkspaces(Number.MAX_SAFE_INTEGER);
9979
10263
  const html = renderWorkspace({
9980
10264
  session: workspace.session,
9981
10265
  changeSet: workspace.changeSet,
@@ -10014,7 +10298,10 @@ async function sendWorkspacePage(res, workspace) {
10014
10298
  changeMap: workspace.changeMap,
10015
10299
  reviewedSha: reviewedCommit(workspace),
10016
10300
  revision: workspaceRevision(workspace),
10017
- lineage: lineagePosition(workspace),
10301
+ lineage: lineagePosition(workspace, everyReview),
10302
+ lifecycle: parseLifecycle(workspace.lifecycle),
10303
+ activity: branchActivity(workspace, everyReview),
10304
+ queued: readQueue().includes(workspace.id),
10018
10305
  job: workspace.jobId ? loadJob(workspace.jobId) : void 0,
10019
10306
  updateJob: liveUpdateFor(workspace.id),
10020
10307
  kissRun,
@@ -10134,7 +10421,8 @@ async function handleFreshness(res, workspace, quiet) {
10134
10421
  prState,
10135
10422
  kiss,
10136
10423
  kissDismissedSha: workspace.dismissedKissFreshnessSha ?? null,
10137
- revision
10424
+ revision,
10425
+ standing: pageStanding(workspace)
10138
10426
  });
10139
10427
  if (!quiet) record({
10140
10428
  event: "freshness_checked",
@@ -10151,7 +10439,8 @@ async function handleFreshness(res, workspace, quiet) {
10151
10439
  dismissedSha: workspace.dismissedFreshnessSha ?? null,
10152
10440
  kiss,
10153
10441
  kissDismissedSha: workspace.dismissedKissFreshnessSha ?? null,
10154
- revision
10442
+ revision,
10443
+ standing: pageStanding(workspace)
10155
10444
  });
10156
10445
  }
10157
10446
  /**
@@ -10609,7 +10898,8 @@ async function handleState(req, res, workspace) {
10609
10898
  send(res, 200, {
10610
10899
  ok: true,
10611
10900
  completedAt: saved.completedAt ?? null,
10612
- pinned: saved.pinned === true
10901
+ pinned: saved.pinned === true,
10902
+ queued: readQueue().includes(saved.id)
10613
10903
  });
10614
10904
  }
10615
10905
  /** What `/state` can change. A body naming none of these writes nothing. */
@@ -10627,10 +10917,7 @@ function applyState(workspace, body) {
10627
10917
  if (Array.isArray(body?.dismissedIssues)) reviewer.dismissedIssues = body.dismissedIssues.filter((entry) => typeof entry?.id === "string" && typeof entry?.sessionId === "string");
10628
10918
  if (typeof body?.dismissFreshnessSha === "string" || body?.dismissFreshnessSha === null) workspace.dismissedFreshnessSha = body.dismissFreshnessSha ?? void 0;
10629
10919
  if (typeof body?.dismissKissFreshnessSha === "string" || body?.dismissKissFreshnessSha === null) workspace.dismissedKissFreshnessSha = body.dismissKissFreshnessSha ?? void 0;
10630
- if (typeof body?.complete === "boolean") {
10631
- workspace.completedAt = body.complete ? Date.now() : void 0;
10632
- dequeueOnComplete(workspace.id, body.complete);
10633
- }
10920
+ if (typeof body?.complete === "boolean") workspace.completedAt = body.complete ? Date.now() : void 0;
10634
10921
  if (typeof body?.pinned === "boolean") workspace.pinned = body.pinned;
10635
10922
  }
10636
10923
  /** 1 MB is far more than any question or id list, and far less than a memory problem. */