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/CHANGELOG.md +70 -0
- package/dist/{delete_review-D52wkt66.js → delete_review-BIXqUnpx.js} +142 -36
- package/dist/main.js +41 -3
- package/dist/server.js +355 -68
- package/package.json +1 -1
- package/static/chunk-BeqgAIrQ.js +1 -1
- package/static/{chunk-DaUgQrSg.js → chunk-vvGSmhmc.js} +1 -1
- package/static/help.js +1 -0
- package/static/reviews.js +1 -1
- package/static/shell.css +173 -53
- package/static/workspace.js +62 -62
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
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">−${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
|
|
5084
|
+
return `<span class="prb-fresh-status" data-fresh-status data-state="${state}">${escapeHtml$1(label)}</span>`;
|
|
5011
5085
|
})() : ""}
|
|
5012
|
-
${view.token ?
|
|
5013
|
-
${view.id ? renderIdMenu(view
|
|
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
|
-
*
|
|
5092
|
+
* Where this review stands, as pills that say and never do.
|
|
5020
5093
|
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
5023
|
-
*
|
|
5024
|
-
*
|
|
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
|
-
*
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
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
|
-
*
|
|
5033
|
-
*
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
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(
|
|
5038
|
-
const safe = escapeHtml$1(id);
|
|
5039
|
-
|
|
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">⋯</button>
|
|
5041
5144
|
<div class="prb-menu-panel" data-menu-panel hidden>
|
|
5042
5145
|
<div data-menu-items>
|
|
5043
|
-
${
|
|
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,
|
|
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
|
-
<
|
|
5583
|
-
|
|
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"
|
|
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)}">⋯</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 <commit></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
|
|
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
|
|
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
|
-
|
|
9597
|
-
at: now,
|
|
9598
|
-
queue: readQueue()
|
|
9886
|
+
...ownStanding(workspace.id)
|
|
9599
9887
|
});
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
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
|
|
10018
|
+
if (stored === null) return null;
|
|
9719
10019
|
if (action === "add") activateQueued([id], stored);
|
|
9720
|
-
|
|
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. */
|