prreviewbuddy 0.82.4 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/dist/server.js +292 -55
- package/package.json +1 -1
- package/static/{chunk-Dio1flWJ.js → chunk-B2vcrGTe.js} +1 -1
- package/static/{chunk-C_KEGoSE.js → chunk-BL6xJoZm.js} +1 -1
- package/static/chunk-DwdwtB9r.js +1 -0
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/runs.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +345 -72
- package/static/sidebar.css +21 -23
- package/static/workspace.js +80 -80
- package/static/chunk-CmdZYpyj.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MIN_WORKSPACE_PORT, $i as resolveModel, $n as RUN_RECORD_RETENTION_MS, $r as deleteCollection, $t as markRead, A as batchIdentity, Ai as isGradeFor, An as escapeHtml$1, At as loadArchitecture, Bi as WITHHELD_REASON, Bn as readMarker, Br as LIFECYCLES, Bt as ancestorsOf, C as RUN_KINDS, Ci as ANALYSIS_ONLY, Cn as UNESTABLISHED_MOTIVATION, Cr as kissDecisionsOf, Ct as mapRuntimes, D as reportRun, Di as findingAssessment, Dn as renderMarkdown, Dr as loadWorkspace, Dt as formatDuration, E as endRun, Ei as currentGradeIn, En as GITHUB_MARKDOWN, Er as lineagePosition, Et as describeRuntimes, F as activateQueued, Fi as followUpFindingId, Fn as withReviewLock, Ft as storedScopes, G as serverEntryExists, Gi as isCommitReview, Gn as findExistingReview, Gr as lifecycleItemLabel, Gt as subjectOf, Hi as resolveTarget, Hn as requestJobStop, Hr as activityLabel, Ht as writeJsonAtomically, I as compactQueue, Ii as asString, In as MANAGED_ROOT, Ir as summarise, It as trackedFiles, Ji as shortNameOf, Jn as followedBranch, Jr as parseLifecycle, Jt as createPrState, K as spawnServer, Ki as resolveBase, Kn as checkCodeFreshness, Kr as lifecyclePill, Kt as categoryOf, Li as extractJsonText, Ln as createWorktree, Lr as touchWorkspace, Lt as ownersOf, M as parsePullRequestLink, Mi as reassessIn, Mr as previousKissResult, Mt as readArchitecture, N as unsupportedReason, Ni as sourcePhrase, Nn as ReviewBeingDeletedError, Nr as recentWorkspaces, Nt as repositoryKey, O as stopRun, Oi as findingIn, On as CODE_BLOCK_CLASS, Ot as ARCHITECTURE_DIR, P as runBatch, Pi as wasReassessed, Pn as isBusy, Pr as reviewedCommit, Pt as storeArchitecture, Q as MAX_WORKSPACE_PORT, Qi as resolveAgent, Qn as fillFileUrlTemplate, Qr as createWithMembers, Qt as markAllRead, R as settleQueue, Ri as objectWithAnyOf, Rn as holdWorktree, Rr as workspaceRevision, Rt as QUOTE_LIMITS, S as track, Si as withExplanation, Sn as chatConversationSection, Sr as kissDecisionRefusal, St as waitingHistory, T as completeRun, Ti as assessmentLookup, Tn as hasAuthorContent, Tr as lineageKey, Tt as RUNTIME_WORDS, U as readServerRecord, Ui as originRemote, Ur as isLifecycle, Ut as announceJob, V as clearServerRecordOwnedBy, Vi as kissOutcome, Vn as removeWorktree, Vr as activityAttributes, Vt as isArchitectureId, Wi as DetachedHeadError, Wr as lifecycleChip, Wt as branchOf, X as writeServerRecord, Xi as git, Xn as fetchHead, Xr as collectionsStamp, Xt as eventName, Yi as shorthandOf, Yn as reviewedShaOf, Yr as sectionOf, Yt as createPrUpdate, Z as DEFAULT_WORKSPACE_PORT, Zi as refExists, Zn as forgeResolver, Zr as createCollection, Zt as listNotifications, _i as processDiscussion, _n as unforeseenFailure, _r as decideKissRecommendation, _t as telemetryUploadConsent, a as wasBlocked, aa as STORE_ROOT, ai as queueAdd, an as removeNotification, ar as loadJob, at as observeReview, b as startKissJob, bi as explanationOf, bn as agentEnvOf, br as indexKeptIds, c as RefreshUnavailableError, ca as agentFor$1, ci as queueStamp, cn as unreadAround, ct as reanalyseReview, d as mergeUpdateInto, da as isAgentId, di as isIssueOutstanding, dn as existingPaths, dr as phasesFor, ea as agentIdOf, ei as nameProblemText, en as markReadForReview, er as allJobs, et as parseWorkspacePort, fa as parkedReason, fi as isQuestionOutstanding, fn as ASK_REREAD_CEILING_MS, fr as progressSteps, ft as readEvents, g as liveUpdateFor, gi as reviewerDispositions, gn as storedConversation, gt as recordTelemetryUploadConsent, h as UpdateAlreadyRunningError, ha as AgentUnavailableError, hi as resolvedByConversation, hn as saveRefresh, hr as isClaimed, i as liveJobsFor, ii as updateCollections, in as readStateOf, ir as isTerminal, it as failureOf$1, j as extractPullRequestLinks, ji as promptGrade, jn as renderInlineText, jr as presentedKissRunOf, jt as partFiles, k as relativeTime$1, ki as gradeScale, kn as CODE_COPY_CLASS, kr as mutateWorkspace, kt as checkArchitecture, l as cancelUpdate, la as agentIdentityOf, li as readQueue, ln as unreadCount$1, lr as isLiveJob, lt as runJob, m as askCheckout, ma as AgentCancelledError, mi as questionsOutstanding, mn as prepareConversationWithin, n as deleteReview, na as readAgentPreference, ni as renameCollection, nn as pruneNotifications, nr as endedAt, nt as statedWorkspacePort, o as discardJob, oa as AGENT_IDS, oi as queueAddMany, on as retireNotification, or as pruneJobResidue, ot as prepareReview, p as updateReview, pa as runnableAgents, pi as issuesOutstanding, pn as adoptLegacyBaseline, pr as purposeOf, pt as record, q as stopChild, qi as AmbiguousRefError, qn as checkReview, qr as offersLifecycle, qt as createArchitectureNotification, r as lineageIds, ra as writeAgentPreference, ri as setMembership, rn as readNotification, rr as fail$1, rt as writePortPreference, s as FETCH_TIMEOUT_MS, sa as agentById, si as queueRemove, sn as toneOf, sr as saveJob, st as settleReview, t as deleteLineage, ta as clearAgentPreference, ti as readCollections, tn as moveIds, tr as attemptIdOf, tt as resolveWorkspacePort, u as cancellableHere, ua as detectAgents, ui as doneVerb, un as withUsageRecorded, ur as isTerminalJobPhase, v as kissAgentId, vi as recommendationsOf, vn as refreshPrContext, vr as followedRefName, w as beginRun, wi as analysisRefOf, wn as authorPreview, wr as latestKissRun, wt as recordRuntimeBestEffort, x as isDriving, xi as isExplanationAction, xn as EXPLAIN_SIMPLY_PROMPT, xr as indexReviewGroups, xt as feedbackUrl, yi as explanationActionOf, yn as isUnchanged, yr as groupByLineage, yt as BUILD_VERSION, z as readIndexToken, zi as describeAuthorship, zn as isWorktreeHeld, zr as workspaceStamps, zt as REPOSITORY_SCOPE } from "./delete_review-BED4mPxB.js";
|
|
1
|
+
import { $ as MIN_WORKSPACE_PORT, $i as resolveModel, $n as RUN_RECORD_RETENTION_MS, $r as deleteCollection, $t as markRead, A as batchIdentity, Ai as isGradeFor, An as escapeHtml$1, At as loadArchitecture, Bi as WITHHELD_REASON, Bn as readMarker, Br as LIFECYCLES, Bt as ancestorsOf, C as RUN_KINDS, Ci as ANALYSIS_ONLY, Cn as UNESTABLISHED_MOTIVATION, Cr as kissDecisionsOf, Ct as mapRuntimes, D as reportRun, Di as findingAssessment, Dn as renderMarkdown, Dr as loadWorkspace, Dt as formatDuration, E as endRun, Ei as currentGradeIn, En as GITHUB_MARKDOWN, Er as lineagePosition, Et as describeRuntimes, F as activateQueued, Fi as followUpFindingId, Fn as withReviewLock, Ft as storedScopes, G as serverEntryExists, Gi as isCommitReview, Gn as findExistingReview, Gr as lifecycleItemLabel, Gt as subjectOf, Hi as resolveTarget, Hn as requestJobStop, Hr as activityLabel, Ht as writeJsonAtomically, I as compactQueue, Ii as asString, In as MANAGED_ROOT, Ir as summarise, It as trackedFiles, Ji as shortNameOf, Jn as followedBranch, Jr as parseLifecycle, Jt as createPrState, K as spawnServer, Ki as resolveBase, Kn as checkCodeFreshness, Kr as lifecyclePill, Kt as categoryOf, Li as extractJsonText, Ln as createWorktree, Lr as touchWorkspace, Lt as ownersOf, M as parsePullRequestLink, Mi as reassessIn, Mr as previousKissResult, Mt as readArchitecture, N as unsupportedReason, Ni as sourcePhrase, Nn as ReviewBeingDeletedError, Nr as recentWorkspaces, Nt as repositoryKey, O as stopRun, Oi as findingIn, On as CODE_BLOCK_CLASS, Ot as ARCHITECTURE_DIR, P as runBatch, Pi as wasReassessed, Pn as isBusy, Pr as reviewedCommit, Pt as storeArchitecture, Q as MAX_WORKSPACE_PORT, Qi as resolveAgent, Qn as fillFileUrlTemplate, Qr as createWithMembers, Qt as markAllRead, R as settleQueue, Ri as objectWithAnyOf, Rn as holdWorktree, Rr as workspaceRevision, Rt as QUOTE_LIMITS, S as track, Si as withExplanation, Sn as chatConversationSection, Sr as kissDecisionRefusal, St as waitingHistory, T as completeRun, Ti as assessmentLookup, Tn as hasAuthorContent, Tr as lineageKey, Tt as RUNTIME_WORDS, U as readServerRecord, Ui as originRemote, Ur as isLifecycle, Ut as announceJob, V as clearServerRecordOwnedBy, Vi as kissOutcome, Vn as removeWorktree, Vr as activityAttributes, Vt as isArchitectureId, Wi as DetachedHeadError, Wr as lifecycleChip, Wt as branchOf, X as writeServerRecord, Xi as git, Xn as fetchHead, Xr as collectionsStamp, Xt as eventName, Yi as shorthandOf, Yn as reviewedShaOf, Yr as sectionOf, Yt as createPrUpdate, Z as DEFAULT_WORKSPACE_PORT, Zi as refExists, Zn as forgeResolver, Zr as createCollection, Zt as listNotifications, _ as runningJobs, _i as processDiscussion, _n as unforeseenFailure, _r as decideKissRecommendation, _t as telemetryUploadConsent, a as wasBlocked, aa as STORE_ROOT, ai as queueAdd, an as removeNotification, ar as loadJob, at as observeReview, b as startKissJob, bi as explanationOf, bn as agentEnvOf, br as indexKeptIds, c as RefreshUnavailableError, ca as agentFor$1, ci as queueStamp, cn as unreadAround, ct as reanalyseReview, d as mergeUpdateInto, da as isAgentId, di as isIssueOutstanding, dn as existingPaths, dr as phasesFor, ea as agentIdOf, ei as nameProblemText, en as markReadForReview, er as allJobs, et as parseWorkspacePort, fa as parkedReason, fi as isQuestionOutstanding, fn as ASK_REREAD_CEILING_MS, fr as progressSteps, ft as readEvents, g as liveUpdateFor, gi as reviewerDispositions, gn as storedConversation, gt as recordTelemetryUploadConsent, h as UpdateAlreadyRunningError, ha as AgentUnavailableError, hi as resolvedByConversation, hn as saveRefresh, hr as isClaimed, i as liveJobsFor, ii as updateCollections, in as readStateOf, ir as isTerminal, it as failureOf$1, j as extractPullRequestLinks, ji as promptGrade, jn as renderInlineText, jr as presentedKissRunOf, jt as partFiles, k as relativeTime$1, ki as gradeScale, kn as CODE_COPY_CLASS, kr as mutateWorkspace, kt as checkArchitecture, l as cancelUpdate, la as agentIdentityOf, li as readQueue, ln as unreadCount$1, lr as isLiveJob, lt as runJob, m as askCheckout, ma as AgentCancelledError, mi as questionsOutstanding, mn as prepareConversationWithin, n as deleteReview, na as readAgentPreference, ni as renameCollection, nn as pruneNotifications, nr as endedAt, nt as statedWorkspacePort, o as discardJob, oa as AGENT_IDS, oi as queueAddMany, on as retireNotification, or as pruneJobResidue, ot as prepareReview, p as updateReview, pa as runnableAgents, pi as issuesOutstanding, pn as adoptLegacyBaseline, pr as purposeOf, pt as record, q as stopChild, qi as AmbiguousRefError, qn as checkReview, qr as offersLifecycle, qt as createArchitectureNotification, r as lineageIds, ra as writeAgentPreference, ri as setMembership, rn as readNotification, rr as fail$1, rt as writePortPreference, s as FETCH_TIMEOUT_MS, sa as agentById, si as queueRemove, sn as toneOf, sr as saveJob, st as settleReview, t as deleteLineage, ta as clearAgentPreference, ti as readCollections, tn as moveIds, tr as attemptIdOf, tt as resolveWorkspacePort, u as cancellableHere, ua as detectAgents, ui as doneVerb, un as withUsageRecorded, ur as isTerminalJobPhase, v as kissAgentId, vi as recommendationsOf, vn as refreshPrContext, vr as followedRefName, w as beginRun, wi as analysisRefOf, wn as authorPreview, wr as latestKissRun, wt as recordRuntimeBestEffort, x as isDriving, xi as isExplanationAction, xn as EXPLAIN_SIMPLY_PROMPT, xr as indexReviewGroups, xt as feedbackUrl, yi as explanationActionOf, yn as isUnchanged, yr as groupByLineage, yt as BUILD_VERSION, z as readIndexToken, zi as describeAuthorship, zn as isWorktreeHeld, zr as workspaceStamps, zt as REPOSITORY_SCOPE } from "./delete_review-BED4mPxB.js";
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -803,7 +803,10 @@ var PATHS = {
|
|
|
803
803
|
closed: "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"m5.6 5.6 12.8 12.8\"/>",
|
|
804
804
|
reopened: "<circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>",
|
|
805
805
|
warning: "<path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z\"/><path d=\"M12 9v4M12 17h.01\"/>",
|
|
806
|
-
forward: "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>"
|
|
806
|
+
forward: "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>",
|
|
807
|
+
plus: "<path d=\"M12 5v14M5 12h14\"/>",
|
|
808
|
+
"sidebar-collapse": "<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M9 4v16\"/><path d=\"m16 9-3 3 3 3\"/>",
|
|
809
|
+
"sidebar-expand": "<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M9 4v16\"/><path d=\"m13 9 3 3-3 3\"/>"
|
|
807
810
|
};
|
|
808
811
|
/** The icon slot for one rail row. Hidden from assistive technology: the label beside it says it. */
|
|
809
812
|
function railIcon(icon) {
|
|
@@ -863,6 +866,18 @@ function serverStatusLabel(status) {
|
|
|
863
866
|
return "Workspace server running";
|
|
864
867
|
}
|
|
865
868
|
//#endregion
|
|
869
|
+
//#region ../../packages/review-harness/src/workspace/sidebar_state.ts
|
|
870
|
+
/**
|
|
871
|
+
* What the sidebar's own toggle, at its foot, will do when pressed, which is what it is called.
|
|
872
|
+
* Only Expanded and Compact: while the sidebar is hidden that toggle is not drawn, and the strip's
|
|
873
|
+
* restore control, which only ever says Show sidebar, stands in for it.
|
|
874
|
+
*/
|
|
875
|
+
function sidebarToggleLabel(sidebar) {
|
|
876
|
+
return sidebar?.compact ? "Expand sidebar" : "Collapse sidebar";
|
|
877
|
+
}
|
|
878
|
+
/** The strip's restore control, drawn only while the sidebar is hidden. */
|
|
879
|
+
var SHOW_SIDEBAR = "Show sidebar";
|
|
880
|
+
//#endregion
|
|
866
881
|
//#region ../../packages/review-harness/src/workspace/workspace_nav.ts
|
|
867
882
|
/**
|
|
868
883
|
* The workspace's own places, and the one model every page draws them from.
|
|
@@ -905,6 +920,13 @@ var WORKSPACE_PLACES = [
|
|
|
905
920
|
}
|
|
906
921
|
];
|
|
907
922
|
/**
|
|
923
|
+
* The sidebar's state, on `<html>` beside the theme, so the page is drawn that way from its first
|
|
924
|
+
* paint. The stylesheet reads it; `client/sidebar.ts` changes it.
|
|
925
|
+
*/
|
|
926
|
+
function sidebarAttributes(sidebar) {
|
|
927
|
+
return `${sidebar?.compact ? " data-sidebar-compact" : ""}${sidebar?.hidden ? " data-sidebar-hidden" : ""}`;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
908
930
|
* The count beside Notifications. Always rendered, hidden at zero, so the page can reveal it when
|
|
909
931
|
* `/revision` or Mark all read reports a new number. See `client/unread_counts.ts`.
|
|
910
932
|
*/
|
|
@@ -934,7 +956,8 @@ function renderGlobalBar(options) {
|
|
|
934
956
|
const live = options.live ?? true;
|
|
935
957
|
return `<header class="prb-strip">
|
|
936
958
|
<span class="prb-wordmark"><b>pr</b>review<b>buddy</b></span>
|
|
937
|
-
${live ?
|
|
959
|
+
${live ? renderSidebarRestore() : ""}
|
|
960
|
+
${live ? renderNewReview() : ""}
|
|
938
961
|
${live ? renderHistoryControls() : ""}
|
|
939
962
|
${live ? renderStripReviews(options) : ""}
|
|
940
963
|
${live ? renderStripRuns(options) : ""}
|
|
@@ -980,6 +1003,37 @@ function renderNotificationsBell(options) {
|
|
|
980
1003
|
</a>`;
|
|
981
1004
|
}
|
|
982
1005
|
/**
|
|
1006
|
+
* Show sidebar: the way back from Hidden, and drawn only then (see `shell.css`). Among the strip's
|
|
1007
|
+
* controls rather than beside the wordmark, so it reads as a control and not as part of the brand.
|
|
1008
|
+
* Named in words and in the strip's own tooltip, never a `title`.
|
|
1009
|
+
*/
|
|
1010
|
+
function renderSidebarRestore() {
|
|
1011
|
+
return `<button type="button" class="prb-strip-action prb-sidebar-restore" data-sidebar-toggle="strip" aria-label="${SHOW_SIDEBAR}" data-tip="${SHOW_SIDEBAR}">${iconSvg("sidebar-expand")}</button>`;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* The sidebar's own toggle, the last row of its foot: Expanded and Compact, back and forth.
|
|
1015
|
+
*
|
|
1016
|
+
* In the same place in both, so it never moves under the pointer that pressed it. Hidden is not a
|
|
1017
|
+
* third press: it is its own act (⌘K H), and the strip's Show sidebar brings it back at the width it
|
|
1018
|
+
* had. The row's words say what pressing it will do, and in Compact they are its tooltip, as every
|
|
1019
|
+
* row's are. Both drawings are rendered and the stylesheet shows the one for the state, so nothing
|
|
1020
|
+
* needs repainting but the words. `client/sidebar.ts` keeps those true.
|
|
1021
|
+
*/
|
|
1022
|
+
function renderSidebarToggle(sidebar) {
|
|
1023
|
+
return `<button type="button" class="prb-rail-item prb-rail-link prb-sidebar-toggle" data-sidebar-toggle="rail" aria-expanded="${!sidebar?.compact}">
|
|
1024
|
+
<span class="prb-rail-icon" aria-hidden="true"><span class="prb-sidebar-collapse-icon">${iconSvg("sidebar-collapse")}</span><span class="prb-sidebar-expand-icon">${iconSvg("sidebar-expand")}</span></span>
|
|
1025
|
+
<span class="prb-rail-label"><span class="prb-rail-title">${sidebarToggleLabel(sidebar)}</span></span>
|
|
1026
|
+
</button>`;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* New review, in the strip: the square stand-in for the rail's labelled button, shown only while
|
|
1030
|
+
* the rail is hidden (a narrow window, a review's focus mode). See `renderNewReviewButton`, and
|
|
1031
|
+
* `shell.css` for the one rule that decides which of the two is showing.
|
|
1032
|
+
*/
|
|
1033
|
+
function renderNewReview() {
|
|
1034
|
+
return `<button type="button" class="prb-strip-action prb-strip-new-review" aria-label="New review" data-new-review="strip">${iconSvg("plus")}</button>`;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
983
1037
|
* Back and Forward through this tab's workspace history, ahead of the places.
|
|
984
1038
|
*
|
|
985
1039
|
* Workspace-wide, so here in the strip rather than in a review's own bar: they move between views,
|
|
@@ -1058,9 +1112,20 @@ function renderServerStatus() {
|
|
|
1058
1112
|
<span class="prb-visually-hidden" data-server-status-label>${escapeHtml$1(serverStatusLabel("running"))}</span>
|
|
1059
1113
|
</span>`;
|
|
1060
1114
|
}
|
|
1061
|
-
/**
|
|
1115
|
+
/**
|
|
1116
|
+
* New review, at the head of the workspace rows: starting a review is the product's primary action,
|
|
1117
|
+
* so it is a labelled button rather than an icon to interpret, set apart from the places below it.
|
|
1118
|
+
*
|
|
1119
|
+
* What it does is wired by the command layer (`client/command_layer.ts`), which runs the very
|
|
1120
|
+
* command ⌘P runs, so the two cannot open different things. The shortcut beside the label is
|
|
1121
|
+
* painted there too, because only the browser knows the platform.
|
|
1122
|
+
*/
|
|
1123
|
+
function renderNewReviewButton() {
|
|
1124
|
+
return `<div class="prb-rail-new"><button type="button" class="prb-new-review" data-new-review="rail">${iconSvg("plus")}<span class="prb-new-review-label">New review</span><kbd class="prb-new-review-key" data-new-review-key aria-hidden="true"></kbd></button></div>`;
|
|
1125
|
+
}
|
|
1126
|
+
/** New review, then the workspace rows, for a rail group. */
|
|
1062
1127
|
function renderWorkspaceNav(options) {
|
|
1063
|
-
return WORKSPACE_PLACES.map((item) => `<a class="prb-rail-item prb-rail-link" href="${item.href}"${item.place === options.current ? " aria-current=\"true\"" : ""}>
|
|
1128
|
+
return renderNewReviewButton() + WORKSPACE_PLACES.map((item) => `<a class="prb-rail-item prb-rail-link" href="${item.href}"${item.place === options.current ? " aria-current=\"true\"" : ""}>
|
|
1064
1129
|
${railIcon(item.icon)}
|
|
1065
1130
|
<span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(item.label)}</span></span>
|
|
1066
1131
|
${item.place === "notifications" ? unreadCount(options.unread, "rail") : ""}
|
|
@@ -1097,20 +1162,9 @@ function renderSendFeedback() {
|
|
|
1097
1162
|
function renderWorkspaceRail(options) {
|
|
1098
1163
|
return `<nav class="prb-rail" aria-label="Workspace">
|
|
1099
1164
|
<div class="prb-rail-group">${renderWorkspaceNav(options)}</div>
|
|
1100
|
-
<div class="prb-rail-group prb-rail-foot">${renderSendFeedback()}</div>
|
|
1165
|
+
<div class="prb-rail-group prb-rail-foot">${renderSendFeedback()}${renderSidebarToggle(options.sidebar)}</div>
|
|
1101
1166
|
</nav>`;
|
|
1102
1167
|
}
|
|
1103
|
-
/**
|
|
1104
|
-
* What the rail carries that the strip does not, shown only while a narrow window hides the rail.
|
|
1105
|
-
*
|
|
1106
|
-
* Which is now the feedback form alone. Every workspace place is an icon in this same strip at
|
|
1107
|
-
* every width, so a text link beside one of them would be the same destination twice; before the
|
|
1108
|
-
* Reviews icon existed this is where Reviews had to go, and the filter that put it here is gone
|
|
1109
|
-
* with the reason for it.
|
|
1110
|
-
*/
|
|
1111
|
-
function renderHeaderNav(_options) {
|
|
1112
|
-
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>`;
|
|
1113
|
-
}
|
|
1114
1168
|
//#endregion
|
|
1115
1169
|
//#region ../../packages/review-harness/src/domain/utils/file_path.ts
|
|
1116
1170
|
/**
|
|
@@ -2508,11 +2562,11 @@ function renderOverviewTab(session, iconUrl = "", options) {
|
|
|
2508
2562
|
return `
|
|
2509
2563
|
${options?.authorContext ? renderAuthorContext(session, options.descriptionLink) : renderDescriptionLink(session, options?.descriptionLink)}
|
|
2510
2564
|
${renderWhyThisChange(result.motivation, session, result.questions, options)}
|
|
2511
|
-
<section class="pr-helper-whats-changing" aria-labelledby="pr-helper-whats-changing-heading">
|
|
2565
|
+
<section class="pr-helper-whats-changing" data-tour="whats-changing" aria-labelledby="pr-helper-whats-changing-heading">
|
|
2512
2566
|
<h4 class="pr-helper-overview-heading" id="pr-helper-whats-changing-heading">What's changing</h4>
|
|
2513
2567
|
<div class="pr-helper-review-summary pr-helper-markdown">${renderMarkdown(result.summary)}</div>
|
|
2514
2568
|
</section>
|
|
2515
|
-
<div class="pr-helper-risk-headline">
|
|
2569
|
+
<div class="pr-helper-risk-headline" data-tour="risk">
|
|
2516
2570
|
<div class="pr-helper-risk-headline-top">
|
|
2517
2571
|
<span class="pr-helper-risk-dot pr-helper-risk-dot-${result.overallRisk}"></span>
|
|
2518
2572
|
<span class="pr-helper-risk-headline-label">${escapeHtml$1(titleCase(result.overallRisk))} Risk</span>
|
|
@@ -2521,7 +2575,7 @@ function renderOverviewTab(session, iconUrl = "", options) {
|
|
|
2521
2575
|
${renderProvisionalReason(session)}
|
|
2522
2576
|
${result.recommendation ? `<div class="pr-helper-risk-headline-takeaway pr-helper-markdown">${renderMarkdown(result.recommendation)}</div>` : ""}
|
|
2523
2577
|
</div>
|
|
2524
|
-
<div class="pr-helper-overview-stats">
|
|
2578
|
+
<div class="pr-helper-overview-stats" data-tour="stats">
|
|
2525
2579
|
${renderOverviewStat(metrics.sections, "Themes", "guide")}
|
|
2526
2580
|
${renderIssuesStat(issuesOutstanding(result.issues, reviewerDispositions(session)))}
|
|
2527
2581
|
${renderRemainingStat(questionsOutstanding(result.questions, new Set(session.answeredQuestionIds ?? [])), "Questions", "questions", "answered")}
|
|
@@ -2644,7 +2698,7 @@ function renderSuggestedPath(sections, heading) {
|
|
|
2644
2698
|
if (sections.length === 0) return "";
|
|
2645
2699
|
const items = sections.map((s) => `<li>${escapeHtml$1(s.title)}</li>`).join("");
|
|
2646
2700
|
return `
|
|
2647
|
-
<div class="pr-helper-suggested-path">
|
|
2701
|
+
<div class="pr-helper-suggested-path" data-tour="suggested-path">
|
|
2648
2702
|
<h4 class="pr-helper-suggested-path-title">${escapeHtml$1(heading)}</h4>
|
|
2649
2703
|
<ol class="pr-helper-suggested-path-list">${items}</ol>
|
|
2650
2704
|
<p class="pr-helper-suggested-path-time">Estimated review time ≈${estimateTotalMinutes(sections)} minutes</p>
|
|
@@ -3501,6 +3555,42 @@ function renderDrawnFrom(diagram, reviewedSha) {
|
|
|
3501
3555
|
return `<p class="prb-diagram-stale">Drawn from <code>${escapeHtml$1(diagram.commitSha.slice(0, 7))}</code>, before this review was brought up to date. Regenerate to draw it from the commit the review now describes.</p>`;
|
|
3502
3556
|
}
|
|
3503
3557
|
//#endregion
|
|
3558
|
+
//#region ../../packages/review-harness/src/workspace/guided_tour.ts
|
|
3559
|
+
function isGuidedTourState(value) {
|
|
3560
|
+
return value === "completed" || value === "dismissed";
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Whether a review page opens the tour by itself: a live page (one with a server behind it) over a
|
|
3564
|
+
* standard result that no update is replacing, for a reviewer who has not already finished or
|
|
3565
|
+
* skipped it. Running reviews never qualify, so the tour cannot interrupt one.
|
|
3566
|
+
*/
|
|
3567
|
+
function offersGuidedTour(page) {
|
|
3568
|
+
return page.live && page.hasResult && !page.updating && page.state === void 0;
|
|
3569
|
+
}
|
|
3570
|
+
/** The hash Replay opens a review at. Not a place the page knows, so the page drops it once read. */
|
|
3571
|
+
var TOUR_REPLAY_HASH = "#tour";
|
|
3572
|
+
/**
|
|
3573
|
+
* Whether the tour can be shown on a review: its analysis finished, and nothing is analysing or
|
|
3574
|
+
* updating it now. Whether the reviewer has marked it Reviewed has nothing to do with it.
|
|
3575
|
+
*/
|
|
3576
|
+
function tourEligible(review, busy) {
|
|
3577
|
+
return review.hasStandardReview && !busy.has(review.id);
|
|
3578
|
+
}
|
|
3579
|
+
/**
|
|
3580
|
+
* The review Replay from Help opens: the one last opened, when the tour can be shown on it, and
|
|
3581
|
+
* otherwise the newest it can be shown on. Never a busy review, so none is chosen only when no
|
|
3582
|
+
* review qualifies. Replay from a review's own menu does not ask: it uses the review on screen.
|
|
3583
|
+
*/
|
|
3584
|
+
function chooseTourReview(reviews, busy) {
|
|
3585
|
+
const last = reviews.filter((review) => review.lastViewedAt !== void 0).reduce((best, review) => !best || review.lastViewedAt > best.lastViewedAt ? review : best, null);
|
|
3586
|
+
if (last && tourEligible(last, busy)) return last;
|
|
3587
|
+
return reviews.filter((review) => tourEligible(review, busy)).reduce((best, review) => !best || review.createdAt > best.createdAt ? review : best, null);
|
|
3588
|
+
}
|
|
3589
|
+
function tourUnavailable(reviews, busy) {
|
|
3590
|
+
if (chooseTourReview(reviews, busy)) return null;
|
|
3591
|
+
return reviews.some((review) => review.hasStandardReview) ? "busy" : "none";
|
|
3592
|
+
}
|
|
3593
|
+
//#endregion
|
|
3504
3594
|
//#region ../../packages/review-harness/src/domain/change_map/types.ts
|
|
3505
3595
|
var MAP_ROLES = [
|
|
3506
3596
|
"intent",
|
|
@@ -6088,7 +6178,7 @@ function renderWorkspace(view) {
|
|
|
6088
6178
|
*/
|
|
6089
6179
|
const freshness = result ? view.freshness : void 0;
|
|
6090
6180
|
return `<!doctype html>
|
|
6091
|
-
<html lang="en"${themeAttribute(view.theme)}>
|
|
6181
|
+
<html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
|
|
6092
6182
|
<head>
|
|
6093
6183
|
<meta charset="utf-8">
|
|
6094
6184
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -6097,7 +6187,12 @@ function renderWorkspace(view) {
|
|
|
6097
6187
|
<link rel="stylesheet" href="/assets/sidebar.css">
|
|
6098
6188
|
<link rel="stylesheet" href="/assets/shell.css">
|
|
6099
6189
|
</head>
|
|
6100
|
-
<body data-api-base="${escapeHtml$1(view.apiBase)}"${view.token ? ` data-token="${escapeHtml$1(view.token)}" data-notifications-since="${Date.now()}"` : ""}${view.editor ? ` data-editor-label="${escapeHtml$1(view.editor.label)}" data-editor-template="${escapeHtml$1(view.editor.template)}"` : ""}${view.repoPath ? ` data-repo-path="${escapeHtml$1(view.repoPath)}"` : ""}${view.revision === void 0 ? "" : ` data-workspace-revision="${escapeHtml$1(String(view.revision))}"`} data-session-id="${escapeHtml$1(view.session.id)}" data-explain-prompt="${escapeHtml$1(EXPLAIN_SIMPLY_PROMPT)}" data-way-back="${escapeHtml$1(wayBackSentence())}" data-default-view="${session.result ? "overview" : showsProgress(view) ? "progress" : "kiss"}"
|
|
6190
|
+
<body data-api-base="${escapeHtml$1(view.apiBase)}"${view.token ? ` data-token="${escapeHtml$1(view.token)}" data-notifications-since="${Date.now()}"` : ""}${view.editor ? ` data-editor-label="${escapeHtml$1(view.editor.label)}" data-editor-template="${escapeHtml$1(view.editor.template)}"` : ""}${view.repoPath ? ` data-repo-path="${escapeHtml$1(view.repoPath)}"` : ""}${view.revision === void 0 ? "" : ` data-workspace-revision="${escapeHtml$1(String(view.revision))}"`} data-session-id="${escapeHtml$1(view.session.id)}" data-explain-prompt="${escapeHtml$1(EXPLAIN_SIMPLY_PROMPT)}" data-way-back="${escapeHtml$1(wayBackSentence())}" data-default-view="${session.result ? "overview" : showsProgress(view) ? "progress" : "kiss"}"${offersGuidedTour({
|
|
6191
|
+
live: view.token !== void 0,
|
|
6192
|
+
hasResult: Boolean(session.result),
|
|
6193
|
+
updating: updating(view),
|
|
6194
|
+
state: view.guidedTour
|
|
6195
|
+
}) ? " data-guided-tour=\"offer\"" : ""}${view.tourBusy ? " data-tour-busy" : ""}>
|
|
6101
6196
|
${renderGlobalBar({
|
|
6102
6197
|
unread: view.unreadNotifications ?? 0,
|
|
6103
6198
|
live: Boolean(view.token),
|
|
@@ -6635,6 +6730,7 @@ function renderIdMenu(view, freshness) {
|
|
|
6635
6730
|
branchItems ? `<button type="button" class="prb-menu-item" data-queue-action="${view.queued ? "remove" : "add"}">${queueItemLabel(view.queued === true)}</button>` : "",
|
|
6636
6731
|
canWrite ? `<button type="button" class="prb-menu-item" data-pin-toggle aria-pressed="${view.pinned === true}">${pinItemLabel(view.pinned === true)}</button>` : "",
|
|
6637
6732
|
`<button type="button" class="prb-menu-item" data-copy-id="${safe}">Copy review ID</button>`,
|
|
6733
|
+
canWrite && view.session.result ? "<button type=\"button\" class=\"prb-menu-item\" data-tour-replay>Replay guided tour</button>" : "",
|
|
6638
6734
|
canWrite ? `<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${safe}">Delete review</button>` : ""
|
|
6639
6735
|
].filter(Boolean);
|
|
6640
6736
|
return `<div class="prb-menu" data-menu data-review-menu${[
|
|
@@ -6681,6 +6777,7 @@ function renderRailFoot(view) {
|
|
|
6681
6777
|
return `<div class="prb-rail-group prb-rail-foot">
|
|
6682
6778
|
${view.token ? renderWorkspaceNav({ unread: view.unreadNotifications ?? 0 }) : ""}
|
|
6683
6779
|
${renderSendFeedback()}
|
|
6780
|
+
${view.token ? renderSidebarToggle(view.sidebar) : ""}
|
|
6684
6781
|
</div>`;
|
|
6685
6782
|
}
|
|
6686
6783
|
/**
|
|
@@ -9676,7 +9773,7 @@ function renderReviewsPage(view) {
|
|
|
9676
9773
|
const storedReviews = view.groups.reduce((total, group) => total + group.history.length, 0);
|
|
9677
9774
|
const renderSectionAt = (section) => renderSection(section, sections[section], (groups) => renderCards(groups, "home", (group) => collectedHome.has(group)), sectionOpen(section, view.sectionsCollapsed), sections[section].filter((group) => !inProgressOnly.has(group.latest.id) && !collectedHome.has(group)).length);
|
|
9678
9775
|
return `<!doctype html>
|
|
9679
|
-
<html lang="en"${themeAttribute(view.theme)}>
|
|
9776
|
+
<html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
|
|
9680
9777
|
<head>
|
|
9681
9778
|
<meta charset="utf-8">
|
|
9682
9779
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -9699,7 +9796,8 @@ ${renderContextBar("Reviews", ordered.length === 0 ? "" : renderControls$1(queue
|
|
|
9699
9796
|
<div class="prb-shell">
|
|
9700
9797
|
${renderWorkspaceRail({
|
|
9701
9798
|
current: "reviews",
|
|
9702
|
-
unread: view.unreadNotifications ?? 0
|
|
9799
|
+
unread: view.unreadNotifications ?? 0,
|
|
9800
|
+
sidebar: view.sidebar
|
|
9703
9801
|
})}
|
|
9704
9802
|
<main class="prb-index">
|
|
9705
9803
|
${ordered.length === 0 ? "" : `<div class="prb-index-summary">
|
|
@@ -10617,7 +10715,7 @@ function readPreferences() {
|
|
|
10617
10715
|
try {
|
|
10618
10716
|
const parsed = JSON.parse(readFileSync(PREFERENCES_PATH, "utf8"));
|
|
10619
10717
|
if (!parsed || typeof parsed !== "object") return {};
|
|
10620
|
-
const { askWidth, dismissedNotices, promptsCollapsed, changeMapRegister, theme, reviewSections, runsSections, reviewLayout } = parsed;
|
|
10718
|
+
const { askWidth, dismissedNotices, promptsCollapsed, changeMapRegister, theme, reviewSections, runsSections, reviewLayout, sidebar, guidedTour } = parsed;
|
|
10621
10719
|
return {
|
|
10622
10720
|
...typeof askWidth === "number" && Number.isFinite(askWidth) ? { askWidth: clampAskWidth(askWidth) } : {},
|
|
10623
10721
|
...Array.isArray(dismissedNotices) ? { dismissedNotices: dismissedNotices.filter((key) => typeof key === "string" && NOTICE_KEY.test(key)) } : {},
|
|
@@ -10626,7 +10724,9 @@ function readPreferences() {
|
|
|
10626
10724
|
...isTheme(theme) ? { theme } : {},
|
|
10627
10725
|
...readReviewSections(reviewSections),
|
|
10628
10726
|
...readRunsSections(runsSections),
|
|
10629
|
-
...readLayout(reviewLayout)
|
|
10727
|
+
...readLayout(reviewLayout),
|
|
10728
|
+
...readSidebar(sidebar),
|
|
10729
|
+
...isGuidedTourState(guidedTour) ? { guidedTour } : {}
|
|
10630
10730
|
};
|
|
10631
10731
|
} catch {
|
|
10632
10732
|
return {};
|
|
@@ -10659,6 +10759,50 @@ function writeReviewLayout(patch) {
|
|
|
10659
10759
|
writeFileSync(PREFERENCES_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
|
10660
10760
|
} catch {}
|
|
10661
10761
|
}
|
|
10762
|
+
/** Only the two booleans, and only when one is set; an empty object is no preference. */
|
|
10763
|
+
function readSidebar(value) {
|
|
10764
|
+
if (!value || typeof value !== "object") return {};
|
|
10765
|
+
const { compact, hidden } = value;
|
|
10766
|
+
const sidebar = {
|
|
10767
|
+
...typeof compact === "boolean" ? { compact } : {},
|
|
10768
|
+
...typeof hidden === "boolean" ? { hidden } : {}
|
|
10769
|
+
};
|
|
10770
|
+
return Object.keys(sidebar).length === 0 ? {} : { sidebar };
|
|
10771
|
+
}
|
|
10772
|
+
/** The sidebar fields a request may set: a boolean or nothing, each. */
|
|
10773
|
+
function readSidebarPatch(value) {
|
|
10774
|
+
const { sidebar } = readSidebar(value);
|
|
10775
|
+
return sidebar ?? null;
|
|
10776
|
+
}
|
|
10777
|
+
/** Remember a change to the sidebar, merged into what is stored. Read-modify-write, like every other field here. */
|
|
10778
|
+
function writeSidebar(patch) {
|
|
10779
|
+
try {
|
|
10780
|
+
mkdirSync(STORE_ROOT, { recursive: true });
|
|
10781
|
+
const current = readPreferences();
|
|
10782
|
+
writeFileSync(PREFERENCES_PATH, `${JSON.stringify({
|
|
10783
|
+
...current,
|
|
10784
|
+
sidebar: {
|
|
10785
|
+
...current.sidebar,
|
|
10786
|
+
...patch
|
|
10787
|
+
}
|
|
10788
|
+
}, null, 2)}\n`);
|
|
10789
|
+
} catch {}
|
|
10790
|
+
}
|
|
10791
|
+
/**
|
|
10792
|
+
* Remember how the guided tour was first answered. The first answer stands: a replay from Help, or
|
|
10793
|
+
* a second window finishing a tour another already skipped, is not a new answer to the offer.
|
|
10794
|
+
*/
|
|
10795
|
+
function writeGuidedTour(state) {
|
|
10796
|
+
try {
|
|
10797
|
+
const current = readPreferences();
|
|
10798
|
+
if (current.guidedTour) return;
|
|
10799
|
+
mkdirSync(STORE_ROOT, { recursive: true });
|
|
10800
|
+
writeFileSync(PREFERENCES_PATH, `${JSON.stringify({
|
|
10801
|
+
...current,
|
|
10802
|
+
guidedTour: state
|
|
10803
|
+
}, null, 2)}\n`);
|
|
10804
|
+
} catch {}
|
|
10805
|
+
}
|
|
10662
10806
|
/** Remember whether "Your prompts" is folded. Read-modify-write, like every other field here. */
|
|
10663
10807
|
function writePromptsCollapsed(collapsed) {
|
|
10664
10808
|
try {
|
|
@@ -10879,7 +11023,7 @@ function renderSettingsPage(view) {
|
|
|
10879
11023
|
const { settings } = view;
|
|
10880
11024
|
const address = `http://localhost:${settings.port.configured}`;
|
|
10881
11025
|
return `<!doctype html>
|
|
10882
|
-
<html lang="en"${themeAttribute(settings.theme)}>
|
|
11026
|
+
<html lang="en"${themeAttribute(settings.theme)}${sidebarAttributes(view.sidebar)}>
|
|
10883
11027
|
<head>
|
|
10884
11028
|
<meta charset="utf-8">
|
|
10885
11029
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -10898,7 +11042,8 @@ ${renderContextBar("Settings", renderBack())}
|
|
|
10898
11042
|
<div class="prb-shell">
|
|
10899
11043
|
${renderWorkspaceRail({
|
|
10900
11044
|
current: "settings",
|
|
10901
|
-
unread: view.unreadNotifications ?? 0
|
|
11045
|
+
unread: view.unreadNotifications ?? 0,
|
|
11046
|
+
sidebar: view.sidebar
|
|
10902
11047
|
})}
|
|
10903
11048
|
<main class="prb-index prb-settings">
|
|
10904
11049
|
<div class="prb-page-panel">
|
|
@@ -11387,6 +11532,30 @@ var AGENT_PRODUCTS = ((names) => names.length <= 1 ? names[0] ?? "" : `${names.s
|
|
|
11387
11532
|
* says what to type and points here for what it means. The diagrams are text on purpose.
|
|
11388
11533
|
*/
|
|
11389
11534
|
var TOPICS = [
|
|
11535
|
+
{
|
|
11536
|
+
id: "welcome",
|
|
11537
|
+
title: "Welcome",
|
|
11538
|
+
heading: `<p class="prb-help-eyebrow">Welcome to PR Review Buddy</p>
|
|
11539
|
+
<h2 class="prb-help-headline">Understand before you approve.</h2>`,
|
|
11540
|
+
body: `<p>Code review isn't just about finding bugs. It's about understanding what changed, why it
|
|
11541
|
+
changed, and whether those changes make sense.</p>
|
|
11542
|
+
<p>AI can generate code faster than ever. But someone still needs to understand it, challenge its
|
|
11543
|
+
assumptions, and take responsibility for approving it.</p>
|
|
11544
|
+
<p>That's why we built PR Review Buddy.</p>
|
|
11545
|
+
<p>PR Review Buddy helps you make sense of changes before you approve them. It brings together
|
|
11546
|
+
code, context, conversations and AI-assisted analysis so you can focus on what matters: making
|
|
11547
|
+
informed engineering decisions.</p>
|
|
11548
|
+
<p>The goal isn't to replace the reviewer. It's to make the reviewer better informed.</p>
|
|
11549
|
+
<h3 class="prb-help-eyebrow">Our principles</h3>
|
|
11550
|
+
<dl class="prb-help-principles">
|
|
11551
|
+
<div><dt>Understanding over automation</dt>
|
|
11552
|
+
<dd>AI should help you reason about changes, not make decisions on your behalf.</dd></div>
|
|
11553
|
+
<div><dt>Evidence over assertions</dt>
|
|
11554
|
+
<dd>Findings should be grounded in the code and context, so you can investigate and challenge them.</dd></div>
|
|
11555
|
+
<div><dt>You remain in control</dt>
|
|
11556
|
+
<dd>Choose your AI agent, explore the analysis, and make the final judgement yourself.</dd></div>
|
|
11557
|
+
</dl>`
|
|
11558
|
+
},
|
|
11390
11559
|
{
|
|
11391
11560
|
id: "getting-started",
|
|
11392
11561
|
title: "Getting started",
|
|
@@ -11399,8 +11568,10 @@ var TOPICS = [
|
|
|
11399
11568
|
<li><code>prreviewbuddy review --pr <url></code> for a pull request</li>
|
|
11400
11569
|
<li><code>prreviewbuddy open</code> to list the reviews you have and open this workspace</li>
|
|
11401
11570
|
</ul>
|
|
11402
|
-
<p>Or
|
|
11403
|
-
|
|
11571
|
+
<p>Or click <b>New review</b> at the top of the sidebar, or press <kbd class="prb-kbd">⌘P</kbd>
|
|
11572
|
+
(<kbd class="prb-kbd">Ctrl+P</kbd> on other platforms), to start a review from anywhere in the
|
|
11573
|
+
workspace. With the sidebar collapsed, New review is the <b>+</b> at its top, and with the
|
|
11574
|
+
sidebar hidden, the <b>+</b> in the top toolbar. The panel follows the reviews it started while it stays open, and closing it
|
|
11404
11575
|
stops nothing. <code>prreviewbuddy help</code> lists every command, and
|
|
11405
11576
|
<code>prreviewbuddy review --help</code> every option a review takes.</p>
|
|
11406
11577
|
<p>There is no account and no API key. PR Review Buddy uses the coding agent installed on your
|
|
@@ -11498,18 +11669,13 @@ feature: E</pre>
|
|
|
11498
11669
|
{
|
|
11499
11670
|
id: "collections",
|
|
11500
11671
|
title: "Collections",
|
|
11501
|
-
body: `<p>
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
<
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
leaves their other collections alone.</p>
|
|
11509
|
-
<p>Each collection lists its reviews that are not Done. Marking one Done moves it under Show
|
|
11510
|
-
completed, in every collection it is in, and Move to active brings it back. A review in a
|
|
11511
|
-
collection is listed there rather than under Active, Waiting or Done. A pinned review stays under
|
|
11512
|
-
Pinned. The Collections filter shows chosen collections, or Uncollected for reviews in none.</p>
|
|
11672
|
+
body: `<p>Collections help you organise related reviews, such as a refactor, release or feature.</p>
|
|
11673
|
+
<p>Create a collection from the Reviews page, then add reviews individually or select several and
|
|
11674
|
+
use <b>Manage collections</b>. A review can belong to multiple collections.</p>
|
|
11675
|
+
<p>Collections show reviews that still need attention. When you mark a review <b>Done</b>, it moves
|
|
11676
|
+
under <b>Show completed</b> within its collection, without losing its membership.</p>
|
|
11677
|
+
<p>You can collapse collections to stay focused, and expand them when you're ready to continue.
|
|
11678
|
+
Removing a review from all collections returns it to the normal review sections.</p>
|
|
11513
11679
|
<p>Reviews in a collection are kept for ninety days, like pinned and Done reviews, counted from
|
|
11514
11680
|
when you last opened or marked them. Adding one to a collection doesn't restart that count.
|
|
11515
11681
|
Deleting a collection keeps its reviews.</p>`
|
|
@@ -11591,7 +11757,27 @@ feature: E</pre>
|
|
|
11591
11757
|
<div data-shortcut-table></div>`
|
|
11592
11758
|
}
|
|
11593
11759
|
];
|
|
11594
|
-
/**
|
|
11760
|
+
/**
|
|
11761
|
+
* Every topic by name beside the topics, so one in the middle is found by reading a list, not a
|
|
11762
|
+
* page. First in the document so a keyboard or a screen reader reaches it before the prose; the
|
|
11763
|
+
* stylesheet places it. `help_index.ts` marks the topic being read.
|
|
11764
|
+
*/
|
|
11765
|
+
/**
|
|
11766
|
+
* Where the tour lives once it has been seen. `/tour` picks the review when Replay is pressed (see
|
|
11767
|
+
* `chooseTourReview`); with none to pick, Help says why rather than offering a button that would
|
|
11768
|
+
* open a review the tour cannot start on.
|
|
11769
|
+
*/
|
|
11770
|
+
function renderTourAction(tour, failed) {
|
|
11771
|
+
const lead = failed ? "<strong>The guided tour couldn't start.</strong> " : "";
|
|
11772
|
+
const role = failed ? " role=\"status\"" : "";
|
|
11773
|
+
if (tour === "available") return `<p class="prb-help-tour">The guided tour walks through a completed review, one part at a time,
|
|
11774
|
+
starting with the one you opened last. A review's ⋯ menu replays it on that review.</p>
|
|
11775
|
+
<p><a class="prb-help-tour-action" href="/tour">Replay guided tour</a></p>`;
|
|
11776
|
+
if (tour === "busy") return `<p class="prb-help-tour"${role}>${lead}The guided tour walks through a completed review, and every
|
|
11777
|
+
one you have is being analysed or updated right now. Replay it from here once that finishes.</p>`;
|
|
11778
|
+
return `<p class="prb-help-tour"${role}>${lead}The guided tour walks through a completed review, and there
|
|
11779
|
+
isn't one yet. Start a review from <a href="/">Reviews</a>, then replay the tour from here once it has finished.</p>`;
|
|
11780
|
+
}
|
|
11595
11781
|
function renderContents() {
|
|
11596
11782
|
return `<nav class="prb-help-contents" aria-label="Help topics">
|
|
11597
11783
|
<ul>${TOPICS.map((topic) => `<li><a href="#${topic.id}">${escapeHtml$1(topic.title)}</a></li>`).join("")}</ul>
|
|
@@ -11599,7 +11785,7 @@ function renderContents() {
|
|
|
11599
11785
|
}
|
|
11600
11786
|
function renderHelpPage(view) {
|
|
11601
11787
|
return `<!doctype html>
|
|
11602
|
-
<html lang="en"${themeAttribute(view.theme)}>
|
|
11788
|
+
<html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
|
|
11603
11789
|
<head>
|
|
11604
11790
|
<meta charset="utf-8">
|
|
11605
11791
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -11618,16 +11804,20 @@ ${renderContextBar("Help", "")}
|
|
|
11618
11804
|
<div class="prb-shell">
|
|
11619
11805
|
${renderWorkspaceRail({
|
|
11620
11806
|
current: "help",
|
|
11621
|
-
unread: view.unreadNotifications ?? 0
|
|
11807
|
+
unread: view.unreadNotifications ?? 0,
|
|
11808
|
+
sidebar: view.sidebar
|
|
11622
11809
|
})}
|
|
11623
11810
|
<main class="prb-index prb-help">
|
|
11624
|
-
<div class="prb-
|
|
11811
|
+
<div class="prb-help-layout">
|
|
11625
11812
|
${renderContents()}
|
|
11813
|
+
<div class="prb-page-panel">
|
|
11626
11814
|
${TOPICS.map((topic) => `<section class="prb-help-topic" id="${topic.id}">
|
|
11627
|
-
|
|
11815
|
+
${topic.heading ?? `<h2>${escapeHtml$1(topic.title)}</h2>`}
|
|
11628
11816
|
${topic.body}
|
|
11817
|
+
${topic.id === "welcome" ? renderTourAction(view.tour, view.tourFailed === true) : ""}
|
|
11629
11818
|
</section>`).join("")}
|
|
11630
11819
|
</div>
|
|
11820
|
+
</div>
|
|
11631
11821
|
<p class="prb-help-version">PR Review Buddy v${escapeHtml$1(BUILD_VERSION)}</p>
|
|
11632
11822
|
</main>
|
|
11633
11823
|
</div>
|
|
@@ -11640,7 +11830,7 @@ ${renderWorkspaceRail({
|
|
|
11640
11830
|
function renderNotificationsPage(view) {
|
|
11641
11831
|
const unread = view.notifications.filter((notification) => notification.readAt === void 0).length;
|
|
11642
11832
|
return `<!doctype html>
|
|
11643
|
-
<html lang="en"${themeAttribute(view.theme)}>
|
|
11833
|
+
<html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
|
|
11644
11834
|
<head>
|
|
11645
11835
|
<meta charset="utf-8">
|
|
11646
11836
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -11659,7 +11849,8 @@ ${renderContextBar("Activity", view.notifications.length === 0 ? "" : renderRead
|
|
|
11659
11849
|
<div class="prb-shell">
|
|
11660
11850
|
${renderWorkspaceRail({
|
|
11661
11851
|
current: "notifications",
|
|
11662
|
-
unread: view.unreadNotifications ?? 0
|
|
11852
|
+
unread: view.unreadNotifications ?? 0,
|
|
11853
|
+
sidebar: view.sidebar
|
|
11663
11854
|
})}
|
|
11664
11855
|
<main class="prb-index prb-notifications-page">
|
|
11665
11856
|
${view.notifications.length === 0 ? `<p class="prb-index-empty" data-notifications-empty>Nothing yet. When a review finishes or fails, or a branch or pull request moves, it appears here.</p>` : `<ol class="prb-notifications" data-notifications>
|
|
@@ -13044,7 +13235,7 @@ function renderRunsPage(view) {
|
|
|
13044
13235
|
const nothing = inProgress.length === 0 && recent.length === 0;
|
|
13045
13236
|
const notResponding = recent.some((entry) => entry.status === "not-responding");
|
|
13046
13237
|
return `<!doctype html>
|
|
13047
|
-
<html lang="en"${themeAttribute(view.theme)}>
|
|
13238
|
+
<html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
|
|
13048
13239
|
<head>
|
|
13049
13240
|
<meta charset="utf-8">
|
|
13050
13241
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -13063,7 +13254,8 @@ ${renderContextBar("Runs", nothing ? "" : renderControls(notResponding))}
|
|
|
13063
13254
|
<div class="prb-shell">
|
|
13064
13255
|
${renderWorkspaceRail({
|
|
13065
13256
|
current: "runs",
|
|
13066
|
-
unread: view.unreadNotifications ?? 0
|
|
13257
|
+
unread: view.unreadNotifications ?? 0,
|
|
13258
|
+
sidebar: view.sidebar
|
|
13067
13259
|
})}
|
|
13068
13260
|
<main class="prb-index prb-runs">
|
|
13069
13261
|
<p class="prb-runs-lede">Monitor review executions and recent activity.</p>
|
|
@@ -14994,11 +15186,13 @@ async function handle(req, res, options) {
|
|
|
14994
15186
|
if (url.pathname === "/runs/remove") return void handleRunRemoval(req, res, options.indexToken);
|
|
14995
15187
|
if (url.pathname === "/runs/sections") return void handleRunsSections(req, res, options.indexToken);
|
|
14996
15188
|
if (url.pathname === "/help") return void handleHelp(req, res, url.searchParams, options.indexToken);
|
|
15189
|
+
if (url.pathname === "/tour") return void handleTour(req, res, url.searchParams, options.indexToken);
|
|
14997
15190
|
if (url.pathname === "/notifications/open") return handleNotificationOpen(req, res, url.searchParams, options.indexToken);
|
|
14998
15191
|
if (url.pathname === "/notifications/recent") return handleRecentNotifications(req, res, url.searchParams, options.indexToken);
|
|
14999
15192
|
if (url.pathname === "/notifications/live") return handleLiveNotifications(req, res, url.searchParams, options.indexToken);
|
|
15000
15193
|
if (url.pathname === "/pin") return handlePin(req, res, options.indexToken);
|
|
15001
15194
|
if (url.pathname === "/sections") return handleSections(req, res, options.indexToken);
|
|
15195
|
+
if (url.pathname === "/sidebar") return handleSidebar(req, res, options.indexToken);
|
|
15002
15196
|
if (url.pathname === "/queue") return handleQueue(req, res, options.indexToken);
|
|
15003
15197
|
if (url.pathname === "/collections") return handleCollections(req, res, options.indexToken);
|
|
15004
15198
|
if (url.pathname === "/collections/members") return handleCollectionMembers(req, res, options.indexToken);
|
|
@@ -15148,6 +15342,7 @@ function indexView() {
|
|
|
15148
15342
|
unreadNotifications: unreadCount$1(),
|
|
15149
15343
|
driving: isDriving(),
|
|
15150
15344
|
theme: readPreferences().theme,
|
|
15345
|
+
sidebar: readPreferences().sidebar,
|
|
15151
15346
|
sectionsCollapsed: readPreferences().reviewSections
|
|
15152
15347
|
};
|
|
15153
15348
|
}
|
|
@@ -15238,7 +15433,8 @@ async function handleSettings(req, res, params, indexToken) {
|
|
|
15238
15433
|
sendHtml(res, 200, renderSettingsPage({
|
|
15239
15434
|
settings: readSettings(),
|
|
15240
15435
|
unreadNotifications: unreadCount$1(),
|
|
15241
|
-
driving: isDriving()
|
|
15436
|
+
driving: isDriving(),
|
|
15437
|
+
sidebar: readPreferences().sidebar
|
|
15242
15438
|
}), indexCookieHeader(indexToken));
|
|
15243
15439
|
}
|
|
15244
15440
|
/**
|
|
@@ -15250,10 +15446,31 @@ function handleHelp(req, res, params, indexToken) {
|
|
|
15250
15446
|
sendHtml(res, 200, renderHelpPage({
|
|
15251
15447
|
unreadNotifications: unreadCount$1(),
|
|
15252
15448
|
driving: isDriving(),
|
|
15253
|
-
theme: readPreferences().theme
|
|
15449
|
+
theme: readPreferences().theme,
|
|
15450
|
+
sidebar: readPreferences().sidebar,
|
|
15451
|
+
tour: tourUnavailable(recentWorkspaces(Number.MAX_SAFE_INTEGER), busyReviewIds()) ?? "available",
|
|
15452
|
+
tourFailed: params.get("tour") === "unavailable"
|
|
15254
15453
|
}), indexCookieHeader(indexToken));
|
|
15255
15454
|
}
|
|
15256
15455
|
/**
|
|
15456
|
+
* Reviews something is analysing or updating right now. The guided tour is not shown on these: what
|
|
15457
|
+
* it would point at is about to be replaced. An assistant answering, or a diagram being drawn, is
|
|
15458
|
+
* not one of these: the review under it stays as it is.
|
|
15459
|
+
*/
|
|
15460
|
+
function busyReviewIds() {
|
|
15461
|
+
return new Set(runningJobs().map((running) => running.job).filter((job) => purposeOf(job) !== "ask").map((job) => job.workspaceId));
|
|
15462
|
+
}
|
|
15463
|
+
/**
|
|
15464
|
+
* Replay guided tour, from Help. Chosen when it is pressed rather than when Help was drawn, so a
|
|
15465
|
+
* review that started updating in between is not opened to a tour that cannot start. With nothing to
|
|
15466
|
+
* open, back to Help's Welcome, which says why.
|
|
15467
|
+
*/
|
|
15468
|
+
function handleTour(req, res, params, indexToken) {
|
|
15469
|
+
if (!indexAuthorised(req, params, indexToken)) return refuseIndex(req, res);
|
|
15470
|
+
const review = chooseTourReview(recentWorkspaces(Number.MAX_SAFE_INTEGER), busyReviewIds());
|
|
15471
|
+
redirect(res, review ? `/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}${TOUR_REPLAY_HASH}` : "/help?tour=unavailable#welcome");
|
|
15472
|
+
}
|
|
15473
|
+
/**
|
|
15257
15474
|
* Whether the caller is a browser navigating, rather than something reading the API.
|
|
15258
15475
|
*
|
|
15259
15476
|
* `Accept` is the only signal available and browsers are consistent about it: a navigation asks
|
|
@@ -15365,7 +15582,8 @@ async function handleNotifications(req, res, params, indexToken) {
|
|
|
15365
15582
|
gone: new Set(notifications.filter((entry) => !loadWorkspace(entry.reviewId)).map((entry) => entry.id)),
|
|
15366
15583
|
unreadNotifications: unreadCount$1(),
|
|
15367
15584
|
driving: isDriving(),
|
|
15368
|
-
theme: readPreferences().theme
|
|
15585
|
+
theme: readPreferences().theme,
|
|
15586
|
+
sidebar: readPreferences().sidebar
|
|
15369
15587
|
}), indexCookieHeader(indexToken));
|
|
15370
15588
|
}
|
|
15371
15589
|
/**
|
|
@@ -15386,6 +15604,7 @@ function handleRuns(req, res, params, indexToken) {
|
|
|
15386
15604
|
unreadNotifications: unreadCount$1(),
|
|
15387
15605
|
driving: isDriving(),
|
|
15388
15606
|
theme: preferences.theme,
|
|
15607
|
+
sidebar: preferences.sidebar,
|
|
15389
15608
|
sectionsCollapsed: preferences.runsSections
|
|
15390
15609
|
}), indexCookieHeader(indexToken));
|
|
15391
15610
|
}
|
|
@@ -15799,6 +16018,18 @@ async function handleRunsSections(req, res, indexToken) {
|
|
|
15799
16018
|
writeRunsSectionCollapsed(body.section, body.collapsed);
|
|
15800
16019
|
send(res, 200, { ok: true });
|
|
15801
16020
|
}
|
|
16021
|
+
/**
|
|
16022
|
+
* The sidebar, from a workspace page that is not a review, with the same credential as `/sections`.
|
|
16023
|
+
* A review page sends the same field to its own `/prefs`, since it may hold no index cookie.
|
|
16024
|
+
*/
|
|
16025
|
+
async function handleSidebar(req, res, indexToken) {
|
|
16026
|
+
if (req.method !== "POST") return send(res, 405, { error: "Use POST." });
|
|
16027
|
+
if (!indexToken || !secretMatches(indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
|
|
16028
|
+
const sidebar = readSidebarPatch((await readJson(req))?.sidebar);
|
|
16029
|
+
if (!sidebar) return send(res, 400, { error: "Not a sidebar setting." });
|
|
16030
|
+
writeSidebar(sidebar);
|
|
16031
|
+
send(res, 200, { ok: true });
|
|
16032
|
+
}
|
|
15802
16033
|
async function handleSections(req, res, indexToken) {
|
|
15803
16034
|
if (req.method !== "POST") return send(res, 405, { error: "Use POST." });
|
|
15804
16035
|
if (!indexToken || !secretMatches(indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
|
|
@@ -16205,9 +16436,12 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
16205
16436
|
repoPath: workspace.repoPath,
|
|
16206
16437
|
askWidth: readPreferences().askWidth,
|
|
16207
16438
|
theme: readPreferences().theme,
|
|
16439
|
+
sidebar: readPreferences().sidebar,
|
|
16208
16440
|
dismissedNotices: readPreferences().dismissedNotices,
|
|
16209
16441
|
promptsCollapsed: readPreferences().promptsCollapsed === true,
|
|
16210
16442
|
reviewLayout: readPreferences().reviewLayout ?? {},
|
|
16443
|
+
guidedTour: readPreferences().guidedTour,
|
|
16444
|
+
tourBusy: busyReviewIds().has(workspace.id),
|
|
16211
16445
|
changeMapRegister: readPreferences().changeMapRegister ?? "simple",
|
|
16212
16446
|
architecture: await architecturePane(workspace),
|
|
16213
16447
|
transcript: workspace.transcript,
|
|
@@ -17008,9 +17242,12 @@ async function handlePrefs(req, res) {
|
|
|
17008
17242
|
if (typeof askWidth === "number") writeAskWidth(askWidth);
|
|
17009
17243
|
else if (askWidth === null) clearAskWidth();
|
|
17010
17244
|
if (typeof body?.promptsCollapsed === "boolean") writePromptsCollapsed(body.promptsCollapsed);
|
|
17245
|
+
const sidebar = readSidebarPatch(body?.sidebar);
|
|
17246
|
+
if (sidebar) writeSidebar(sidebar);
|
|
17011
17247
|
const layout = readReviewLayoutPatch(body?.reviewLayout);
|
|
17012
17248
|
if (layout) writeReviewLayout(layout);
|
|
17013
17249
|
if (body?.changeMapRegister === "simple" || body?.changeMapRegister === "technical") writeChangeMapRegister(body.changeMapRegister);
|
|
17250
|
+
if (isGuidedTourState(body?.guidedTour)) writeGuidedTour(body.guidedTour);
|
|
17014
17251
|
if (typeof body?.dismissNotice === "string" && !dismissNotice(body.dismissNotice)) return send(res, 400, { error: "Not a notice this page can dismiss." });
|
|
17015
17252
|
send(res, 200, { ok: true });
|
|
17016
17253
|
}
|