prreviewbuddy 0.83.3 → 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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.85.0
4
+
5
+ **Replay the guided tour from any review.** A review's ⋯ menu now has Replay guided tour, which
6
+ runs the tour on the review you are reading. Replay in Help opens the review you opened last, or,
7
+ if that one is being analysed or updated, your newest review the tour can start on. It never picks
8
+ a review that is busy while another would do. When the tour cannot start, you are told why rather
9
+ than left on a page where nothing happens.
10
+
11
+ ## 0.84.2
12
+
13
+ **Replay guided tour sits below its sentence** on Help's Welcome, rather than beside it.
14
+
15
+ ## 0.84.1
16
+
17
+ **Replay guided tour is readable.** The button on Help's Welcome showed its label in the link
18
+ colour on its own purple, so the words could not be seen. They are white now.
19
+
20
+ ## 0.84.0
21
+
22
+ **A guided tour of your first review.** The first time you open a completed review, a short tour
23
+ walks you through it: the story behind the change, where to focus, the suggested path, diagrams,
24
+ issues, questions, the files, Buddy AI, and the review status that records your judgement. Each
25
+ stop lights up the part of the page it describes. Skip it, or press Escape, and it does not come
26
+ back by itself. Replay it from Help's Welcome at any time. The tour changes nothing about your
27
+ review or your layout: whatever it opens to show you, it puts back.
28
+
29
+ ## 0.83.4
30
+
31
+ **Help opens on Welcome, and its index stays beside you.** Help now starts with a short welcome:
32
+ why PR Review Buddy exists, and the three principles behind it. The list of topics moves from the
33
+ top of the page to a column beside them that stays in view as you scroll, and marks the topic you
34
+ are reading. In a narrow window it goes back above the topics.
35
+
36
+ **No underlined links.** Links across the workspace are told apart by colour, not underlines, at
37
+ rest or on hover.
38
+
3
39
  ## 0.83.3
4
40
 
5
41
  **Peek at a collapsed sidebar.** Rest the pointer on the collapsed sidebar, or tab into it, and it
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";
@@ -2562,11 +2562,11 @@ function renderOverviewTab(session, iconUrl = "", options) {
2562
2562
  return `
2563
2563
  ${options?.authorContext ? renderAuthorContext(session, options.descriptionLink) : renderDescriptionLink(session, options?.descriptionLink)}
2564
2564
  ${renderWhyThisChange(result.motivation, session, result.questions, options)}
2565
- <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">
2566
2566
  <h4 class="pr-helper-overview-heading" id="pr-helper-whats-changing-heading">What's changing</h4>
2567
2567
  <div class="pr-helper-review-summary pr-helper-markdown">${renderMarkdown(result.summary)}</div>
2568
2568
  </section>
2569
- <div class="pr-helper-risk-headline">
2569
+ <div class="pr-helper-risk-headline" data-tour="risk">
2570
2570
  <div class="pr-helper-risk-headline-top">
2571
2571
  <span class="pr-helper-risk-dot pr-helper-risk-dot-${result.overallRisk}"></span>
2572
2572
  <span class="pr-helper-risk-headline-label">${escapeHtml$1(titleCase(result.overallRisk))} Risk</span>
@@ -2575,7 +2575,7 @@ function renderOverviewTab(session, iconUrl = "", options) {
2575
2575
  ${renderProvisionalReason(session)}
2576
2576
  ${result.recommendation ? `<div class="pr-helper-risk-headline-takeaway pr-helper-markdown">${renderMarkdown(result.recommendation)}</div>` : ""}
2577
2577
  </div>
2578
- <div class="pr-helper-overview-stats">
2578
+ <div class="pr-helper-overview-stats" data-tour="stats">
2579
2579
  ${renderOverviewStat(metrics.sections, "Themes", "guide")}
2580
2580
  ${renderIssuesStat(issuesOutstanding(result.issues, reviewerDispositions(session)))}
2581
2581
  ${renderRemainingStat(questionsOutstanding(result.questions, new Set(session.answeredQuestionIds ?? [])), "Questions", "questions", "answered")}
@@ -2698,7 +2698,7 @@ function renderSuggestedPath(sections, heading) {
2698
2698
  if (sections.length === 0) return "";
2699
2699
  const items = sections.map((s) => `<li>${escapeHtml$1(s.title)}</li>`).join("");
2700
2700
  return `
2701
- <div class="pr-helper-suggested-path">
2701
+ <div class="pr-helper-suggested-path" data-tour="suggested-path">
2702
2702
  <h4 class="pr-helper-suggested-path-title">${escapeHtml$1(heading)}</h4>
2703
2703
  <ol class="pr-helper-suggested-path-list">${items}</ol>
2704
2704
  <p class="pr-helper-suggested-path-time">Estimated review time ≈${estimateTotalMinutes(sections)} minutes</p>
@@ -3555,6 +3555,42 @@ function renderDrawnFrom(diagram, reviewedSha) {
3555
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>`;
3556
3556
  }
3557
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
3558
3594
  //#region ../../packages/review-harness/src/domain/change_map/types.ts
3559
3595
  var MAP_ROLES = [
3560
3596
  "intent",
@@ -6151,7 +6187,12 @@ function renderWorkspace(view) {
6151
6187
  <link rel="stylesheet" href="/assets/sidebar.css">
6152
6188
  <link rel="stylesheet" href="/assets/shell.css">
6153
6189
  </head>
6154
- <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" : ""}>
6155
6196
  ${renderGlobalBar({
6156
6197
  unread: view.unreadNotifications ?? 0,
6157
6198
  live: Boolean(view.token),
@@ -6689,6 +6730,7 @@ function renderIdMenu(view, freshness) {
6689
6730
  branchItems ? `<button type="button" class="prb-menu-item" data-queue-action="${view.queued ? "remove" : "add"}">${queueItemLabel(view.queued === true)}</button>` : "",
6690
6731
  canWrite ? `<button type="button" class="prb-menu-item" data-pin-toggle aria-pressed="${view.pinned === true}">${pinItemLabel(view.pinned === true)}</button>` : "",
6691
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>" : "",
6692
6734
  canWrite ? `<button type="button" class="prb-menu-item prb-menu-item-danger" data-delete="${safe}">Delete review</button>` : ""
6693
6735
  ].filter(Boolean);
6694
6736
  return `<div class="prb-menu" data-menu data-review-menu${[
@@ -10673,7 +10715,7 @@ function readPreferences() {
10673
10715
  try {
10674
10716
  const parsed = JSON.parse(readFileSync(PREFERENCES_PATH, "utf8"));
10675
10717
  if (!parsed || typeof parsed !== "object") return {};
10676
- const { askWidth, dismissedNotices, promptsCollapsed, changeMapRegister, theme, reviewSections, runsSections, reviewLayout, sidebar } = parsed;
10718
+ const { askWidth, dismissedNotices, promptsCollapsed, changeMapRegister, theme, reviewSections, runsSections, reviewLayout, sidebar, guidedTour } = parsed;
10677
10719
  return {
10678
10720
  ...typeof askWidth === "number" && Number.isFinite(askWidth) ? { askWidth: clampAskWidth(askWidth) } : {},
10679
10721
  ...Array.isArray(dismissedNotices) ? { dismissedNotices: dismissedNotices.filter((key) => typeof key === "string" && NOTICE_KEY.test(key)) } : {},
@@ -10683,7 +10725,8 @@ function readPreferences() {
10683
10725
  ...readReviewSections(reviewSections),
10684
10726
  ...readRunsSections(runsSections),
10685
10727
  ...readLayout(reviewLayout),
10686
- ...readSidebar(sidebar)
10728
+ ...readSidebar(sidebar),
10729
+ ...isGuidedTourState(guidedTour) ? { guidedTour } : {}
10687
10730
  };
10688
10731
  } catch {
10689
10732
  return {};
@@ -10745,6 +10788,21 @@ function writeSidebar(patch) {
10745
10788
  }, null, 2)}\n`);
10746
10789
  } catch {}
10747
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
+ }
10748
10806
  /** Remember whether "Your prompts" is folded. Read-modify-write, like every other field here. */
10749
10807
  function writePromptsCollapsed(collapsed) {
10750
10808
  try {
@@ -11474,6 +11532,30 @@ var AGENT_PRODUCTS = ((names) => names.length <= 1 ? names[0] ?? "" : `${names.s
11474
11532
  * says what to type and points here for what it means. The diagrams are text on purpose.
11475
11533
  */
11476
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
+ },
11477
11559
  {
11478
11560
  id: "getting-started",
11479
11561
  title: "Getting started",
@@ -11675,7 +11757,27 @@ feature: E</pre>
11675
11757
  <div data-shortcut-table></div>`
11676
11758
  }
11677
11759
  ];
11678
- /** 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
+ }
11679
11781
  function renderContents() {
11680
11782
  return `<nav class="prb-help-contents" aria-label="Help topics">
11681
11783
  <ul>${TOPICS.map((topic) => `<li><a href="#${topic.id}">${escapeHtml$1(topic.title)}</a></li>`).join("")}</ul>
@@ -11706,13 +11808,16 @@ ${renderWorkspaceRail({
11706
11808
  sidebar: view.sidebar
11707
11809
  })}
11708
11810
  <main class="prb-index prb-help">
11709
- <div class="prb-page-panel">
11811
+ <div class="prb-help-layout">
11710
11812
  ${renderContents()}
11813
+ <div class="prb-page-panel">
11711
11814
  ${TOPICS.map((topic) => `<section class="prb-help-topic" id="${topic.id}">
11712
- <h2>${escapeHtml$1(topic.title)}</h2>
11815
+ ${topic.heading ?? `<h2>${escapeHtml$1(topic.title)}</h2>`}
11713
11816
  ${topic.body}
11817
+ ${topic.id === "welcome" ? renderTourAction(view.tour, view.tourFailed === true) : ""}
11714
11818
  </section>`).join("")}
11715
11819
  </div>
11820
+ </div>
11716
11821
  <p class="prb-help-version">PR Review Buddy v${escapeHtml$1(BUILD_VERSION)}</p>
11717
11822
  </main>
11718
11823
  </div>
@@ -15081,6 +15186,7 @@ async function handle(req, res, options) {
15081
15186
  if (url.pathname === "/runs/remove") return void handleRunRemoval(req, res, options.indexToken);
15082
15187
  if (url.pathname === "/runs/sections") return void handleRunsSections(req, res, options.indexToken);
15083
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);
15084
15190
  if (url.pathname === "/notifications/open") return handleNotificationOpen(req, res, url.searchParams, options.indexToken);
15085
15191
  if (url.pathname === "/notifications/recent") return handleRecentNotifications(req, res, url.searchParams, options.indexToken);
15086
15192
  if (url.pathname === "/notifications/live") return handleLiveNotifications(req, res, url.searchParams, options.indexToken);
@@ -15341,10 +15447,30 @@ function handleHelp(req, res, params, indexToken) {
15341
15447
  unreadNotifications: unreadCount$1(),
15342
15448
  driving: isDriving(),
15343
15449
  theme: readPreferences().theme,
15344
- sidebar: readPreferences().sidebar
15450
+ sidebar: readPreferences().sidebar,
15451
+ tour: tourUnavailable(recentWorkspaces(Number.MAX_SAFE_INTEGER), busyReviewIds()) ?? "available",
15452
+ tourFailed: params.get("tour") === "unavailable"
15345
15453
  }), indexCookieHeader(indexToken));
15346
15454
  }
15347
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
+ /**
15348
15474
  * Whether the caller is a browser navigating, rather than something reading the API.
15349
15475
  *
15350
15476
  * `Accept` is the only signal available and browsers are consistent about it: a navigation asks
@@ -16314,6 +16440,8 @@ async function sendWorkspacePage(res, workspace) {
16314
16440
  dismissedNotices: readPreferences().dismissedNotices,
16315
16441
  promptsCollapsed: readPreferences().promptsCollapsed === true,
16316
16442
  reviewLayout: readPreferences().reviewLayout ?? {},
16443
+ guidedTour: readPreferences().guidedTour,
16444
+ tourBusy: busyReviewIds().has(workspace.id),
16317
16445
  changeMapRegister: readPreferences().changeMapRegister ?? "simple",
16318
16446
  architecture: await architecturePane(workspace),
16319
16447
  transcript: workspace.transcript,
@@ -17119,6 +17247,7 @@ async function handlePrefs(req, res) {
17119
17247
  const layout = readReviewLayoutPatch(body?.reviewLayout);
17120
17248
  if (layout) writeReviewLayout(layout);
17121
17249
  if (body?.changeMapRegister === "simple" || body?.changeMapRegister === "technical") writeChangeMapRegister(body.changeMapRegister);
17250
+ if (isGuidedTourState(body?.guidedTour)) writeGuidedTour(body.guidedTour);
17122
17251
  if (typeof body?.dismissNotice === "string" && !dismissNotice(body.dismissNotice)) return send(res, 400, { error: "Not a notice this page can dismiss." });
17123
17252
  send(res, 200, { ok: true });
17124
17253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.83.3",
3
+ "version": "0.85.0",
4
4
  "description": "Understand code changes before you approve them. Local branch and pull request reviews with Claude Code or Codex.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://prreviewbuddy.com",
package/static/help.js CHANGED
@@ -1 +1 @@
1
- import{at as e,d as t,f as n,ft as r,p as i,rt as a,t as o}from"./chunk-DwdwtB9r.js";a(),t(`global`,{}),document.querySelector(`[data-shortcut-table]`)?.replaceChildren(n(document,i(e(navigator)))),o(`/restart`),r(window,()=>window.location.reload());
1
+ import{at as e,d as t,f as n,ft as r,p as i,rt as a,t as o}from"./chunk-DwdwtB9r.js";function s(e,t){if(e.length===0)return-1;if(t)return e.length-1;let n=0;return e.forEach((e,t)=>{e<=120&&(n=t)}),n}var c=new Set([`ArrowDown`,`ArrowUp`,`PageDown`,`PageUp`,`Home`,`End`,` `]);function l(e,t){let n=[...e.querySelectorAll(`.prb-help-contents a[href^="#"]`)],r=n.map(t=>e.getElementById(decodeURIComponent(t.hash.slice(1)))),i=e.defaultView;if(n.length===0||!i)return;let a=r.findIndex(e=>e!==null&&`#${e.id}`===i.location.hash),o=e=>{n.forEach((t,n)=>{n===e?t.setAttribute(`aria-current`,`location`):t.removeAttribute(`aria-current`)})},l=()=>{if(a>=0)return o(a);let n=t.scrollHeight>t.clientHeight?t:e.scrollingElement??e.documentElement,i=Math.max(0,n===t?t.getBoundingClientRect().top:0),c=r.map(e=>e?e.getBoundingClientRect().top-i:1/0),l=n.scrollTop>0&&n.scrollTop+n.clientHeight>=n.scrollHeight-2;o(s(c,l))},u=e=>{let t=r.findIndex(t=>t!==null&&`#${t.id}`===e);t<0||(a=t,o(t))},d=()=>{a<0||(a=-1,l())};e.addEventListener(`click`,e=>{let t=e.target?.closest?.(`a[href^="#"]`);t&&u(t.hash)}),i.addEventListener(`hashchange`,()=>u(i.location.hash)),t.addEventListener(`wheel`,d,{passive:!0}),t.addEventListener(`touchmove`,d,{passive:!0}),t.addEventListener(`pointerdown`,d),e.addEventListener(`keydown`,e=>{c.has(e.key)&&d()}),e.addEventListener(`scroll`,l,{capture:!0,passive:!0}),i.addEventListener(`resize`,l),l()}a(),t(`global`,{}),document.querySelector(`[data-shortcut-table]`)?.replaceChildren(n(document,i(e(navigator)))),o(`/restart`);var u=document.querySelector(`main.prb-help`);u&&l(document,u),r(window,()=>window.location.reload());