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/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 ? renderHeaderNav(options) : ""}
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
- /** The workspace rows, for a rail group. */
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 &lt;url&gt;</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 press <kbd class="prb-kbd">⌘P</kbd> (<kbd class="prb-kbd">Ctrl+P</kbd> elsewhere) to add a
11403
- review from here. The panel follows the reviews it started while it stays open, and closing it
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>A collection groups reviews that belong together, such as the pull requests of one
11502
- refactor or one release. It says what belongs together; whether each review still needs your
11503
- attention is still Active, Waiting or Done, so adding or removing a review never changes its
11504
- state.</p>
11505
- <p>Make one with New collection on the Reviews page, or from Manage collections in a card's menu.
11506
- To add several reviews at once, choose Select, tick them, and choose Manage collections. A review
11507
- can be in more than one collection. Ticking a collection adds every selected review to it and
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
- /** Every topic by name at the top, so one in the middle is found by reading a list, not a page. */
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 &#8943; 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-page-panel">
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
- <h2>${escapeHtml$1(topic.title)}</h2>
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
  }