prreviewbuddy 0.62.0 → 0.64.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 +73 -0
- package/dist/{delete_review-LzUgEUzN.js → delete_review-l4p_p6oh.js} +166 -41
- package/dist/main.js +147 -19
- package/dist/server.js +314 -131
- package/package.json +1 -1
- package/static/chunk-CXf6hwXL.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 +189 -22
- package/static/workspace.js +84 -78
- package/static/chunk-CxNfDBSA.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as storedScopes, $n as loadWorkspace, A as reanalyseReview, An as endedAt, Ar as issuesOutstanding, At as renderMarkdown, Br as isCommitReview, Bt as createWorktree, C as track, Cn as remoteHost, Cr as queueAddMany, Ct as unreadCount$1, D as endRun, Dn as followedBranch, Dr as doneVerb, Dt as authorPreview, E as completeRun, En as checkReview, Er as readQueue, Et as UNESTABLISHED_MOTIVATION, F as record, Fr as describeAuthorship, Ft as asString, G as describeRuntimes, Gn as groupByLineage, Gr as readAgentPreference, Gt as requestJobStop, H as mapRuntimes, Hn as isClaimed, Ht as isWorktreeHeld, In as isLiveJob, Ir as WITHHELD_REASON, It as extractJsonText, J as loadArchitecture, Jn as kissDecisionRefusal, Jr as STORE_ROOT, Jt as activateQueued, K as ARCHITECTURE_DIR, Kn as indexKeptIds, Kr as writeAgentPreference, Ln as phasesFor, Lr as kissOutcome, Lt as objectWithAnyOf, Mn as isTerminal, Mr as reviewerDispositions, Mt as CODE_COPY_CLASS, Nn as loadJob, Nr as processDiscussion, Nt as escapeHtml$1, O as reportRun, On as reviewedShaOf, Or as isIssueOutstanding, Ot as hasAuthorContent, P as readEvents, Pn as saveJob, Pr as recommendationsOf, Pt as renderInlineText, Q as storeArchitecture, Qn as lineagePosition, Qr as agentFor, Qt as readIndexToken, R as recordTelemetryUploadConsent, Rn as progressSteps, Rt as ReviewBeingDeletedError, S as isDriving, Sn as forgeFor, Sr as queueAdd, St as toneOf, T as beginRun, Tn as checkCodeFreshness, Tr as queueStamp, Tt as EXPLAIN_SIMPLY_PROMPT, U as recordRuntimeBestEffort, Un as decideKissRecommendation, Ur as resolveModel, Ut as readMarker, V as waitingHistory, Vr as git, Vt as holdWorktree, W as RUNTIME_WORDS, Wn as followedRefName, Wr as clearAgentPreference, Wt as removeWorktree, X as readArchitecture, Xn as latestKissRun, Y as partFiles, Yn as kissDecisionsOf, Yr as AGENT_IDS, Yt as compactQueue, Z as repositoryKey, Zt as settleQueue, _ as liveUpdateFor, _n as resolveWorkspacePort, _r as lifecycleItemLabel, _t as markRead, a as wasBlocked, an as spawnServer, ar as recentWorkspaces, at as isArchitectureId, bn as claimRequestUrl, br as parseLifecycle, bt as readNotification, c as RefreshUnavailableError, ct as branchOf, d as mergeUpdateInto, dr as workspaceStamps, dt as createPrState, ei as AgentCancelledError, en as clearServerRecordOwnedBy, et as trackedFiles, f as updateReview, fn as feedbackUrl, fr as LIFECYCLES, ft as createPrUpdate, g as UpdateAlreadyRunningError, gn as parseWorkspacePort, gr as lifecycleChip, gt as markAllRead, h as askCheckout, hn as MIN_WORKSPACE_PORT, hr as isLifecycle, ht as listNotifications, i as liveJobsFor, in as serverEntryExists, ir as previousKissResult, it as ancestorsOf, j as runJob, jn as fail, jr as questionsOutstanding, jt as CODE_BLOCK_CLASS, k as stopRun, kn as allJobs, kr as isQuestionOutstanding, kt as GITHUB_MARKDOWN, l as cancelUpdate, ln as writeServerRecord, lr as touchWorkspace, lt as subjectOf, m as isUnchanged, mn as MAX_WORKSPACE_PORT, mr as activityLabel, mt as headline, n as deleteReview, nn as readServerRecord, nt as QUOTE_LIMITS, o as discardJob, on as stopChild, or as reviewedCommit, ot as writeJsonAtomically, p as refreshPrContext, pn as DEFAULT_WORKSPACE_PORT, pr as activityAttributes, pt as eventName, q as checkArchitecture, qn as indexReviewGroups, qt as relativeTime$1, r as lineageIds, rr as presentedKissRunOf, rt as REPOSITORY_SCOPE, s as FETCH_TIMEOUT_MS, st as announceJob, t as deleteLineage, ti as AgentUnavailableError, tr as mutateWorkspace, tt as ownersOf, u as cancellableHere, un as BUILD_VERSION, ur as workspaceRevision, ut as createArchitectureNotification, vn as statedWorkspacePort, vr as lifecyclePill, vt as markReadForReview, w as RUN_KINDS, wn as fillFileUrlTemplate, wr as queueRemove, wt as withUsageRecorded, x as startKissJob, xr as sectionOf, xt as removeNotification, y as agentEnvOf, yn as writePortPreference, yr as offersLifecycle, yt as pruneNotifications, z as telemetryUploadConsent, zn as purposeOf, zt as MANAGED_ROOT } from "./delete_review-l4p_p6oh.js";
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -322,11 +322,35 @@ function countLabel(reviews, branches) {
|
|
|
322
322
|
return `${label} · ${branches} branch${branches === 1 ? "" : "es"}`;
|
|
323
323
|
}
|
|
324
324
|
/**
|
|
325
|
-
* Every login the author dropdown offers, once each, in alphabetical order
|
|
326
|
-
*
|
|
325
|
+
* Every login the author dropdown offers, once each, in alphabetical order of what the reader sees:
|
|
326
|
+
* the name where there is one, the login otherwise, regardless of case. Read from the cards the page
|
|
327
|
+
* is showing, so it never lists somebody with nothing to show.
|
|
328
|
+
*
|
|
329
|
+
* A login's name is the one on its newest review that has a name, chosen here by `createdAt` rather
|
|
330
|
+
* than by the order the caller happens to pass, so an account renamed upstream reads as it does now.
|
|
327
331
|
*/
|
|
328
|
-
function authorOptions(
|
|
329
|
-
|
|
332
|
+
function authorOptions(cards) {
|
|
333
|
+
const newest = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const card of cards) {
|
|
335
|
+
if (!card.handle) continue;
|
|
336
|
+
const seen = newest.get(card.handle);
|
|
337
|
+
if (!seen) newest.set(card.handle, {
|
|
338
|
+
name: card.name || void 0,
|
|
339
|
+
createdAt: card.createdAt
|
|
340
|
+
});
|
|
341
|
+
else if (card.name && (!seen.name || card.createdAt > seen.createdAt)) newest.set(card.handle, {
|
|
342
|
+
name: card.name,
|
|
343
|
+
createdAt: card.createdAt
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return [...newest].map(([handle, { name }]) => name && name !== handle ? {
|
|
347
|
+
handle,
|
|
348
|
+
name
|
|
349
|
+
} : { handle }).sort((a, b) => (a.name ?? a.handle).localeCompare(b.name ?? b.handle, void 0, { sensitivity: "base" }));
|
|
350
|
+
}
|
|
351
|
+
/** An author option as the dropdown spells it: the name and the login, or the login alone. */
|
|
352
|
+
function authorOptionLabel(option) {
|
|
353
|
+
return option.name ? `${option.name} · @${option.handle}` : `@${option.handle}`;
|
|
330
354
|
}
|
|
331
355
|
/**
|
|
332
356
|
* A card menu's status item, which sets the state it names. The bulk bar's words for the same two
|
|
@@ -334,7 +358,7 @@ function authorOptions(handles) {
|
|
|
334
358
|
* the browser relabels the item after a change, and that renderer reaches the store.
|
|
335
359
|
*/
|
|
336
360
|
function markItemLabel(mark) {
|
|
337
|
-
return mark === "complete" ? "Mark reviewed" : "Mark
|
|
361
|
+
return mark === "complete" ? "Mark reviewed" : "Mark not reviewed";
|
|
338
362
|
}
|
|
339
363
|
/**
|
|
340
364
|
* The review page's Pin item, which says what pressing it will do. The words of the index's star,
|
|
@@ -371,7 +395,7 @@ function queueOptionLabel(queued) {
|
|
|
371
395
|
*/
|
|
372
396
|
function statusLabel(status) {
|
|
373
397
|
if (status === "complete") return "Reviewed";
|
|
374
|
-
return status === "in-progress" ? "
|
|
398
|
+
return status === "in-progress" ? "Not reviewed" : "New";
|
|
375
399
|
}
|
|
376
400
|
/**
|
|
377
401
|
* The bulk bar's way to tick everything the filters show, across pages: "Select all 7".
|
|
@@ -422,6 +446,206 @@ function anythingRunning(states) {
|
|
|
422
446
|
var FETCH_REVIEW_LABEL = "Fetch remote updates";
|
|
423
447
|
var FETCH_SELECTED_LABEL = "Fetch selected";
|
|
424
448
|
//#endregion
|
|
449
|
+
//#region ../../packages/review-harness/src/domain/ui/components/message_copy.ts
|
|
450
|
+
var MESSAGE_COPY_CLASS = "pr-helper-msg-copy";
|
|
451
|
+
var MESSAGE_ACTIONS_CLASS = "pr-helper-chat-actions";
|
|
452
|
+
/** The Markdown itself, for surfaces that hold no message state to resolve an id against. */
|
|
453
|
+
var MESSAGE_SOURCE_ATTR = "data-copy-source";
|
|
454
|
+
var STATES$1 = {
|
|
455
|
+
idle: {
|
|
456
|
+
icon: "<path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"></path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"></path>",
|
|
457
|
+
label: "",
|
|
458
|
+
aria: "Copy message"
|
|
459
|
+
},
|
|
460
|
+
copied: {
|
|
461
|
+
icon: "<path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"></path>",
|
|
462
|
+
label: "Copied",
|
|
463
|
+
aria: "Copied"
|
|
464
|
+
},
|
|
465
|
+
failed: {
|
|
466
|
+
icon: "<path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path>",
|
|
467
|
+
label: "Copy failed",
|
|
468
|
+
aria: "Copy failed"
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* The same row, carrying the Markdown itself.
|
|
473
|
+
*
|
|
474
|
+
* For a surface with nothing to resolve an id against — the local workspace renders its stored
|
|
475
|
+
* transcript to HTML server-side and appends live answers straight to the DOM, so there is no
|
|
476
|
+
* conversation object on the client at all. Duplicating the source into the attribute is the
|
|
477
|
+
* honest trade there, and it is the same shape the prompt bank already uses to ship its data.
|
|
478
|
+
*/
|
|
479
|
+
function renderMessageCopySource(source) {
|
|
480
|
+
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`);
|
|
481
|
+
}
|
|
482
|
+
/** The class that places a finding's copy control in its card's heading, and gives it its tooltip. */
|
|
483
|
+
var FINDING_COPY_CLASS = "pr-helper-finding-copy";
|
|
484
|
+
/**
|
|
485
|
+
* The same control on an issue or question card, carrying that finding as Markdown.
|
|
486
|
+
*
|
|
487
|
+
* It sits in the card's heading, not its footer. It copies the whole finding, so the heading is
|
|
488
|
+
* the thing it acts on. And it is a different kind of act from Investigate, Dismiss and Mark
|
|
489
|
+
* answered, which change the review: those keep the footer to themselves, so the footer holds
|
|
490
|
+
* only actions that change something.
|
|
491
|
+
*
|
|
492
|
+
* Icon only at rest, like a chat reply's. It used to show a word because it sat in a row of worded
|
|
493
|
+
* buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
|
|
494
|
+
* would give a utility the weight of the title beside it. What it does is said by a hover
|
|
495
|
+
* tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
|
|
496
|
+
* accessible name. After a click it shows a green check and says "Copied" (or "Copy failed") in
|
|
497
|
+
* the bubble below it, the words used everywhere else, but not inline: beside it is Ask Buddy. The
|
|
498
|
+
* resting name and tip are kept on the button so the reset returns to "Copy finding" rather than to
|
|
499
|
+
* a chat reply's "Copy message".
|
|
500
|
+
*/
|
|
501
|
+
function renderFindingCopySource(source, kind) {
|
|
502
|
+
const name = kind === "issue" ? "Copy finding" : kind === "question" ? "Copy question" : "Copy recommendation";
|
|
503
|
+
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`, {
|
|
504
|
+
label: "",
|
|
505
|
+
aria: name,
|
|
506
|
+
tip: name
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The same control with its word showing, for a footer of worded actions.
|
|
511
|
+
*
|
|
512
|
+
* A KISS card's Copy draft comment sits among Plan to simplify and Leave it, where an icon alone
|
|
513
|
+
* would read as decoration. `extra` is for attributes the caller's own client needs, such as the
|
|
514
|
+
* handle Copy recommendations is found by; the source may be updated after render, since the
|
|
515
|
+
* listener reads it at the moment of the click.
|
|
516
|
+
*/
|
|
517
|
+
function renderLabelledCopySource(source, label, extra = "") {
|
|
518
|
+
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"${extra ? ` ${extra}` : ""}`, {
|
|
519
|
+
label,
|
|
520
|
+
aria: label,
|
|
521
|
+
tip: label
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
|
|
525
|
+
var IDLE_LABEL_ATTR = "data-copy-idle-label";
|
|
526
|
+
var IDLE_ARIA_ATTR = "data-copy-idle-aria";
|
|
527
|
+
var IDLE_TIP_ATTR = "data-copy-idle-tip";
|
|
528
|
+
function button(handle, idle) {
|
|
529
|
+
const { icon } = STATES$1.idle;
|
|
530
|
+
const label = idle?.label ?? STATES$1.idle.label;
|
|
531
|
+
const aria = idle?.aria ?? STATES$1.idle.aria;
|
|
532
|
+
const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}" ${IDLE_TIP_ATTR}="${escapeHtml$1(idle.tip)}"` : "";
|
|
533
|
+
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>`;
|
|
534
|
+
return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
|
|
535
|
+
}
|
|
536
|
+
function svg(paths) {
|
|
537
|
+
return `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">${paths}</svg>`;
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region ../../packages/review-harness/src/workspace/feedback_view.ts
|
|
541
|
+
/**
|
|
542
|
+
* Feedback selection: the reviewer picks the Issues and Questions they mean to raise with the
|
|
543
|
+
* author, across both panes, and takes them away as one Markdown draft.
|
|
544
|
+
*
|
|
545
|
+
* The words and the composition, in one module the server renders from and the client imports.
|
|
546
|
+
* The same split the inbox's bulk bar settled (`reviews_index.ts`): every sentence here carries a
|
|
547
|
+
* number only the browser knows, so the whole sentence lives where both sides can reach it, and
|
|
548
|
+
* where the copy style and vocabulary scans already look. Nothing here may import a node builtin.
|
|
549
|
+
*
|
|
550
|
+
* Each item is the text the card's own Copy button carries (`finding_copy.ts`), read from the card.
|
|
551
|
+
* There is one rendering of a finding as text, so what this exports, what Copy gives and what Ask
|
|
552
|
+
* Buddy is asked about can never disagree. That is also why no severity, theme or status appears:
|
|
553
|
+
* those sort the findings for the reviewer and are not part of what the author is told.
|
|
554
|
+
*/
|
|
555
|
+
function count(n, noun) {
|
|
556
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* The tray's count line, in its three parts: "5 chosen", "2 issues", "3 questions".
|
|
560
|
+
*
|
|
561
|
+
* Chosen rather than selected: the reviewer is choosing what to tell the author, not operating a
|
|
562
|
+
* list.
|
|
563
|
+
* "Nothing chosen" rather than "0 chosen", the inbox bar's reason: the mode opens in this
|
|
564
|
+
* state, and a zero reads as a count of something that happened. A side with nothing picked is
|
|
565
|
+
* empty, and left out, rather than said as a zero beside the side that has some. The tray draws
|
|
566
|
+
* the two sides as buttons back to their panes; everywhere else reads the joined sentence.
|
|
567
|
+
*/
|
|
568
|
+
function feedbackSelectionParts(issues, questions) {
|
|
569
|
+
const total = issues + questions;
|
|
570
|
+
return {
|
|
571
|
+
total: total === 0 ? "Nothing chosen" : `${total} chosen`,
|
|
572
|
+
issues: issues ? count(issues, "issue") : "",
|
|
573
|
+
questions: questions ? count(questions, "question") : ""
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** "5 chosen · 2 issues · 3 questions", "1 chosen · 1 question", "Nothing chosen". */
|
|
577
|
+
function feedbackSelectionLabel(issues, questions) {
|
|
578
|
+
const parts = feedbackSelectionParts(issues, questions);
|
|
579
|
+
return [
|
|
580
|
+
parts.total,
|
|
581
|
+
parts.issues,
|
|
582
|
+
parts.questions
|
|
583
|
+
].filter(Boolean).join(" · ");
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The pane's way into the mode, and out of it. Named for what it is for rather than for the
|
|
587
|
+
* mechanism ("Select"). It shares its name with the tray's last step on purpose, and the two are
|
|
588
|
+
* never on screen together: pressed, this one reads Done with a tick, the same act as the tray's
|
|
589
|
+
* Done, and that is when the tray appears. A checklist, not a sparkle, because nothing here is AI.
|
|
590
|
+
* Both faces are in the markup and the stylesheet shows the one `aria-pressed` calls for, so the
|
|
591
|
+
* hidden one is not part of the name a screen reader hears.
|
|
592
|
+
*/
|
|
593
|
+
var PREPARE_LABEL = "Prepare feedback";
|
|
594
|
+
var DONE_LABEL = "Done";
|
|
595
|
+
var ICON = (paths) => `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
596
|
+
var LIST_CHECK = ICON("<path d=\"m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8\"/>");
|
|
597
|
+
var CHECK = ICON("<path d=\"M20 6 9 17l-5-5\"/>");
|
|
598
|
+
function renderFeedbackSelectButton() {
|
|
599
|
+
return `<button type="button" class="prb-selection-toggle" data-feedback-select aria-pressed="false"><span class="prb-selection-toggle-idle">${LIST_CHECK}${PREPARE_LABEL}</span><span class="prb-selection-toggle-active">${CHECK}${DONE_LABEL}</span></button>`;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The tray, at the foot of the document pane on every view while the mode is on. The mode belongs
|
|
603
|
+
* to the review rather than to a pane, so leaving Issues does not take the tray with it, and its
|
|
604
|
+
* counts are the way back to each pane.
|
|
605
|
+
*/
|
|
606
|
+
function renderFeedbackTray() {
|
|
607
|
+
return `<div class="prb-selection-tray" data-feedback-tray role="region" aria-label="Chosen for feedback" hidden>
|
|
608
|
+
<p class="prb-selection-count" aria-live="polite">
|
|
609
|
+
<span data-feedback-total>${escapeHtml$1(feedbackSelectionLabel(0, 0))}</span>${renderGoto("issues")}${renderGoto("questions")}
|
|
610
|
+
</p>
|
|
611
|
+
<span class="prb-selection-spacer"></span>
|
|
612
|
+
<button type="button" class="prb-selection-button" data-feedback-clear>Clear</button>
|
|
613
|
+
<button type="button" class="prb-selection-button" data-feedback-done>${DONE_LABEL}</button>
|
|
614
|
+
<button type="button" class="prb-selection-button prb-selection-primary" data-feedback-prepare disabled>${PREPARE_LABEL}</button>
|
|
615
|
+
</div>`;
|
|
616
|
+
}
|
|
617
|
+
/** A side's count, which is also the way back to its pane. The dot is text, so the line reads whole. */
|
|
618
|
+
function renderGoto(pane) {
|
|
619
|
+
return `<button type="button" class="prb-selection-goto" data-feedback-goto="${pane}" hidden><span class="prb-selection-sep" aria-hidden="true"> · </span><span data-feedback-goto-label></span></button>`;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Where the draft is read, edited and copied.
|
|
623
|
+
*
|
|
624
|
+
* Two views of one draft, as tabs, because that is what they are: Preview, the default, for reading
|
|
625
|
+
* what the author will see, and Markdown for changing it. Both are drawn from the same text, the
|
|
626
|
+
* text area's, so an edit is in the preview the moment the tab changes, and Copy takes exactly what
|
|
627
|
+
* is in the box: the client rewrites the button's source on every keystroke.
|
|
628
|
+
*/
|
|
629
|
+
function renderFeedbackDialog() {
|
|
630
|
+
const tab = (name, label, selected) => `<button type="button" role="tab" class="prb-draft-tab" id="prb-draft-tab-${name}" data-draft-tab="${name}" aria-controls="prb-draft-${name}" aria-selected="${selected}" tabindex="${selected ? 0 : -1}">${label}</button>`;
|
|
631
|
+
return `<dialog class="prb-dialog prb-draft-dialog" data-feedback-dialog aria-labelledby="prb-feedback-title">
|
|
632
|
+
<div class="prb-draft-head">
|
|
633
|
+
<h2 id="prb-feedback-title">Prepare feedback</h2>
|
|
634
|
+
<p class="prb-draft-count" data-feedback-dialog-count></p>
|
|
635
|
+
</div>
|
|
636
|
+
<div class="prb-draft-tabs" role="tablist" aria-label="Draft">${tab("preview", "Preview", true)}${tab("markdown", "Markdown", false)}</div>
|
|
637
|
+
<div class="prb-draft-panel prb-draft-preview prb-md" id="prb-draft-preview" role="tabpanel" aria-labelledby="prb-draft-tab-preview" tabindex="0" data-feedback-preview></div>
|
|
638
|
+
<div class="prb-draft-panel" id="prb-draft-markdown" role="tabpanel" aria-labelledby="prb-draft-tab-markdown" hidden>
|
|
639
|
+
<textarea class="prb-draft-text" data-feedback-text aria-label="Markdown for the author" spellcheck="true" autocomplete="off"></textarea>
|
|
640
|
+
</div>
|
|
641
|
+
<div class="prb-dialog-actions">
|
|
642
|
+
<span class="prb-dialog-spacer"></span>
|
|
643
|
+
<button type="button" data-feedback-close>Close</button>
|
|
644
|
+
${renderLabelledCopySource("", "Copy Markdown", "data-feedback-copy")}
|
|
645
|
+
</div>
|
|
646
|
+
</dialog>`;
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
425
649
|
//#region ../../packages/review-harness/src/workspace/way_back.ts
|
|
426
650
|
var CLI = {
|
|
427
651
|
command: "prreviewbuddy open",
|
|
@@ -842,97 +1066,6 @@ function deriveReviewMetrics(result) {
|
|
|
842
1066
|
};
|
|
843
1067
|
}
|
|
844
1068
|
//#endregion
|
|
845
|
-
//#region ../../packages/review-harness/src/domain/ui/components/message_copy.ts
|
|
846
|
-
var MESSAGE_COPY_CLASS = "pr-helper-msg-copy";
|
|
847
|
-
var MESSAGE_ACTIONS_CLASS = "pr-helper-chat-actions";
|
|
848
|
-
/** The Markdown itself, for surfaces that hold no message state to resolve an id against. */
|
|
849
|
-
var MESSAGE_SOURCE_ATTR = "data-copy-source";
|
|
850
|
-
var STATES$1 = {
|
|
851
|
-
idle: {
|
|
852
|
-
icon: "<path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"></path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"></path>",
|
|
853
|
-
label: "",
|
|
854
|
-
aria: "Copy message"
|
|
855
|
-
},
|
|
856
|
-
copied: {
|
|
857
|
-
icon: "<path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z\"></path>",
|
|
858
|
-
label: "Copied",
|
|
859
|
-
aria: "Copied"
|
|
860
|
-
},
|
|
861
|
-
failed: {
|
|
862
|
-
icon: "<path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path>",
|
|
863
|
-
label: "Copy failed",
|
|
864
|
-
aria: "Copy failed"
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
/**
|
|
868
|
-
* The same row, carrying the Markdown itself.
|
|
869
|
-
*
|
|
870
|
-
* For a surface with nothing to resolve an id against — the local workspace renders its stored
|
|
871
|
-
* transcript to HTML server-side and appends live answers straight to the DOM, so there is no
|
|
872
|
-
* conversation object on the client at all. Duplicating the source into the attribute is the
|
|
873
|
-
* honest trade there, and it is the same shape the prompt bank already uses to ship its data.
|
|
874
|
-
*/
|
|
875
|
-
function renderMessageCopySource(source) {
|
|
876
|
-
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`);
|
|
877
|
-
}
|
|
878
|
-
/** The class that places a finding's copy control in its card's heading, and gives it its tooltip. */
|
|
879
|
-
var FINDING_COPY_CLASS = "pr-helper-finding-copy";
|
|
880
|
-
/**
|
|
881
|
-
* The same control on an issue or question card, carrying that finding as Markdown.
|
|
882
|
-
*
|
|
883
|
-
* It sits in the card's heading, not its footer. It copies the whole finding, so the heading is
|
|
884
|
-
* the thing it acts on. And it is a different kind of act from Investigate, Dismiss and Mark
|
|
885
|
-
* answered, which change the review: those keep the footer to themselves, so the footer holds
|
|
886
|
-
* only actions that change something.
|
|
887
|
-
*
|
|
888
|
-
* Icon only at rest, like a chat reply's. It used to show a word because it sat in a row of worded
|
|
889
|
-
* buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
|
|
890
|
-
* would give a utility the weight of the title beside it. What it does is said by a hover
|
|
891
|
-
* tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
|
|
892
|
-
* accessible name. After a click it shows a green check and says "Copied" (or "Copy failed") in
|
|
893
|
-
* the bubble below it, the words used everywhere else, but not inline: beside it is Ask Buddy. The
|
|
894
|
-
* resting name and tip are kept on the button so the reset returns to "Copy finding" rather than to
|
|
895
|
-
* a chat reply's "Copy message".
|
|
896
|
-
*/
|
|
897
|
-
function renderFindingCopySource(source, kind) {
|
|
898
|
-
const name = kind === "issue" ? "Copy finding" : kind === "question" ? "Copy question" : "Copy recommendation";
|
|
899
|
-
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`, {
|
|
900
|
-
label: "",
|
|
901
|
-
aria: name,
|
|
902
|
-
tip: name
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* The same control with its word showing, for a footer of worded actions.
|
|
907
|
-
*
|
|
908
|
-
* A KISS card's Copy draft comment sits among Plan to simplify and Leave it, where an icon alone
|
|
909
|
-
* would read as decoration. `extra` is for attributes the caller's own client needs, such as the
|
|
910
|
-
* handle Copy recommendations is found by; the source may be updated after render, since the
|
|
911
|
-
* listener reads it at the moment of the click.
|
|
912
|
-
*/
|
|
913
|
-
function renderLabelledCopySource(source, label, extra = "") {
|
|
914
|
-
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"${extra ? ` ${extra}` : ""}`, {
|
|
915
|
-
label,
|
|
916
|
-
aria: label,
|
|
917
|
-
tip: label
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
/** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
|
|
921
|
-
var IDLE_LABEL_ATTR = "data-copy-idle-label";
|
|
922
|
-
var IDLE_ARIA_ATTR = "data-copy-idle-aria";
|
|
923
|
-
var IDLE_TIP_ATTR = "data-copy-idle-tip";
|
|
924
|
-
function button(handle, idle) {
|
|
925
|
-
const { icon } = STATES$1.idle;
|
|
926
|
-
const label = idle?.label ?? STATES$1.idle.label;
|
|
927
|
-
const aria = idle?.aria ?? STATES$1.idle.aria;
|
|
928
|
-
const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}" ${IDLE_TIP_ATTR}="${escapeHtml$1(idle.tip)}"` : "";
|
|
929
|
-
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>`;
|
|
930
|
-
return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
|
|
931
|
-
}
|
|
932
|
-
function svg(paths) {
|
|
933
|
-
return `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">${paths}</svg>`;
|
|
934
|
-
}
|
|
935
|
-
//#endregion
|
|
936
1069
|
//#region ../../packages/review-harness/src/domain/ui/components/ask_buddy_menu.ts
|
|
937
1070
|
/**
|
|
938
1071
|
* Ask Buddy on a finding card: a sparkle beside Copy that opens the assistant's actions for the
|
|
@@ -2397,6 +2530,7 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
2397
2530
|
<div class="pr-helper-issue-card pr-helper-severity-${issue.severity}${issue.reconciliationStatus ? ` pr-helper-issue-${issue.reconciliationStatus}` : ""}${issue.investigationImpact ? ` pr-helper-issue-investigation-${issue.investigationImpact}` : ""}${isDismissed && !stale ? " pr-helper-issue-card-dismissed" : ""}" data-finding-card="${escapeHtml$1(issue.id)}">
|
|
2398
2531
|
<div class="pr-helper-issue-header">
|
|
2399
2532
|
<div class="pr-helper-issue-title-row">
|
|
2533
|
+
${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title) : ""}
|
|
2400
2534
|
<p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
|
|
2401
2535
|
${renderFindingCopySource(issueMarkdown(issue), "issue")}
|
|
2402
2536
|
${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
|
|
@@ -2499,6 +2633,7 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2499
2633
|
<div class="pr-helper-question-card${answered ? " pr-helper-question-card-answered" : ""}${q.investigationImpact ? ` pr-helper-issue-investigation-${q.investigationImpact}` : ""}" data-finding-card="${escapeHtml$1(q.id)}"${questionHome(q) ? ` data-section-id="${escapeHtml$1(questionHome(q))}"` : ""}>
|
|
2500
2634
|
<div class="pr-helper-issue-header">
|
|
2501
2635
|
<div class="pr-helper-issue-title-row">
|
|
2636
|
+
${options?.feedbackSelect ? renderFeedbackPick("question", q.question) : ""}
|
|
2502
2637
|
<p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
|
|
2503
2638
|
${renderFindingCopySource(questionMarkdown(q), "question")}
|
|
2504
2639
|
${options?.askBuddy ? renderAskBuddyMenu("question") : ""}
|
|
@@ -2550,6 +2685,17 @@ function renderQuestionsProgress(questions, answered) {
|
|
|
2550
2685
|
if (counts.total < 2) return "";
|
|
2551
2686
|
return `<div class="pr-helper-questions-progress" role="status">${questionsProgressBody(counts.total, counts.open)}</div>`;
|
|
2552
2687
|
}
|
|
2688
|
+
/**
|
|
2689
|
+
* The card's feedback checkbox. Named by the finding, so a list of them is not one word announced
|
|
2690
|
+
* fifteen times. The id is the card's own `data-finding-card`.
|
|
2691
|
+
*
|
|
2692
|
+
* `autocomplete="off"` because the browser otherwise restores a box's tick when the page is loaded
|
|
2693
|
+
* again from history, and the selection it belonged to went with the page that made it: Back to a
|
|
2694
|
+
* review showed a ticked box that nothing had selected.
|
|
2695
|
+
*/
|
|
2696
|
+
function renderFeedbackPick(kind, title) {
|
|
2697
|
+
return `<input type="checkbox" class="pr-helper-feedback-pick" data-feedback-pick="${kind}" autocomplete="off" aria-label="${escapeHtml$1(`Include in feedback: ${title.trim()}`)}">`;
|
|
2698
|
+
}
|
|
2553
2699
|
function renderQuestionJump(file, options) {
|
|
2554
2700
|
if (!file || options?.suppressFileJump) return "";
|
|
2555
2701
|
return `<button type="button" class="pr-helper-question-jump" data-file="${escapeHtml$1(file)}">Jump to file →</button>`;
|
|
@@ -5441,6 +5587,7 @@ ${updating(view) ? "" : renderUpdateOutcome(view.lastUpdate)}
|
|
|
5441
5587
|
${updating(view) ? "" : renderLineageNotice(view)}
|
|
5442
5588
|
${renderReviewBody(view, sections)}
|
|
5443
5589
|
${view.token ? renderPromptDialog() : ""}
|
|
5590
|
+
${offersFeedback(view) ? renderFeedbackDialog() : ""}
|
|
5444
5591
|
<script type="module" src="/assets/workspace.js"><\/script>
|
|
5445
5592
|
</body>
|
|
5446
5593
|
</html>`;
|
|
@@ -5498,12 +5645,22 @@ function renderReviewBody(view, sections) {
|
|
|
5498
5645
|
${renderRail(view, sections, updating(view))}
|
|
5499
5646
|
<main class="prb-doc">
|
|
5500
5647
|
${updating(view) ? renderUpdatingDoc(view) : renderReviewDoc(view, sections)}
|
|
5648
|
+
${offersFeedback(view) ? renderFeedbackTray() : ""}
|
|
5501
5649
|
</main>
|
|
5502
5650
|
${result ? renderAsk(view, sections, updating(view)) : ""}
|
|
5503
5651
|
${result ? renderAskTab() : ""}
|
|
5504
5652
|
</div>`;
|
|
5505
5653
|
}
|
|
5506
5654
|
/**
|
|
5655
|
+
* Whether the reviewer can choose findings to raise with the author: a standard result with at least
|
|
5656
|
+
* one issue or question, and no update running over it. Select, the tray and the dialog are drawn
|
|
5657
|
+
* together or not at all.
|
|
5658
|
+
*/
|
|
5659
|
+
function offersFeedback(view) {
|
|
5660
|
+
const result = view.session.result;
|
|
5661
|
+
return Boolean(result && (result.issues.length > 0 || result.questions.length > 0)) && !updating(view);
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5507
5664
|
* Buddy AI's own way in: a tab on the window's right edge, where its pane opens.
|
|
5508
5665
|
*
|
|
5509
5666
|
* Out of the header, which says where the review stands; the assistant is a tool beside the review,
|
|
@@ -5658,15 +5815,19 @@ function renderStandardViews(view, sections) {
|
|
|
5658
5815
|
${renderDiagrams(view.diagrams ?? [], sections, view.reviewedSha ?? null)}
|
|
5659
5816
|
</section>
|
|
5660
5817
|
<section class="prb-view" data-view="issues" hidden>
|
|
5661
|
-
${renderDocHead("Issues", renderFocusToggle("issues", "Issues"))}
|
|
5818
|
+
${renderDocHead("Issues", `${(result?.issues.length ?? 0) > 0 ? renderFeedbackSelectButton() : ""}${renderFocusToggle("issues", "Issues")}`)}
|
|
5662
5819
|
${renderAttributionAge(view)}
|
|
5663
|
-
${renderIssuesTab(session, null, {
|
|
5820
|
+
${renderIssuesTab(session, null, {
|
|
5821
|
+
askBuddy: true,
|
|
5822
|
+
feedbackSelect: true
|
|
5823
|
+
})}
|
|
5664
5824
|
</section>
|
|
5665
5825
|
<section class="prb-view" data-view="questions" hidden>
|
|
5666
|
-
${renderDocHead("Questions", renderFocusToggle("questions", "Questions"))}
|
|
5826
|
+
${renderDocHead("Questions", `${(result?.questions.length ?? 0) > 0 ? renderFeedbackSelectButton() : ""}${renderFocusToggle("questions", "Questions")}`)}
|
|
5667
5827
|
${renderQuestionsTab(session, null, {
|
|
5668
5828
|
suppressFileJump: true,
|
|
5669
|
-
askBuddy: true
|
|
5829
|
+
askBuddy: true,
|
|
5830
|
+
feedbackSelect: true
|
|
5670
5831
|
})}
|
|
5671
5832
|
</section>
|
|
5672
5833
|
<section class="prb-view" data-view="prcontext" hidden>
|
|
@@ -5763,7 +5924,7 @@ function renderLineageNotice(view) {
|
|
|
5763
5924
|
* Two facts per row and no more. The commit is the one that does the real work, because two
|
|
5764
5925
|
* analyses an hour apart are told apart by what they were of rather than by when they happened;
|
|
5765
5926
|
* the risk says whether the newer one found something worse, which is the reason to go and read
|
|
5766
|
-
* it. There is deliberately no `New` / `
|
|
5927
|
+
* it. There is deliberately no `New` / `Not reviewed` / `Reviewed` here: those describe where
|
|
5767
5928
|
* somebody is in a piece of work, and on a superseded review that is a claim about work nobody
|
|
5768
5929
|
* should be continuing. The index's history rows withhold it for the same reason.
|
|
5769
5930
|
*/
|
|
@@ -5894,7 +6055,7 @@ function renderReviewBar(view, freshness) {
|
|
|
5894
6055
|
/**
|
|
5895
6056
|
* Where this review stands, as pills that say and never do.
|
|
5896
6057
|
*
|
|
5897
|
-
* Two, because they answer two questions. Reviewed or
|
|
6058
|
+
* Two, because they answer two questions. Reviewed or Not reviewed is what the reviewer has done;
|
|
5898
6059
|
* Waiting or Done is what happens next with the branch. A review can be Reviewed and Waiting, so a
|
|
5899
6060
|
* single pill would have to drop one of them. Active draws nothing, as an open pull request does
|
|
5900
6061
|
* not: it is where every review starts, and a pill that never varies teaches nothing.
|
|
@@ -6047,8 +6208,8 @@ function renderRail(view, sections, stale = false) {
|
|
|
6047
6208
|
${themes || "<p class=\"prb-rail-heading\">No themes</p>"}
|
|
6048
6209
|
</div>
|
|
6049
6210
|
${railLink("map", "diagrams", "Diagrams", diagramsBadge(diagramCount(view)), void 0, "diagram architecture")}
|
|
6050
|
-
${railLink("issues", "issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)))}
|
|
6051
|
-
${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered), "question", "answered"))}
|
|
6211
|
+
${railLink("issues", "issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)), void 0, void 0, true)}
|
|
6212
|
+
${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered), "question", "answered"), void 0, void 0, true)}
|
|
6052
6213
|
${railLink("files", "files", "Files", totalBadge(view.changeSet.data.files.length, "files"))}
|
|
6053
6214
|
${view.prContext ? railLink("prcontext", "prcontext", "PR context", totalBadge(view.prContext.processed.stats.totalThreads, "PR comment threads")) : railLink("prcontext", "prcontext", "PR context", null, "No pull request was connected to this review")}`
|
|
6054
6215
|
}) : "";
|
|
@@ -6255,10 +6416,15 @@ function renderBadge(badge) {
|
|
|
6255
6416
|
return `<span class="prb-rail-count${badge.done ? " prb-rail-count-done" : ""}" aria-label="${escapeHtml$1(badge.label)}">${escapeHtml$1(badge.text)}</span>`;
|
|
6256
6417
|
}
|
|
6257
6418
|
/** A null badge renders nothing, for the case where zero would be a claim rather than a total. */
|
|
6258
|
-
|
|
6419
|
+
/**
|
|
6420
|
+
* `selectable` leaves room for the feedback selection's quiet "2 selected" under the title, empty and
|
|
6421
|
+
* hidden until the client has something to say there.
|
|
6422
|
+
*/
|
|
6423
|
+
function railLink(view, icon, label, badge, note, alsoCurrentOn, selectable = false) {
|
|
6424
|
+
const selected = selectable ? `<span class="prb-rail-selected" data-rail-selected="${view}" hidden></span>` : "";
|
|
6259
6425
|
return `<a class="prb-rail-item prb-rail-link" href="${escapeHtml$1(hashForView(view))}" data-view-nav="${view}"${alsoCurrentOn ? ` data-view-also="${alsoCurrentOn}"` : ""}${note ? ` title="${escapeHtml$1(note)}"` : ""}>
|
|
6260
6426
|
${railIcon(icon)}
|
|
6261
|
-
<span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(label)}</span
|
|
6427
|
+
<span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(label)}</span>${selected}</span>
|
|
6262
6428
|
${renderBadge(badge)}
|
|
6263
6429
|
</a>`;
|
|
6264
6430
|
}
|
|
@@ -7427,7 +7593,7 @@ async function fetchHead(cwd, request, env = {}) {
|
|
|
7427
7593
|
* What each pull request's forge last said about whether it is still open.
|
|
7428
7594
|
*
|
|
7429
7595
|
* **A fact about the request, never about the review.** A review can be `Reviewed` while its PR is
|
|
7430
|
-
* open, and `
|
|
7596
|
+
* open, and `Not reviewed` while its PR has merged; the reviewer's decision lives on the workspace
|
|
7431
7597
|
* record as `completedAt`, and nothing here reads or writes it.
|
|
7432
7598
|
*
|
|
7433
7599
|
* **Its own file, not a field of `watch.json`**, because the two have different lives. `watch.json`
|
|
@@ -8775,7 +8941,11 @@ ${renderGlobalBar({
|
|
|
8775
8941
|
unread: view.unreadNotifications ?? 0,
|
|
8776
8942
|
driving: view.driving
|
|
8777
8943
|
})}
|
|
8778
|
-
${renderContextBar("Reviews", ordered.length === 0 ? "" : renderControls(queue.length, authorOptions(ordered.map((
|
|
8944
|
+
${renderContextBar("Reviews", ordered.length === 0 ? "" : renderControls(queue.length, authorOptions(ordered.map(({ latest }) => ({
|
|
8945
|
+
handle: latest.authorHandle,
|
|
8946
|
+
name: latest.authorName,
|
|
8947
|
+
createdAt: latest.createdAt
|
|
8948
|
+
}))), ordered.some((group) => group.latest.prNumber !== void 0)))}
|
|
8779
8949
|
<div class="prb-shell">
|
|
8780
8950
|
${renderWorkspaceRail({
|
|
8781
8951
|
current: "reviews",
|
|
@@ -8801,19 +8971,19 @@ ${renderWorkspaceRail({
|
|
|
8801
8971
|
*
|
|
8802
8972
|
* Search covers everything a person might remember about a review — the repository, the branch,
|
|
8803
8973
|
* the title, the model's summary, and the paths it touched — because which of those they recall
|
|
8804
|
-
* is not predictable. The
|
|
8805
|
-
* would be four more controls for a question ("show me only the risky ones") that
|
|
8806
|
-
* recency already answers badly and that scanning answers well.
|
|
8974
|
+
* is not predictable. The review state filter is the reviewer's own say and nothing else; a filter
|
|
8975
|
+
* per risk level would be four more controls for a question ("show me only the risky ones") that
|
|
8976
|
+
* sorting by recency already answers badly and that scanning answers well.
|
|
8807
8977
|
*/
|
|
8808
8978
|
function renderControls(queued, authors, anyRequest) {
|
|
8809
8979
|
return `<div class="prb-index-controls">
|
|
8810
8980
|
<input type="search" class="prb-index-search" data-search placeholder="Search repo, branch, title, summary or file…" aria-label="Search reviews" spellcheck="false">
|
|
8811
8981
|
${renderAuthorFilter(authors)}
|
|
8812
|
-
<label class="prb-index-status">
|
|
8813
|
-
<select data-status-filter aria-label="Filter by
|
|
8982
|
+
<label class="prb-index-status">Review state:
|
|
8983
|
+
<select data-status-filter aria-label="Filter by review state">
|
|
8814
8984
|
<option value="all">All</option>
|
|
8815
8985
|
<option value="queue" data-queue-option>${escapeHtml$1(queueOptionLabel(queued))}</option>
|
|
8816
|
-
<option value="in-progress">
|
|
8986
|
+
<option value="in-progress">Not reviewed</option>
|
|
8817
8987
|
<option value="complete">Reviewed</option>
|
|
8818
8988
|
</select>
|
|
8819
8989
|
</label>
|
|
@@ -8821,13 +8991,13 @@ function renderControls(queued, authors, anyRequest) {
|
|
|
8821
8991
|
</div>`;
|
|
8822
8992
|
}
|
|
8823
8993
|
/**
|
|
8824
|
-
* The pull request's state upstream, beside
|
|
8994
|
+
* The pull request's state upstream, beside Review state and not inside it, so the two combine.
|
|
8825
8995
|
*
|
|
8826
8996
|
* Absent when no card is of a pull request, for the author filter's reason: a select whose every
|
|
8827
8997
|
* choice but Any shows nothing is not a choice.
|
|
8828
8998
|
*/
|
|
8829
8999
|
function renderPrFilter() {
|
|
8830
|
-
return `<label class="prb-index-status"
|
|
9000
|
+
return `<label class="prb-index-status prb-index-forge">${GITHUB_MARK}PR:
|
|
8831
9001
|
<select data-pr-filter aria-label="Filter by pull request state">${PR_FILTER_OPTIONS.map((option) => `<option value="${option.value}">${escapeHtml$1(option.label)}</option>`).join("")}</select>
|
|
8832
9002
|
</label>`;
|
|
8833
9003
|
}
|
|
@@ -8837,16 +9007,25 @@ function prStateOf(review, states) {
|
|
|
8837
9007
|
return states?.get(requestKey(review.repoPath, review.prNumber));
|
|
8838
9008
|
}
|
|
8839
9009
|
/**
|
|
8840
|
-
*
|
|
9010
|
+
* Marks the filters whose answers come from GitHub, Author and PR, so the mark means one thing.
|
|
9011
|
+
* Review state is the reviewer's own and goes without. Hidden from assistive technology: each
|
|
9012
|
+
* select's label already names it.
|
|
9013
|
+
*/
|
|
9014
|
+
var GITHUB_MARK = "<svg class=\"prb-index-forge-mark\" viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" focusable=\"false\" aria-hidden=\"true\" fill=\"currentColor\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\"/></svg>";
|
|
9015
|
+
/**
|
|
9016
|
+
* Whose pull requests to show, by name where GitHub gave one. The login is still what it filters
|
|
9017
|
+
* by, and is kept beside the name because two people can share a name and nobody shares a login.
|
|
8841
9018
|
*
|
|
8842
9019
|
* Absent when no card carries a login, which is every page of local-branch reviews: a dropdown
|
|
8843
9020
|
* whose only choice is Everyone is not a choice.
|
|
8844
9021
|
*/
|
|
8845
9022
|
function renderAuthorFilter(authors) {
|
|
8846
9023
|
if (authors.length === 0) return "";
|
|
8847
|
-
return `<
|
|
8848
|
-
<
|
|
8849
|
-
|
|
9024
|
+
return `<label class="prb-index-status prb-index-forge">${GITHUB_MARK}Author:
|
|
9025
|
+
<select name="author" data-author-filter aria-label="Filter by pull request author">
|
|
9026
|
+
<option value="">Everyone</option>${authors.map((author) => `<option value="${escapeHtml$1(author.handle)}">${escapeHtml$1(authorOptionLabel(author))}</option>`).join("")}
|
|
9027
|
+
</select>
|
|
9028
|
+
</label>`;
|
|
8850
9029
|
}
|
|
8851
9030
|
/**
|
|
8852
9031
|
* One section's Load more, which the browser shows once that section holds more than it is showing.
|
|
@@ -8899,8 +9078,8 @@ function renderBulkBar() {
|
|
|
8899
9078
|
<button type="button" class="prb-bulk-button" data-bulk-select-all>${escapeHtml$1(selectAllLabel(0))}</button>
|
|
8900
9079
|
<button type="button" class="prb-bulk-button" data-bulk-fetch hidden>${escapeHtml$1(FETCH_SELECTED_LABEL)}</button>
|
|
8901
9080
|
<button type="button" class="prb-bulk-button" data-bulk-queue disabled>Add to review queue</button>
|
|
8902
|
-
<button type="button" class="prb-bulk-button" data-bulk-mark="complete"
|
|
8903
|
-
<button type="button" class="prb-bulk-button" data-bulk-mark="in-progress"
|
|
9081
|
+
<button type="button" class="prb-bulk-button" data-bulk-mark="complete">${markItemLabel("complete")}</button>
|
|
9082
|
+
<button type="button" class="prb-bulk-button" data-bulk-mark="in-progress">${markItemLabel("in-progress")}</button>
|
|
8904
9083
|
${LIFECYCLES.map((lifecycle) => `<button type="button" class="prb-bulk-button" data-bulk-lifecycle="${lifecycle.id}">${escapeHtml$1(lifecycle.action)}</button>`).join("\n ")}
|
|
8905
9084
|
<button type="button" class="prb-bulk-button prb-bulk-button-danger" data-bulk-delete></button>
|
|
8906
9085
|
<button type="button" class="prb-bulk-button prb-bulk-cancel" data-bulk-cancel>Cancel</button>
|
|
@@ -9096,9 +9275,9 @@ function renderHistory(group, now) {
|
|
|
9096
9275
|
* branch started. The same numbering the workspace itself uses, so "Review 1 of 3" there and
|
|
9097
9276
|
* "Review 1" here are the same review.
|
|
9098
9277
|
*
|
|
9099
|
-
* **Only the latest row carries a workflow status.** `New`, `
|
|
9278
|
+
* **Only the latest row carries a workflow status.** `New`, `Not reviewed` and `Reviewed` describe
|
|
9100
9279
|
* where someone is in a piece of work, and on a superseded review that is a claim about work
|
|
9101
|
-
* nobody should be continuing: a row reading `
|
|
9280
|
+
* nobody should be continuing: a row reading `NOT REVIEWED` under a newer review of the same branch
|
|
9102
9281
|
* invites exactly the wrong click. The second slot is the lineage instead, `Latest` or `Previous`,
|
|
9103
9282
|
* which is the only thing about an older review that a reader needs before opening it.
|
|
9104
9283
|
*/
|
|
@@ -11583,17 +11762,17 @@ function handleNotificationOpen(req, res, params, indexToken) {
|
|
|
11583
11762
|
return redirect(res, `/r/${workspace.id}?t=${encodeURIComponent(workspace.token)}${hash}`);
|
|
11584
11763
|
}
|
|
11585
11764
|
/**
|
|
11586
|
-
* Mark a selection reviewed, or put it back to
|
|
11765
|
+
* Mark a selection reviewed, or put it back to not reviewed.
|
|
11587
11766
|
*
|
|
11588
11767
|
* Authorised by the index credential, like pinning and deleting, because this is the list acting on
|
|
11589
11768
|
* itself. Deliberately not `/api/<id>/state`, which needs a review's own token: routing a bulk mark
|
|
11590
11769
|
* through it would mean putting fifteen credentials into one request, and the index is the one page
|
|
11591
11770
|
* that holds them all.
|
|
11592
11771
|
*
|
|
11593
|
-
* **Marking
|
|
11772
|
+
* **Marking not reviewed fills in `lastViewedAt` where there is none.** Clearing `completedAt` alone
|
|
11594
11773
|
* would leave a review that was never opened showing `New`, and a button that names a state has to
|
|
11595
|
-
* produce it.
|
|
11596
|
-
*
|
|
11774
|
+
* produce it. Taking the mark back is a claim that the work has started, which is what that field
|
|
11775
|
+
* records. It is only ever filled in, never moved, so a review that has been opened
|
|
11597
11776
|
* keeps the time it was opened.
|
|
11598
11777
|
*
|
|
11599
11778
|
* No lock and no running-job check, unlike deletion. A mark is a note about where the reviewer is,
|
|
@@ -12936,7 +13115,11 @@ function applyState(workspace, body) {
|
|
|
12936
13115
|
if (Array.isArray(body?.dismissedIssues)) reviewer.dismissedIssues = body.dismissedIssues.filter((entry) => typeof entry?.id === "string" && typeof entry?.sessionId === "string");
|
|
12937
13116
|
if (typeof body?.dismissFreshnessSha === "string" || body?.dismissFreshnessSha === null) workspace.dismissedFreshnessSha = body.dismissFreshnessSha ?? void 0;
|
|
12938
13117
|
if (typeof body?.dismissKissFreshnessSha === "string" || body?.dismissKissFreshnessSha === null) workspace.dismissedKissFreshnessSha = body.dismissKissFreshnessSha ?? void 0;
|
|
12939
|
-
if (typeof body?.complete === "boolean")
|
|
13118
|
+
if (typeof body?.complete === "boolean") {
|
|
13119
|
+
const now = Date.now();
|
|
13120
|
+
workspace.completedAt = body.complete ? now : void 0;
|
|
13121
|
+
if (!body.complete) workspace.lastViewedAt = workspace.lastViewedAt ?? now;
|
|
13122
|
+
}
|
|
12940
13123
|
if (typeof body?.pinned === "boolean") workspace.pinned = body.pinned;
|
|
12941
13124
|
}
|
|
12942
13125
|
/** 1 MB is far more than any question or id list, and far less than a memory problem. */
|