prreviewbuddy 0.50.0 → 0.52.2
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 +97 -0
- package/dist/{delete_review-BIXqUnpx.js → delete_review-BiV1aBpx.js} +152 -42
- package/dist/main.js +7 -6
- package/dist/server.js +230 -104
- package/package.json +1 -1
- package/static/{chunk-BeL0x-wt.js → chunk-B5CvMZtt.js} +1 -1
- package/static/chunk-BeqgAIrQ.js +1 -1
- package/static/chunk-CofqJ-Qn.js +1 -0
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +98 -56
- package/static/sidebar.css +250 -34
- package/static/workspace.js +59 -59
- package/static/chunk-vvGSmhmc.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as compactQueue, A as createPrState, An as
|
|
1
|
+
import { $ as compactQueue, $t as loadWorkspace, A as createPrState, An as reviewerDispositions, At as followedBranch, B as toneOf, Bn as resolveModel, Bt as phasesFor, C as readEvents, Cn as queueStamp, Ct as claimRequestUrl, D as announceJob, Dn as isQuestionOutstanding, Dt as fillFileUrlTemplate, E as telemetryUploadConsent, En as isIssueOutstanding, Et as remoteHost, F as markAllRead, Ft as isTerminal, G as extractJsonText, Gn as STORE_ROOT, Gt as isClaimed, H as withUsageRecorded, Hn as readAgentPreference, Ht as purposeOf, I as markRead, It as loadJob, J as readMarker, Jt as indexKeptIds, K as ReviewBeingDeletedError, Kn as AGENT_IDS, Kt as followedRefName, L as pruneNotifications, Ln as isCommitReview, Lt as saveJob, M as eventName, Mn as describeAuthorship, Mt as allJobs, N as headline, Nn as WITHHELD_REASON, Nt as endedAt, O as branchOf, On as issuesOutstanding, Ot as checkCodeFreshness, P as listNotifications, Pn as kissOutcome, Pt as fail, Q as activateQueued, Qn as AgentUnavailableError, Qt as lineagePosition, R as readNotification, Rn as git, Sn as queueRemove, St as writePortPreference, T as recordTelemetryUploadConsent, Tn as doneVerb, Tt as forgeFor, U as EXPLAIN_SIMPLY_PROMPT, Un as writeAgentPreference, V as unreadCount$1, Vn as clearAgentPreference, Vt as progressSteps, W as asString, Xt as latestKissRun, Y as removeWorktree, Yn as agentFor, Yt as indexReviewGroups, Z as relativeTime$1, Zn as AgentCancelledError, _ as agentEnvOf, _n as offersLifecycle, _t as MAX_WORKSPACE_PORT, a as wasBlocked, an as reviewedCommit, bn as queueAdd, bt as resolveWorkspacePort, c as RefreshUnavailableError, cn as touchWorkspace, ct as serverEntryExists, d as refreshPrContext, dn as LIFECYCLES, f as isUnchanged, fn as activityLabel, ft as writeServerRecord, gn as lifecyclePill, gt as DEFAULT_WORKSPACE_PORT, h as liveUpdateFor, hn as lifecycleChip, ht as feedbackUrl, i as liveJobsFor, in as recentWorkspaces, it as clearServerRecord, j as createPrUpdate, jn as processDiscussion, jt as reviewedShaOf, k as subjectOf, kn as questionsOutstanding, kt as checkReview, l as mergeUpdateInto, ln as workspaceRevision, lt as spawnServer, m as UpdateAlreadyRunningError, mn as lifecycleAction, n as deleteReview, nt as readIndexToken, o as discardJob, ot as readServerRecord, p as askCheckout, pn as isLifecycle, pt as BUILD_VERSION, q as MANAGED_ROOT, qt as groupByLineage, r as lineageIds, rn as previousKissResult, s as FETCH_TIMEOUT_MS, t as deleteLineage, tn as mutateWorkspace, tt as settleQueue, u as updateReview, un as workspaceStamps, vn as parseLifecycle, vt as MIN_WORKSPACE_PORT, w as record, wn as readQueue, x as runJob, xn as queueAddMany, xt as statedWorkspacePort, y as startKissJob, yn as sectionOf, yt as parseWorkspacePort, z as removeNotification, zt as isLiveJob } from "./delete_review-BiV1aBpx.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";
|
|
@@ -208,6 +208,30 @@ function resolveSectionId(run, sectionId) {
|
|
|
208
208
|
return run.result.sections.some((section) => section.id === sectionId) ? namespaced(run.id, sectionId) : sectionId;
|
|
209
209
|
}
|
|
210
210
|
//#endregion
|
|
211
|
+
//#region ../../packages/review-harness/src/domain/ai_review/ask_scope.ts
|
|
212
|
+
/**
|
|
213
|
+
* Where an earlier question was asked, which the question alone does not say.
|
|
214
|
+
*
|
|
215
|
+
* "What is risky here?" in one theme followed by "and how does that compare?" in another reads, as
|
|
216
|
+
* bare text, as two questions about the same thing. `undefined` is a question recorded before
|
|
217
|
+
* scopes were, and gets no label: calling it "the whole change" would state something nobody
|
|
218
|
+
* recorded. A theme that has since gone, because the review was updated, is said to have gone.
|
|
219
|
+
*/
|
|
220
|
+
function askedWhere(sections, sectionId) {
|
|
221
|
+
if (sectionId === void 0) return null;
|
|
222
|
+
if (sectionId === null) return "(asked about the whole change)";
|
|
223
|
+
const section = sections.find((s) => s.id === sectionId);
|
|
224
|
+
return section ? `(asked while reading the "${section.title}" theme)` : "(asked while reading a theme that is no longer in the review)";
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The theme the reviewer has open, named rather than used to filter what the model sees: they
|
|
228
|
+
* routinely ask "does this interact with X", where X is in another theme.
|
|
229
|
+
*/
|
|
230
|
+
function readingThemeLine(sections, sectionId) {
|
|
231
|
+
const section = sectionId === null ? void 0 : sections.find((s) => s.id === sectionId);
|
|
232
|
+
return section ? `The reviewer is currently reading the "${section.title}" theme. Answer with that in view, but do not ignore the rest of the change if it bears on the question.` : null;
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
211
235
|
//#region ../../packages/review-harness/src/domain/utils/html.ts
|
|
212
236
|
function escapeHtml$1(value) {
|
|
213
237
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -218,6 +242,47 @@ function renderInlineText(value) {
|
|
|
218
242
|
//#endregion
|
|
219
243
|
//#region ../../packages/review-harness/src/workspace/reviews_index.ts
|
|
220
244
|
/**
|
|
245
|
+
* The sections of the list, top to bottom.
|
|
246
|
+
*
|
|
247
|
+
* Pinned is not a lifecycle state. It is where a card is drawn, on top of a lifecycle it keeps: a
|
|
248
|
+
* pinned review can be Active, Waiting or Done, and unpinning it puts it back in the section that
|
|
249
|
+
* says. That is why it lives here, beside the other placement rules, and not in `LIFECYCLES`.
|
|
250
|
+
*/
|
|
251
|
+
var INDEX_SECTIONS = [
|
|
252
|
+
"pinned",
|
|
253
|
+
"active",
|
|
254
|
+
"waiting",
|
|
255
|
+
"done"
|
|
256
|
+
];
|
|
257
|
+
/**
|
|
258
|
+
* Which section a card is drawn in: Pinned when pinned, and otherwise where its lifecycle says.
|
|
259
|
+
*
|
|
260
|
+
* Pinning controls prominence in the general list and never overrides an ordering the reviewer
|
|
261
|
+
* asked for. Under such a filter a pinned card stays in its lifecycle section, in the order the
|
|
262
|
+
* filter gives, and only its star says it is pinned.
|
|
263
|
+
*/
|
|
264
|
+
function indexSectionOf(pinned, lifecycle, activity, filter) {
|
|
265
|
+
return pinned && !ordersExplicitly(filter) ? "pinned" : sectionOf(lifecycle, activity);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Whether a filter replaces the activity order with one the reviewer set. Queue is the only one:
|
|
269
|
+
* the list is the queue, in its order.
|
|
270
|
+
* `orderIndex` and `indexSectionOf` both ask this, so the ordering and the placement cannot disagree.
|
|
271
|
+
*/
|
|
272
|
+
function ordersExplicitly(filter) {
|
|
273
|
+
return filter === "queue";
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The lifecycle chip on a card, or absent.
|
|
277
|
+
*
|
|
278
|
+
* In a lifecycle section the heading already names the lifecycle, so the chip only says what moved
|
|
279
|
+
* (`lifecycleChip`). In Pinned nothing names it, so the chip does, in the words the review page's
|
|
280
|
+
* pill uses: the list and the page give one answer about the same review.
|
|
281
|
+
*/
|
|
282
|
+
function cardLifecycleChip(pinned, lifecycle, activity) {
|
|
283
|
+
return pinned ? lifecyclePill(lifecycle, activity)?.text : lifecycleChip(lifecycle, activity);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
221
286
|
* "45 reviews · 13 branches", and "13 reviews" when those are the same thirteen things.
|
|
222
287
|
*
|
|
223
288
|
* The two numbers are different questions and only both worth asking once they differ. A page of
|
|
@@ -245,12 +310,12 @@ function markItemLabel(mark) {
|
|
|
245
310
|
return mark === "complete" ? "Mark reviewed" : "Mark in progress";
|
|
246
311
|
}
|
|
247
312
|
/**
|
|
248
|
-
* The review page's
|
|
249
|
-
*
|
|
250
|
-
*
|
|
313
|
+
* The review page's Pin item, which says what pressing it will do. The words of the index's star,
|
|
314
|
+
* so pinning from either place reads as the same act. Here because the browser relabels the item
|
|
315
|
+
* after the change, for the reason `markItemLabel` is here.
|
|
251
316
|
*/
|
|
252
|
-
function
|
|
253
|
-
return
|
|
317
|
+
function pinItemLabel(pinned) {
|
|
318
|
+
return pinned ? "Unpin review" : "Pin review";
|
|
254
319
|
}
|
|
255
320
|
/**
|
|
256
321
|
* The queue item, on a card and on the review's own page. "review queue" rather than "queue",
|
|
@@ -261,16 +326,6 @@ function queueItemLabel(queued) {
|
|
|
261
326
|
return queued ? "Remove from review queue" : "Add to review queue";
|
|
262
327
|
}
|
|
263
328
|
/**
|
|
264
|
-
* What `Continue reviewing` opens, and whether it exists at all.
|
|
265
|
-
*
|
|
266
|
-
* One rule rather than an expression in the template, because "is there anything to continue" and
|
|
267
|
-
* "what is first" are the same question and must not be able to answer differently -- a button that
|
|
268
|
-
* is present but navigates nowhere is worse than no button.
|
|
269
|
-
*/
|
|
270
|
-
function continueTarget(queue) {
|
|
271
|
-
return queue[0] ?? null;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
329
|
* The Queue pill, which carries a number only once there is one.
|
|
275
330
|
*
|
|
276
331
|
* "Queue 0" is a count of something that is not there, and reads as a fault. Bare "Queue" reads as
|
|
@@ -335,6 +390,11 @@ function anythingRunning(states) {
|
|
|
335
390
|
return false;
|
|
336
391
|
}
|
|
337
392
|
//#endregion
|
|
393
|
+
//#region ../../packages/review-harness/src/workspace/fetch_updates_view.ts
|
|
394
|
+
/** A card's and a review page's item. Named for what it reaches: a remote branch, not the list. */
|
|
395
|
+
var FETCH_REVIEW_LABEL = "Fetch remote updates";
|
|
396
|
+
var FETCH_SELECTED_LABEL = "Fetch selected";
|
|
397
|
+
//#endregion
|
|
338
398
|
//#region ../../packages/review-harness/src/workspace/way_back.ts
|
|
339
399
|
var CLI = {
|
|
340
400
|
command: "prreviewbuddy open",
|
|
@@ -636,7 +696,7 @@ function renderWorkspaceRail(options) {
|
|
|
636
696
|
* with the reason for it.
|
|
637
697
|
*/
|
|
638
698
|
function renderHeaderNav(_options) {
|
|
639
|
-
return `<nav class="prb-header-nav" aria-label="Workspace"><a class="prb-feedback" href="${escapeHtml$1(feedbackUrl())}" target="_blank" rel="noopener noreferrer">Send feedback</a></nav>`;
|
|
699
|
+
return `<nav class="prb-header-nav" aria-label="Workspace"><a class="prb-feedback" href="${escapeHtml$1(feedbackUrl())}" target="_blank" rel="noopener noreferrer"><span class="prb-feedback-icon" aria-hidden="true">${iconSvg("feedback")}</span><span class="prb-feedback-label">Send feedback</span></a></nav>`;
|
|
640
700
|
}
|
|
641
701
|
//#endregion
|
|
642
702
|
//#region ../../packages/review-harness/src/domain/utils/file_path.ts
|
|
@@ -773,9 +833,10 @@ var FINDING_COPY_CLASS = "pr-helper-finding-copy";
|
|
|
773
833
|
* buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
|
|
774
834
|
* would give a utility the weight of the title beside it. What it does is said by a hover
|
|
775
835
|
* tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
|
|
776
|
-
* accessible name.
|
|
777
|
-
*
|
|
778
|
-
*
|
|
836
|
+
* accessible name. After a click it shows a green check and says "Copied" (or "Copy failed") in
|
|
837
|
+
* the bubble below it, the words used everywhere else, but not inline: beside it is Ask Buddy. The
|
|
838
|
+
* resting name and tip are kept on the button so the reset returns to "Copy finding" rather than to
|
|
839
|
+
* a chat reply's "Copy message".
|
|
779
840
|
*/
|
|
780
841
|
function renderFindingCopySource(source, kind) {
|
|
781
842
|
const name = kind === "issue" ? "Copy finding" : "Copy question";
|
|
@@ -788,11 +849,12 @@ function renderFindingCopySource(source, kind) {
|
|
|
788
849
|
/** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
|
|
789
850
|
var IDLE_LABEL_ATTR = "data-copy-idle-label";
|
|
790
851
|
var IDLE_ARIA_ATTR = "data-copy-idle-aria";
|
|
852
|
+
var IDLE_TIP_ATTR = "data-copy-idle-tip";
|
|
791
853
|
function button(handle, idle) {
|
|
792
854
|
const { icon } = STATES$1.idle;
|
|
793
855
|
const label = idle?.label ?? STATES$1.idle.label;
|
|
794
856
|
const aria = idle?.aria ?? STATES$1.idle.aria;
|
|
795
|
-
const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}"` : "";
|
|
857
|
+
const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}" ${IDLE_TIP_ATTR}="${escapeHtml$1(idle.tip)}"` : "";
|
|
796
858
|
const control = `<button type="button" class="${idle ? `${MESSAGE_COPY_CLASS} ${FINDING_COPY_CLASS}` : MESSAGE_COPY_CLASS}" ${handle}${remembered}${idle ? ` data-tip="${escapeHtml$1(idle.tip)}"` : ""} aria-label="${escapeHtml$1(aria)}"><span class="${MESSAGE_COPY_CLASS}-icon" aria-hidden="true">${svg(icon)}</span><span class="${MESSAGE_COPY_CLASS}-label"${label ? "" : " hidden"}>${escapeHtml$1(label)}</span></button>`;
|
|
797
859
|
return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
|
|
798
860
|
}
|
|
@@ -813,8 +875,7 @@ function svg(paths) {
|
|
|
813
875
|
* closes and positions exactly as that one does. The panel is empty here and filled when it opens,
|
|
814
876
|
* because it lists the reviewer's saved prompts and those change without the page re-rendering.
|
|
815
877
|
*
|
|
816
|
-
*
|
|
817
|
-
* there for this to open, so `ReviewTabOptions.askBuddy` gates it and the extension never sets it.
|
|
878
|
+
* The host decides where the answer appears; `ReviewTabOptions.askBuddy` says the host has wired it.
|
|
818
879
|
*/
|
|
819
880
|
var ASK_BUDDY_LABEL = "Ask Buddy";
|
|
820
881
|
/** Four-point sparkle, the conventional mark for "ask the AI". currentColor, like Copy beside it. */
|
|
@@ -3617,10 +3678,11 @@ ${renderRunControl(view.canRun, "Run KISS review again")}`;
|
|
|
3617
3678
|
function renderResultBody(run, provenance = "inline") {
|
|
3618
3679
|
const result = run.result;
|
|
3619
3680
|
if (!result) return "";
|
|
3620
|
-
|
|
3681
|
+
const outcome = kissOutcome(result, run.audit);
|
|
3682
|
+
return `${outcome === "proportionate" ? `<div class="prb-kiss-outcome" data-review-outcome="empty">
|
|
3621
3683
|
<h4 class="prb-kiss-outcome-title">Appropriately simple</h4>
|
|
3622
3684
|
<p class="prb-kiss-outcome-body">This change appears appropriately simple for the current requirements. No material simplifications were identified.</p>
|
|
3623
|
-
</div>` : ""}
|
|
3685
|
+
</div>` : outcome === "incomplete" ? renderIncomplete(run) : ""}
|
|
3624
3686
|
<div class="prb-kiss-assessment pr-helper-markdown">${renderMarkdown(result.assessment)}</div>
|
|
3625
3687
|
<div class="prb-kiss-summary pr-helper-markdown">${renderMarkdown(result.summary)}</div>
|
|
3626
3688
|
${result.aggregateEstimate ? `<p class="prb-kiss-estimate"><span class="prb-kiss-estimate-label">Rough estimate</span> ${escapeHtml$1(result.aggregateEstimate)}</p>` : ""}
|
|
@@ -3649,15 +3711,38 @@ function renderKissRecommendations(view) {
|
|
|
3649
3711
|
const result = presented?.result;
|
|
3650
3712
|
if (!result) return `${RECS_HEAD}${renderNoRunYet(view)}`;
|
|
3651
3713
|
const earlier = earlierNote(view, presented);
|
|
3652
|
-
|
|
3714
|
+
const outcome = kissOutcome(result, presented.audit);
|
|
3715
|
+
if (outcome === "incomplete") return `${RECS_HEAD}${earlier}${renderIncomplete(presented)}`;
|
|
3716
|
+
if (outcome === "proportionate") return `${RECS_HEAD}${earlier}
|
|
3653
3717
|
<div class="prb-kiss-outcome" data-review-outcome="empty">
|
|
3654
3718
|
<h4 class="prb-kiss-outcome-title">Nothing to simplify</h4>
|
|
3655
3719
|
<p class="prb-kiss-outcome-body">The analysis completed and did not identify a materially simpler way to meet these requirements.</p>
|
|
3656
3720
|
</div>`;
|
|
3657
|
-
return `${RECS_HEAD}${earlier}
|
|
3721
|
+
return `${RECS_HEAD}${earlier}${renderWithheldNote(presented)}
|
|
3658
3722
|
${result.recommendations.map((recommendation) => renderRecommendation(recommendation)).join("")}`;
|
|
3659
3723
|
}
|
|
3660
3724
|
/**
|
|
3725
|
+
* A run whose every recommendation was withheld: incomplete, and said to be.
|
|
3726
|
+
*
|
|
3727
|
+
* Never "appropriately simple" and never "nothing to simplify". The analysis did recommend
|
|
3728
|
+
* something; it just did not return enough of it to show, and the one conclusion that cannot be
|
|
3729
|
+
* drawn from that is that the change is proportionate. Its own outcome value, so a test can tell it
|
|
3730
|
+
* from both a clean answer and a failed run.
|
|
3731
|
+
*/
|
|
3732
|
+
function renderIncomplete(run) {
|
|
3733
|
+
const withheld = run.audit.withheldRecommendations ?? 0;
|
|
3734
|
+
return `<div class="prb-kiss-outcome" data-review-outcome="incomplete">
|
|
3735
|
+
<h4 class="prb-kiss-outcome-title">Recommendations came back incomplete</h4>
|
|
3736
|
+
<p class="prb-kiss-outcome-body">The analysis returned ${withheld === 1 ? "a recommendation" : `${withheld} recommendations`}, but ${withheld === 1 ? "it was" : "each was"} ${escapeHtml$1(WITHHELD_REASON)}, so ${withheld === 1 ? "it cannot" : "none can"} be shown. This is not a finding that the change is appropriately simple. Running KISS again usually produces a complete answer.</p>
|
|
3737
|
+
</div>`;
|
|
3738
|
+
}
|
|
3739
|
+
/** Said on the list itself when some were withheld, since the correction box is on the other pane. */
|
|
3740
|
+
function renderWithheldNote(run) {
|
|
3741
|
+
const withheld = run.audit.withheldRecommendations ?? 0;
|
|
3742
|
+
if (withheld === 0) return "";
|
|
3743
|
+
return `<p class="prb-kiss-note" data-kiss-withheld>${withheld === 1 ? "1 more recommendation was" : `${withheld} more recommendations were`} withheld as incomplete: ${escapeHtml$1(WITHHELD_REASON)}.</p>`;
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3661
3746
|
* Which run these came from, said only when it is not the one the section is headed by.
|
|
3662
3747
|
*
|
|
3663
3748
|
* Silent in the ordinary case, because a permanent "this is the current run" strip is the noise
|
|
@@ -3871,14 +3956,30 @@ function renderAudit(run) {
|
|
|
3871
3956
|
const notes = [];
|
|
3872
3957
|
if (run.audit.unknownPaths.length > 0) notes.push(`${run.audit.unknownPaths.length} cited ${run.audit.unknownPaths.length === 1 ? "path does" : "paths do"} not exist in the reviewed version of the repository, and ${run.audit.unknownPaths.length === 1 ? "it was" : "they were"} withdrawn: ${run.audit.unknownPaths.join(", ")}.`);
|
|
3873
3958
|
if (run.audit.unlocatedRecommendations > 0) notes.push(`${run.audit.unlocatedRecommendations} recommendation${run.audit.unlocatedRecommendations === 1 ? "" : "s"} ${run.audit.unlocatedRecommendations === 1 ? "is" : "are"} shown without a file to open, because nothing ${run.audit.unlocatedRecommendations === 1 ? "it" : "they"} cited exists in the reviewed version of the repository.`);
|
|
3959
|
+
const withheld = run.audit.withheldRecommendations ?? 0;
|
|
3960
|
+
if (withheld > 0 && run.result && kissOutcome(run.result, run.audit) !== "incomplete") notes.push(`${withheld} recommendation${withheld === 1 ? " was" : "s were"} withheld as incomplete: ${WITHHELD_REASON}.`);
|
|
3874
3961
|
if (notes.length === 0) return "";
|
|
3875
3962
|
return `<div class="prb-kiss-audit" role="note">
|
|
3876
3963
|
<p class="prb-kiss-audit-title">What was corrected</p>
|
|
3877
3964
|
${notes.map((note) => `<p>${escapeHtml$1(note)}</p>`).join("")}
|
|
3878
3965
|
</div>`;
|
|
3879
3966
|
}
|
|
3967
|
+
/**
|
|
3968
|
+
* One recommendation, as a decision aid rather than an analysis record.
|
|
3969
|
+
*
|
|
3970
|
+
* The card carries what a reviewer needs to understand and weigh the recommendation: the title,
|
|
3971
|
+
* the case in a sentence or two, the simpler approach, what it gains, the trade-off that could
|
|
3972
|
+
* change the decision, and where to start. Everything that exists to audit or challenge it sits in
|
|
3973
|
+
* one disclosure beneath, closed until asked for.
|
|
3974
|
+
*
|
|
3975
|
+
* Reads both schemas. A record written under schema 1 has no gist, and its `complexity` stands in
|
|
3976
|
+
* as the lede, because it is the only field of that shape that names what the card is about; it is
|
|
3977
|
+
* then not repeated in the disclosure. Stored runs are history and are not rewritten to fit.
|
|
3978
|
+
*/
|
|
3880
3979
|
function renderRecommendation(recommendation) {
|
|
3881
3980
|
const worthiness = recommendation.commentWorthiness;
|
|
3981
|
+
const lede = recommendation.gist || recommendation.complexity || "";
|
|
3982
|
+
const [start, ...rest] = orderedLocations(recommendation.locations);
|
|
3882
3983
|
return `<article class="prb-kiss-rec" data-payoff="${recommendation.payoff}"${worthiness ? ` data-worthiness="${worthiness}"` : ""}>
|
|
3883
3984
|
<header class="prb-kiss-rec-head">
|
|
3884
3985
|
<h4 class="prb-kiss-rec-title">${escapeHtml$1(recommendation.title)}</h4>
|
|
@@ -3887,20 +3988,56 @@ function renderRecommendation(recommendation) {
|
|
|
3887
3988
|
${worthiness ? `<span class="prb-kiss-worthiness" data-worthiness="${worthiness}">${escapeHtml$1(WORTHINESS[worthiness])}</span>` : ""}
|
|
3888
3989
|
</div>
|
|
3889
3990
|
</header>
|
|
3890
|
-
${
|
|
3891
|
-
${field("
|
|
3892
|
-
${
|
|
3893
|
-
${
|
|
3894
|
-
${
|
|
3895
|
-
|
|
3991
|
+
${lede ? `<div class="prb-kiss-gist pr-helper-markdown" data-kiss-gist>${renderMarkdown(lede)}</div>` : ""}
|
|
3992
|
+
${field("Simpler approach", recommendation.simplerDirection)}
|
|
3993
|
+
${renderGains(recommendation)}
|
|
3994
|
+
${recommendation.caveat ? `<p class="prb-kiss-caveat" data-kiss-caveat><span class="prb-kiss-caveat-label">Trade-off</span> ${escapeHtml$1(recommendation.caveat)}</p>` : ""}
|
|
3995
|
+
${start ? `<div class="prb-kiss-locations">
|
|
3996
|
+
<p class="prb-kiss-locations-label">Start here</p>
|
|
3997
|
+
${renderLocationList([start])}
|
|
3998
|
+
</div>` : ""}
|
|
3999
|
+
${renderEvidence(recommendation, rest)}
|
|
3896
4000
|
${recommendation.draftComment ? `<details class="prb-kiss-draft">
|
|
3897
4001
|
<summary class="prb-kiss-draft-title">Draft comment</summary>
|
|
3898
4002
|
<blockquote class="pr-helper-markdown">${renderMarkdown(recommendation.draftComment)}</blockquote>
|
|
3899
4003
|
${renderMessageCopySource(recommendation.draftComment)}
|
|
3900
4004
|
</details>` : ""}
|
|
3901
|
-
${renderLocations(recommendation.locations)}
|
|
3902
4005
|
</article>`;
|
|
3903
4006
|
}
|
|
4007
|
+
/**
|
|
4008
|
+
* What acting on it gains, on one line: the estimate first, then the gains.
|
|
4009
|
+
*
|
|
4010
|
+
* The estimate says it is one in words beside it, so a phrase like "~35–50 lines" cannot be taken
|
|
4011
|
+
* for a measurement, and the gains beside it are not read as estimates too.
|
|
4012
|
+
*/
|
|
4013
|
+
function renderGains(recommendation) {
|
|
4014
|
+
const items = [...recommendation.reductionEstimate ? [`<span class="prb-kiss-gain" data-estimate>${escapeHtml$1(recommendation.reductionEstimate)} <span class="prb-kiss-gain-note">(estimate)</span></span>`] : [], ...(recommendation.gains ?? []).map((gain) => `<span class="prb-kiss-gain">${escapeHtml$1(gain)}</span>`)];
|
|
4015
|
+
if (items.length === 0) return "";
|
|
4016
|
+
return `<p class="prb-kiss-gains" data-kiss-gains>${items.join("<span class=\"prb-kiss-gain-sep\" aria-hidden=\"true\"> · </span>")}</p>`;
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* The audit trail, in one disclosure: enough to check or challenge the recommendation.
|
|
4020
|
+
*
|
|
4021
|
+
* Closed by default. A reviewer should never have to open it to understand what is being
|
|
4022
|
+
* recommended, only to verify it. The other locations live here too, because the card names the
|
|
4023
|
+
* one place to start and a list of files up front is a decision the reader has not asked to make.
|
|
4024
|
+
*/
|
|
4025
|
+
function renderEvidence(recommendation, rest) {
|
|
4026
|
+
const body = [
|
|
4027
|
+
field("Evidence in this change", recommendation.evidence),
|
|
4028
|
+
field("Why the requirements do not call for it", recommendation.whyNotJustified),
|
|
4029
|
+
field("Trade-offs", recommendation.payoffAndTradeoffs),
|
|
4030
|
+
rest.length > 0 ? `<div class="prb-kiss-locations">
|
|
4031
|
+
<p class="prb-kiss-locations-label">Also relevant</p>
|
|
4032
|
+
${renderLocationList(rest)}
|
|
4033
|
+
</div>` : ""
|
|
4034
|
+
].filter(Boolean);
|
|
4035
|
+
if (body.length === 0) return "";
|
|
4036
|
+
return `<details class="prb-kiss-evidence" data-kiss-evidence>
|
|
4037
|
+
<summary class="prb-kiss-evidence-title">Evidence & trade-offs${escapeHtml$1(rest.length > 0 ? ` · ${rest.length} more ${rest.length === 1 ? "file" : "files"}` : "")}</summary>
|
|
4038
|
+
${body.join("\n ")}
|
|
4039
|
+
</details>`;
|
|
4040
|
+
}
|
|
3904
4041
|
function field(label, value) {
|
|
3905
4042
|
if (!value) return "";
|
|
3906
4043
|
return `<div class="prb-kiss-field">
|
|
@@ -3909,7 +4046,8 @@ function field(label, value) {
|
|
|
3909
4046
|
</div>`;
|
|
3910
4047
|
}
|
|
3911
4048
|
/**
|
|
3912
|
-
* Where
|
|
4049
|
+
* Where an item of complexity worth keeping points, primary first. A recommendation draws its own
|
|
4050
|
+
* "Start here" on the card and keeps the rest in its evidence disclosure; see `renderRecommendation`.
|
|
3913
4051
|
*
|
|
3914
4052
|
* `data-file` and `data-line` and nothing else: the workspace already opens anything carrying
|
|
3915
4053
|
* those, so a KISS location behaves exactly as a standard finding's does and there is no second
|
|
@@ -3945,7 +4083,7 @@ function field(label, value) {
|
|
|
3945
4083
|
*/
|
|
3946
4084
|
function renderLocations(locations) {
|
|
3947
4085
|
if (locations.length === 0) return "";
|
|
3948
|
-
const [first, ...rest] =
|
|
4086
|
+
const [first, ...rest] = orderedLocations(locations);
|
|
3949
4087
|
if (rest.length === 0) return `<div class="prb-kiss-locations">${renderLocationList([first])}</div>`;
|
|
3950
4088
|
return `<div class="prb-kiss-locations">
|
|
3951
4089
|
<p class="prb-kiss-locations-label">Start here</p>
|
|
@@ -3954,6 +4092,10 @@ function renderLocations(locations) {
|
|
|
3954
4092
|
${renderLocationList(rest)}
|
|
3955
4093
|
</div>`;
|
|
3956
4094
|
}
|
|
4095
|
+
/** Primary first, the rest in the order the model gave them. */
|
|
4096
|
+
function orderedLocations(locations) {
|
|
4097
|
+
return [...locations].sort((a, b) => Number(b.primary) - Number(a.primary));
|
|
4098
|
+
}
|
|
3957
4099
|
function renderLocationList(locations) {
|
|
3958
4100
|
return `<ul class="prb-kiss-locations-list">${locations.map((location) => {
|
|
3959
4101
|
const { dir, name } = splitFilePath(location.path);
|
|
@@ -5132,10 +5274,11 @@ function renderIdMenu(view, freshness) {
|
|
|
5132
5274
|
const branchItems = canWrite && latest;
|
|
5133
5275
|
const items = [
|
|
5134
5276
|
canWrite && (freshness || view.remote) ? "<button type=\"button\" class=\"prb-menu-item\" data-fresh-check>Check for updates</button>" : "",
|
|
5277
|
+
branchItems ? `<button type="button" class="prb-menu-item" data-fetch-remote hidden>${escapeHtml$1(FETCH_REVIEW_LABEL)}</button>` : "",
|
|
5135
5278
|
canWrite ? `<button type="button" class="prb-menu-item" data-mark-status="${mark}">${markItemLabel(mark)}</button>` : "",
|
|
5136
5279
|
...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
5280
|
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-
|
|
5281
|
+
canWrite ? `<button type="button" class="prb-menu-item" data-pin-toggle aria-pressed="${view.pinned === true}">${pinItemLabel(view.pinned === true)}</button>` : "",
|
|
5139
5282
|
`<button type="button" class="prb-menu-item" data-copy-id="${safe}">Copy review ID</button>`,
|
|
5140
5283
|
canWrite ? `<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${safe}">Delete review</button>` : ""
|
|
5141
5284
|
].filter(Boolean);
|
|
@@ -6811,10 +6954,6 @@ function startWatching(makeDeps = defaultDeps) {
|
|
|
6811
6954
|
};
|
|
6812
6955
|
}
|
|
6813
6956
|
//#endregion
|
|
6814
|
-
//#region ../../packages/review-harness/src/workspace/fetch_updates_view.ts
|
|
6815
|
-
var FETCH_UPDATES_LABEL = "Fetch updates";
|
|
6816
|
-
var FETCH_SELECTED_LABEL = "Fetch selected";
|
|
6817
|
-
//#endregion
|
|
6818
6957
|
//#region ../../packages/review-harness/src/workspace/reviews_page.ts
|
|
6819
6958
|
function renderReviewsPage(view) {
|
|
6820
6959
|
const now = view.now ?? Date.now();
|
|
@@ -6822,20 +6961,19 @@ function renderReviewsPage(view) {
|
|
|
6822
6961
|
at: group.latest.createdAt,
|
|
6823
6962
|
kind: "created"
|
|
6824
6963
|
};
|
|
6825
|
-
const ordered = [...view.groups].sort((a, b) =>
|
|
6964
|
+
const ordered = [...view.groups].sort((a, b) => activityOf(b).at - activityOf(a).at);
|
|
6826
6965
|
const sections = {
|
|
6966
|
+
pinned: [],
|
|
6827
6967
|
active: [],
|
|
6828
6968
|
waiting: [],
|
|
6829
6969
|
done: []
|
|
6830
6970
|
};
|
|
6831
|
-
for (const group of ordered) sections[
|
|
6971
|
+
for (const group of ordered) sections[indexSectionOf(group.latest.pinned === true, group.latest.lifecycle, activityOf(group), "all")].push(group);
|
|
6832
6972
|
const rank = new Map(ordered.map((group, index) => [group, index]));
|
|
6833
6973
|
const renderCards = (groups) => groups.map((group) => renderCard(group, now, activityOf(group), rank.get(group), view.analyses?.get(group.latest.id), positions.get(group.latest.id), group.history.map((review) => view.moved?.get(review.id)).find((kind) => kind !== void 0), prStateOf(group.latest, view.prStates))).join("");
|
|
6834
6974
|
const storedReviews = view.groups.reduce((total, group) => total + group.history.length, 0);
|
|
6835
6975
|
const queue = view.queue ?? [];
|
|
6836
6976
|
const positions = new Map(queue.map((id, position) => [id, position]));
|
|
6837
|
-
const continues = continueTarget(queue);
|
|
6838
|
-
const front = continues === null ? void 0 : findReview(view.groups, continues);
|
|
6839
6977
|
return `<!doctype html>
|
|
6840
6978
|
<html lang="en"${themeAttribute(view.theme)}>
|
|
6841
6979
|
<head>
|
|
@@ -6859,20 +6997,14 @@ ${renderWorkspaceRail({
|
|
|
6859
6997
|
unread: view.unreadNotifications ?? 0
|
|
6860
6998
|
})}
|
|
6861
6999
|
<main class="prb-index">
|
|
6862
|
-
${front === void 0 ? "" : renderContinue(front)}
|
|
6863
7000
|
${ordered.length === 0 ? "" : `<div class="prb-index-summary">
|
|
6864
7001
|
<p class="prb-index-count" data-count>${escapeHtml$1(countLabel(storedReviews, ordered.length))}</p>
|
|
6865
7002
|
<button type="button" class="prb-index-select" data-select-mode aria-pressed="false">Select</button>
|
|
6866
7003
|
</div>`}
|
|
6867
|
-
|
|
6868
|
-
${renderCards(sections.active)}
|
|
6869
|
-
</div>
|
|
6870
|
-
${renderLoadMore("active")}
|
|
7004
|
+
${INDEX_SECTIONS.map((section) => renderSection(section, sections[section], renderCards)).join("\n ")}
|
|
6871
7005
|
${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>`}
|
|
6872
|
-
${renderSection("waiting", "Waiting", sections.waiting, true, renderCards)}
|
|
6873
|
-
${renderSection("done", "Done", sections.done, false, renderCards)}
|
|
6874
7006
|
${ordered.length === 0 ? "" : renderBulkBar()}
|
|
6875
|
-
<p class="prb-index-note">Reviews are kept for thirty days, on this machine only.
|
|
7007
|
+
<p class="prb-index-note">Reviews are kept for thirty days, on this machine only. Pinned reviews, Done reviews and anything in your review queue are kept for ninety days instead, counting from the last time you opened or marked 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>
|
|
6876
7008
|
</main>
|
|
6877
7009
|
</div>
|
|
6878
7010
|
<script src="/assets/reviews.js" type="module"><\/script>
|
|
@@ -6896,13 +7028,11 @@ function renderControls(queued, authors, anyRequest) {
|
|
|
6896
7028
|
<select data-status-filter aria-label="Filter by status">
|
|
6897
7029
|
<option value="all">All</option>
|
|
6898
7030
|
<option value="queue" data-queue-option>${escapeHtml$1(queueOptionLabel(queued))}</option>
|
|
6899
|
-
<option value="saved">Saved</option>
|
|
6900
7031
|
<option value="in-progress">In progress</option>
|
|
6901
7032
|
<option value="complete">Reviewed</option>
|
|
6902
7033
|
</select>
|
|
6903
7034
|
</label>
|
|
6904
7035
|
${anyRequest ? renderPrFilter() : ""}
|
|
6905
|
-
<button type="button" class="prb-index-fetch-button" data-fetch-updates disabled>${escapeHtml$1(FETCH_UPDATES_LABEL)}</button>
|
|
6906
7036
|
</div>`;
|
|
6907
7037
|
}
|
|
6908
7038
|
/**
|
|
@@ -6934,25 +7064,6 @@ function renderAuthorFilter(authors) {
|
|
|
6934
7064
|
</select>`;
|
|
6935
7065
|
}
|
|
6936
7066
|
/**
|
|
6937
|
-
* The way back into the batch, above the list.
|
|
6938
|
-
*
|
|
6939
|
-
* A link rather than a button, and carrying the review's own token, because that is what every
|
|
6940
|
-
* other way into a review on this page is -- the list is the only thing holding every credential.
|
|
6941
|
-
*
|
|
6942
|
-
* It names the front of the queue rather than saying only "Continue reviewing", so somebody
|
|
6943
|
-
* returning after an hour can tell whether it is about to open what they expect before they click.
|
|
6944
|
-
*/
|
|
6945
|
-
function renderContinue(review) {
|
|
6946
|
-
return `<a class="prb-index-continue" href="${escapeHtml$1(`/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}`)}">
|
|
6947
|
-
<span class="prb-index-continue-label">Continue reviewing</span>
|
|
6948
|
-
<span class="prb-index-continue-title">${escapeHtml$1(review.title)}</span>
|
|
6949
|
-
</a>`;
|
|
6950
|
-
}
|
|
6951
|
-
/** The review a queued id refers to. The caller has already guaranteed it is one this page shows. */
|
|
6952
|
-
function findReview(groups, id) {
|
|
6953
|
-
for (const group of groups) for (const review of group.history) if (review.id === id) return review;
|
|
6954
|
-
}
|
|
6955
|
-
/**
|
|
6956
7067
|
* One section's Load more, which the browser shows once that section holds more than it is showing.
|
|
6957
7068
|
*
|
|
6958
7069
|
* One per section rather than one pager for the page, so revealing older Done reviews never costs
|
|
@@ -6963,15 +7074,17 @@ function renderLoadMore(section) {
|
|
|
6963
7074
|
return `<button type="button" class="prb-index-more" data-load-more="${section}" hidden>Load more</button>`;
|
|
6964
7075
|
}
|
|
6965
7076
|
/**
|
|
6966
|
-
*
|
|
7077
|
+
* One section, as a disclosure headed with its name and count.
|
|
6967
7078
|
*
|
|
6968
|
-
*
|
|
6969
|
-
*
|
|
6970
|
-
*
|
|
6971
|
-
* browser moves here after a mark
|
|
7079
|
+
* Every section is headed, Active included. Active used to go without, because the count line
|
|
7080
|
+
* directly above it said what it held; with Pinned drawn first, the count line sits over Pinned and
|
|
7081
|
+
* would be heading the wrong list. Everything starts open but Done, which is the archive. Rendered
|
|
7082
|
+
* empty and hidden rather than left out, so a card the browser moves here after a mark or a pin has
|
|
7083
|
+
* somewhere to go.
|
|
6972
7084
|
*/
|
|
6973
|
-
function renderSection(section,
|
|
6974
|
-
|
|
7085
|
+
function renderSection(section, groups, renderCards) {
|
|
7086
|
+
const title = section === "pinned" ? "Pinned" : LIFECYCLES.find((lifecycle) => lifecycle.id === section).name;
|
|
7087
|
+
return `<details class="prb-index-section" data-section="${section}"${section !== "done" ? " open" : ""}${groups.length === 0 ? " hidden" : ""}>
|
|
6975
7088
|
<summary class="prb-index-section-head">${title} · <span data-section-count>${groups.length}</span></summary>
|
|
6976
7089
|
<div class="prb-cards" data-cards="${section}">
|
|
6977
7090
|
${renderCards(groups)}
|
|
@@ -6998,7 +7111,7 @@ function renderBulkBar() {
|
|
|
6998
7111
|
<p class="prb-bulk-count" data-bulk-count aria-live="polite"></p>
|
|
6999
7112
|
<div class="prb-bulk-actions" data-bulk-actions>
|
|
7000
7113
|
<button type="button" class="prb-bulk-button" data-bulk-select-all>${escapeHtml$1(selectAllLabel(0))}</button>
|
|
7001
|
-
<button type="button" class="prb-bulk-button" data-bulk-fetch
|
|
7114
|
+
<button type="button" class="prb-bulk-button" data-bulk-fetch hidden>${escapeHtml$1(FETCH_SELECTED_LABEL)}</button>
|
|
7002
7115
|
<button type="button" class="prb-bulk-button" data-bulk-queue disabled>Add to review queue</button>
|
|
7003
7116
|
<button type="button" class="prb-bulk-button" data-bulk-mark="complete">Mark reviewed</button>
|
|
7004
7117
|
<button type="button" class="prb-bulk-button" data-bulk-mark="in-progress">Mark in progress</button>
|
|
@@ -7039,12 +7152,12 @@ function renderBulkBar() {
|
|
|
7039
7152
|
function renderCard(group, now, activity, rank, analysis, queued, moved, prState) {
|
|
7040
7153
|
const review = group.latest;
|
|
7041
7154
|
const status = statusOf(review);
|
|
7042
|
-
const chip =
|
|
7155
|
+
const chip = cardLifecycleChip(review.pinned === true, review.lifecycle, activity);
|
|
7043
7156
|
const href = `/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}`;
|
|
7044
7157
|
const followed = review.targetType === "commit" ? `commit ${(review.commitSha ?? review.branch).slice(0, 7)}` : followedRefName(review.branch, review.headResolvedRef) ?? review.branch;
|
|
7045
7158
|
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}"`}${prState === void 0 ? "" : ` data-pr-state="${prState}"`}${review.authorHandle ? ` data-author="${escapeHtml$1(review.authorHandle)}"` : ""} data-lifecycle="${review.lifecycle?.state ?? "active"}"${review.lifecycle ? ` data-lifecycle-at="${review.lifecycle.at}"` : ""} data-activity-at="${activity.at}" data-activity-kind="${activity.kind}" data-rank="${rank}" data-reviews="${group.history.length}" data-lineage="${escapeHtml$1(group.history.map((entry) => entry.id).join(" "))}" data-search-text="${escapeHtml$1(groupSearchText(group))}">
|
|
7046
7159
|
<input type="checkbox" class="prb-card-select" data-select="${escapeHtml$1(review.id)}" aria-label="Select ${escapeHtml$1(followed)}">
|
|
7047
|
-
<button type="button" class="prb-card-pin" data-pin="${escapeHtml$1(review.id)}" aria-pressed="${review.pinned}" aria-label="${review.pinned ? "
|
|
7160
|
+
<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">${review.pinned ? "★" : "☆"}</button>
|
|
7048
7161
|
${renderCardMenu(group, followed, queued !== void 0, status, activity)}
|
|
7049
7162
|
<a class="prb-card-link" href="${escapeHtml$1(href)}">
|
|
7050
7163
|
<div class="prb-card-head">
|
|
@@ -7107,7 +7220,8 @@ function renderCardMenu(group, followed, queued, status, activity) {
|
|
|
7107
7220
|
copy: "Copy review ID",
|
|
7108
7221
|
queue: queued ? "remove" : "add",
|
|
7109
7222
|
mark,
|
|
7110
|
-
lifecycle
|
|
7223
|
+
lifecycle,
|
|
7224
|
+
fetch: true
|
|
7111
7225
|
});
|
|
7112
7226
|
const all = `all ${count} reviews following ${followed}`;
|
|
7113
7227
|
return renderMenu({
|
|
@@ -7119,6 +7233,7 @@ function renderCardMenu(group, followed, queued, status, activity) {
|
|
|
7119
7233
|
queue: queued ? "remove" : "add",
|
|
7120
7234
|
mark,
|
|
7121
7235
|
lifecycle,
|
|
7236
|
+
fetch: true,
|
|
7122
7237
|
copy: "Copy latest review ID"
|
|
7123
7238
|
});
|
|
7124
7239
|
}
|
|
@@ -7137,6 +7252,7 @@ function renderCardMenu(group, followed, queued, status, activity) {
|
|
|
7137
7252
|
*/
|
|
7138
7253
|
function renderMenu(item) {
|
|
7139
7254
|
const id = escapeHtml$1(item.id);
|
|
7255
|
+
const fetchItem = item.fetch ? `<button type="button" class="prb-menu-item" data-fetch-review="${id}" hidden>${escapeHtml$1(FETCH_REVIEW_LABEL)}</button>` : "";
|
|
7140
7256
|
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>`;
|
|
7141
7257
|
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>`;
|
|
7142
7258
|
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 ");
|
|
@@ -7144,6 +7260,7 @@ function renderMenu(item) {
|
|
|
7144
7260
|
<button type="button" class="prb-menu-trigger" data-menu-trigger aria-expanded="false" aria-haspopup="true" aria-label="${escapeHtml$1(item.label)}">⋯</button>
|
|
7145
7261
|
<div class="prb-menu-panel" data-menu-panel hidden>
|
|
7146
7262
|
<div data-menu-items>
|
|
7263
|
+
${fetchItem}
|
|
7147
7264
|
${markItem}
|
|
7148
7265
|
${lifecycleItems}
|
|
7149
7266
|
${queueItem}
|
|
@@ -7902,6 +8019,10 @@ async function fetchTargetIndex(deps = defaultFetchDeps()) {
|
|
|
7902
8019
|
}
|
|
7903
8020
|
return targets;
|
|
7904
8021
|
}
|
|
8022
|
+
/** Whether one review is a fetch target: the review page's question about itself. */
|
|
8023
|
+
async function isFetchTarget(reviewId, deps = defaultFetchDeps()) {
|
|
8024
|
+
return (await fetchTargets(deps, /* @__PURE__ */ new Set([reviewId]))).eligible.has(reviewId);
|
|
8025
|
+
}
|
|
7905
8026
|
var inFlight = /* @__PURE__ */ new Map();
|
|
7906
8027
|
var queue = Promise.resolve();
|
|
7907
8028
|
/**
|
|
@@ -8362,7 +8483,8 @@ function extractAnswer(raw) {
|
|
|
8362
8483
|
* and wrongly.
|
|
8363
8484
|
*/
|
|
8364
8485
|
function buildPrompt$2(workspace, question, sectionId, cwd) {
|
|
8365
|
-
const
|
|
8486
|
+
const sections = workspace.session.result?.sections ?? [];
|
|
8487
|
+
const reading = readingThemeLine(sections, sectionId);
|
|
8366
8488
|
const recent = workspace.transcript.slice(-4);
|
|
8367
8489
|
return [
|
|
8368
8490
|
SYSTEM_PROMPT$1,
|
|
@@ -8374,29 +8496,19 @@ function buildPrompt$2(workspace, question, sectionId, cwd) {
|
|
|
8374
8496
|
"---",
|
|
8375
8497
|
"DIFF:",
|
|
8376
8498
|
workspace.diff,
|
|
8377
|
-
...
|
|
8499
|
+
...reading ? ["---", reading] : [],
|
|
8378
8500
|
...recent.length > 0 ? [
|
|
8379
8501
|
"---",
|
|
8380
8502
|
"EARLIER IN THIS CONVERSATION:",
|
|
8381
|
-
recent.map((entry) =>
|
|
8503
|
+
recent.map((entry) => {
|
|
8504
|
+
const where = askedWhere(sections, entry.sectionId);
|
|
8505
|
+
return `Q${where ? ` ${where}` : ""}: ${entry.question}\nA: ${entry.answer}`;
|
|
8506
|
+
}).join("\n\n")
|
|
8382
8507
|
] : [],
|
|
8383
8508
|
"---",
|
|
8384
8509
|
`QUESTION: ${question}`
|
|
8385
8510
|
].join("\n");
|
|
8386
8511
|
}
|
|
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
|
-
}
|
|
8400
8512
|
//#endregion
|
|
8401
8513
|
//#region ../../packages/review-harness/src/workspace/assistant_runs.ts
|
|
8402
8514
|
var RUN_KINDS = ["ask", "diagram"];
|
|
@@ -9429,6 +9541,7 @@ async function handle(req, res, options) {
|
|
|
9429
9541
|
if (action === "/job") return send(res, 200, jobReport(workspace));
|
|
9430
9542
|
if (action === "/retry") return handleRetry(res, workspace);
|
|
9431
9543
|
if (action === "/freshness") return handleFreshness(res, workspace, url.searchParams.get("quiet") === "1");
|
|
9544
|
+
if (action === "/fetch") return handleFetchOwn(req, res, workspace);
|
|
9432
9545
|
if (action === "/refresh") return handleRefreshPr(res, workspace);
|
|
9433
9546
|
if (action === "/update") return handleUpdate(res, workspace);
|
|
9434
9547
|
if (action === "/review") return send(res, 200, workspace.session.result ?? {});
|
|
@@ -9503,10 +9616,10 @@ function handleIndex(req, res, params, indexToken) {
|
|
|
9503
9616
|
const cookie = indexCookieHeader(indexToken);
|
|
9504
9617
|
if (onward) return redirect(res, onward, cookie);
|
|
9505
9618
|
const states = analysisStates();
|
|
9506
|
-
const queue = readQueue();
|
|
9507
9619
|
const reviews = recentWorkspaces(Number.MAX_SAFE_INTEGER);
|
|
9508
|
-
const kept = indexKeptIds(reviews, queue);
|
|
9509
9620
|
const everyGroup = groupByLineage(reviews);
|
|
9621
|
+
const queue = settleQueue(everyGroup);
|
|
9622
|
+
const kept = indexKeptIds(reviews, queue);
|
|
9510
9623
|
const activity = reviewActivity(everyGroup);
|
|
9511
9624
|
const groups = indexReviewGroups(MAX_LISTED_BRANCHES, MAX_KEPT_BRANCHES, kept, (group) => activity.get(group.latest.id)?.at ?? group.latest.createdAt, everyGroup);
|
|
9512
9625
|
const rendered = new Set(groups.flatMap((group) => group.history.map((review) => review.id)));
|
|
@@ -9967,6 +10080,19 @@ async function handleFetchUpdates(req, res, indexToken) {
|
|
|
9967
10080
|
send(res, 200, await runFetchUpdates(new Set(ids)));
|
|
9968
10081
|
}
|
|
9969
10082
|
/**
|
|
10083
|
+
* Fetch remote updates, from one review's own page, on its own token.
|
|
10084
|
+
*
|
|
10085
|
+
* The list's fetch scoped to this review: `GET` says whether it applies (the page draws the item
|
|
10086
|
+
* only if so, asked after load so rendering never waits on git), `POST` runs it. The review page's
|
|
10087
|
+
* Check for updates stays read-only; this is the one explicit act on that page that reaches the
|
|
10088
|
+
* network, and it writes remote-tracking refs and nothing else.
|
|
10089
|
+
*/
|
|
10090
|
+
async function handleFetchOwn(req, res, workspace) {
|
|
10091
|
+
if (req.method === "GET") return send(res, 200, { fetchable: await isFetchTarget(workspace.id) });
|
|
10092
|
+
if (req.method !== "POST") return send(res, 405, { error: "Method not allowed." });
|
|
10093
|
+
send(res, 200, await runFetchUpdates(/* @__PURE__ */ new Set([workspace.id])));
|
|
10094
|
+
}
|
|
10095
|
+
/**
|
|
9970
10096
|
* Pin or unpin a review.
|
|
9971
10097
|
*
|
|
9972
10098
|
* Authorised by the index credential rather than the review's own, because pinning is a fact about
|
|
@@ -10280,7 +10406,7 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
10280
10406
|
pendingDiagram: reportRun(workspace.id, "diagram"),
|
|
10281
10407
|
prompts: readPromptBank(),
|
|
10282
10408
|
complete: workspace.completedAt !== void 0,
|
|
10283
|
-
|
|
10409
|
+
pinned: workspace.pinned === true,
|
|
10284
10410
|
unreadNotifications: unreadCount$1(),
|
|
10285
10411
|
driving: isDriving(),
|
|
10286
10412
|
prContext: workspace.prContext ? {
|