prreviewbuddy 0.82.4 → 0.83.3

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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.83.3
4
+
5
+ **Peek at a collapsed sidebar.** Rest the pointer on the collapsed sidebar, or tab into it, and it
6
+ slides out to full width over the page, with every name and section, until you move away. The page
7
+ underneath stays where it was, and the sidebar stays collapsed.
8
+
9
+ ## 0.83.2
10
+
11
+ **Send feedback is only in the sidebar.** A narrow window no longer draws a Send feedback link in
12
+ the top toolbar; it is in the sidebar, expanded or collapsed, and nowhere else.
13
+
14
+ ## 0.83.1
15
+
16
+ **New review is the sidebar's one purple button, and Collapse sidebar moves to the sidebar's foot.**
17
+ New review is filled in purple wherever it is: the full-width button, the square + of the collapsed
18
+ sidebar, and the + in the top toolbar. Collapse sidebar is now the last row of the sidebar, below
19
+ Send feedback, and expands it again from the same place. While the sidebar is hidden, Show sidebar
20
+ in the top toolbar brings it back, and Send feedback is hidden with it.
21
+
22
+ ## 0.83.0
23
+
24
+ **Start a review from New review in the sidebar, and collapse the sidebar to icons.** A New review
25
+ button, with its shortcut beside it, sits above Reviews in the sidebar on every workspace page. It
26
+ opens the same panel as ⌘P (Ctrl+P on other platforms), which works as before; that panel is now
27
+ called New review too. The button at the far left of the top toolbar collapses the sidebar to a
28
+ column of icons, with New review as a + and each name shown on hover, and expands it again. ⌘K H
29
+ hides the sidebar completely; the same toolbar button then brings it back, and a + in the toolbar
30
+ starts a review while it is hidden. Your choice is remembered.
31
+
32
+ ## 0.82.5
33
+
34
+ **Help explains Collections more plainly.** The Collections topic in Workspace Help now says what
35
+ collections are for, how to add reviews, what happens when one is marked Done, and that collapsing
36
+ helps you stay focused. The note on how long reviews in a collection are kept is unchanged.
37
+
3
38
  ## 0.82.4
4
39
 
5
40
  **⌘K L C lists your collections, to expand or collapse one.** Each row shows the collection's
package/dist/server.js CHANGED
@@ -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
  /**
@@ -6088,7 +6142,7 @@ function renderWorkspace(view) {
6088
6142
  */
6089
6143
  const freshness = result ? view.freshness : void 0;
6090
6144
  return `<!doctype html>
6091
- <html lang="en"${themeAttribute(view.theme)}>
6145
+ <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
6092
6146
  <head>
6093
6147
  <meta charset="utf-8">
6094
6148
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -6681,6 +6735,7 @@ function renderRailFoot(view) {
6681
6735
  return `<div class="prb-rail-group prb-rail-foot">
6682
6736
  ${view.token ? renderWorkspaceNav({ unread: view.unreadNotifications ?? 0 }) : ""}
6683
6737
  ${renderSendFeedback()}
6738
+ ${view.token ? renderSidebarToggle(view.sidebar) : ""}
6684
6739
  </div>`;
6685
6740
  }
6686
6741
  /**
@@ -9676,7 +9731,7 @@ function renderReviewsPage(view) {
9676
9731
  const storedReviews = view.groups.reduce((total, group) => total + group.history.length, 0);
9677
9732
  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
9733
  return `<!doctype html>
9679
- <html lang="en"${themeAttribute(view.theme)}>
9734
+ <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
9680
9735
  <head>
9681
9736
  <meta charset="utf-8">
9682
9737
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -9699,7 +9754,8 @@ ${renderContextBar("Reviews", ordered.length === 0 ? "" : renderControls$1(queue
9699
9754
  <div class="prb-shell">
9700
9755
  ${renderWorkspaceRail({
9701
9756
  current: "reviews",
9702
- unread: view.unreadNotifications ?? 0
9757
+ unread: view.unreadNotifications ?? 0,
9758
+ sidebar: view.sidebar
9703
9759
  })}
9704
9760
  <main class="prb-index">
9705
9761
  ${ordered.length === 0 ? "" : `<div class="prb-index-summary">
@@ -10617,7 +10673,7 @@ function readPreferences() {
10617
10673
  try {
10618
10674
  const parsed = JSON.parse(readFileSync(PREFERENCES_PATH, "utf8"));
10619
10675
  if (!parsed || typeof parsed !== "object") return {};
10620
- const { askWidth, dismissedNotices, promptsCollapsed, changeMapRegister, theme, reviewSections, runsSections, reviewLayout } = parsed;
10676
+ const { askWidth, dismissedNotices, promptsCollapsed, changeMapRegister, theme, reviewSections, runsSections, reviewLayout, sidebar } = parsed;
10621
10677
  return {
10622
10678
  ...typeof askWidth === "number" && Number.isFinite(askWidth) ? { askWidth: clampAskWidth(askWidth) } : {},
10623
10679
  ...Array.isArray(dismissedNotices) ? { dismissedNotices: dismissedNotices.filter((key) => typeof key === "string" && NOTICE_KEY.test(key)) } : {},
@@ -10626,7 +10682,8 @@ function readPreferences() {
10626
10682
  ...isTheme(theme) ? { theme } : {},
10627
10683
  ...readReviewSections(reviewSections),
10628
10684
  ...readRunsSections(runsSections),
10629
- ...readLayout(reviewLayout)
10685
+ ...readLayout(reviewLayout),
10686
+ ...readSidebar(sidebar)
10630
10687
  };
10631
10688
  } catch {
10632
10689
  return {};
@@ -10659,6 +10716,35 @@ function writeReviewLayout(patch) {
10659
10716
  writeFileSync(PREFERENCES_PATH, `${JSON.stringify(next, null, 2)}\n`);
10660
10717
  } catch {}
10661
10718
  }
10719
+ /** Only the two booleans, and only when one is set; an empty object is no preference. */
10720
+ function readSidebar(value) {
10721
+ if (!value || typeof value !== "object") return {};
10722
+ const { compact, hidden } = value;
10723
+ const sidebar = {
10724
+ ...typeof compact === "boolean" ? { compact } : {},
10725
+ ...typeof hidden === "boolean" ? { hidden } : {}
10726
+ };
10727
+ return Object.keys(sidebar).length === 0 ? {} : { sidebar };
10728
+ }
10729
+ /** The sidebar fields a request may set: a boolean or nothing, each. */
10730
+ function readSidebarPatch(value) {
10731
+ const { sidebar } = readSidebar(value);
10732
+ return sidebar ?? null;
10733
+ }
10734
+ /** Remember a change to the sidebar, merged into what is stored. Read-modify-write, like every other field here. */
10735
+ function writeSidebar(patch) {
10736
+ try {
10737
+ mkdirSync(STORE_ROOT, { recursive: true });
10738
+ const current = readPreferences();
10739
+ writeFileSync(PREFERENCES_PATH, `${JSON.stringify({
10740
+ ...current,
10741
+ sidebar: {
10742
+ ...current.sidebar,
10743
+ ...patch
10744
+ }
10745
+ }, null, 2)}\n`);
10746
+ } catch {}
10747
+ }
10662
10748
  /** Remember whether "Your prompts" is folded. Read-modify-write, like every other field here. */
10663
10749
  function writePromptsCollapsed(collapsed) {
10664
10750
  try {
@@ -10879,7 +10965,7 @@ function renderSettingsPage(view) {
10879
10965
  const { settings } = view;
10880
10966
  const address = `http://localhost:${settings.port.configured}`;
10881
10967
  return `<!doctype html>
10882
- <html lang="en"${themeAttribute(settings.theme)}>
10968
+ <html lang="en"${themeAttribute(settings.theme)}${sidebarAttributes(view.sidebar)}>
10883
10969
  <head>
10884
10970
  <meta charset="utf-8">
10885
10971
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -10898,7 +10984,8 @@ ${renderContextBar("Settings", renderBack())}
10898
10984
  <div class="prb-shell">
10899
10985
  ${renderWorkspaceRail({
10900
10986
  current: "settings",
10901
- unread: view.unreadNotifications ?? 0
10987
+ unread: view.unreadNotifications ?? 0,
10988
+ sidebar: view.sidebar
10902
10989
  })}
10903
10990
  <main class="prb-index prb-settings">
10904
10991
  <div class="prb-page-panel">
@@ -11399,8 +11486,10 @@ var TOPICS = [
11399
11486
  <li><code>prreviewbuddy review --pr &lt;url&gt;</code> for a pull request</li>
11400
11487
  <li><code>prreviewbuddy open</code> to list the reviews you have and open this workspace</li>
11401
11488
  </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
11489
+ <p>Or click <b>New review</b> at the top of the sidebar, or press <kbd class="prb-kbd">⌘P</kbd>
11490
+ (<kbd class="prb-kbd">Ctrl+P</kbd> on other platforms), to start a review from anywhere in the
11491
+ workspace. With the sidebar collapsed, New review is the <b>+</b> at its top, and with the
11492
+ sidebar hidden, the <b>+</b> in the top toolbar. The panel follows the reviews it started while it stays open, and closing it
11404
11493
  stops nothing. <code>prreviewbuddy help</code> lists every command, and
11405
11494
  <code>prreviewbuddy review --help</code> every option a review takes.</p>
11406
11495
  <p>There is no account and no API key. PR Review Buddy uses the coding agent installed on your
@@ -11498,18 +11587,13 @@ feature: E</pre>
11498
11587
  {
11499
11588
  id: "collections",
11500
11589
  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>
11590
+ body: `<p>Collections help you organise related reviews, such as a refactor, release or feature.</p>
11591
+ <p>Create a collection from the Reviews page, then add reviews individually or select several and
11592
+ use <b>Manage collections</b>. A review can belong to multiple collections.</p>
11593
+ <p>Collections show reviews that still need attention. When you mark a review <b>Done</b>, it moves
11594
+ under <b>Show completed</b> within its collection, without losing its membership.</p>
11595
+ <p>You can collapse collections to stay focused, and expand them when you're ready to continue.
11596
+ Removing a review from all collections returns it to the normal review sections.</p>
11513
11597
  <p>Reviews in a collection are kept for ninety days, like pinned and Done reviews, counted from
11514
11598
  when you last opened or marked them. Adding one to a collection doesn't restart that count.
11515
11599
  Deleting a collection keeps its reviews.</p>`
@@ -11599,7 +11683,7 @@ function renderContents() {
11599
11683
  }
11600
11684
  function renderHelpPage(view) {
11601
11685
  return `<!doctype html>
11602
- <html lang="en"${themeAttribute(view.theme)}>
11686
+ <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
11603
11687
  <head>
11604
11688
  <meta charset="utf-8">
11605
11689
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -11618,7 +11702,8 @@ ${renderContextBar("Help", "")}
11618
11702
  <div class="prb-shell">
11619
11703
  ${renderWorkspaceRail({
11620
11704
  current: "help",
11621
- unread: view.unreadNotifications ?? 0
11705
+ unread: view.unreadNotifications ?? 0,
11706
+ sidebar: view.sidebar
11622
11707
  })}
11623
11708
  <main class="prb-index prb-help">
11624
11709
  <div class="prb-page-panel">
@@ -11640,7 +11725,7 @@ ${renderWorkspaceRail({
11640
11725
  function renderNotificationsPage(view) {
11641
11726
  const unread = view.notifications.filter((notification) => notification.readAt === void 0).length;
11642
11727
  return `<!doctype html>
11643
- <html lang="en"${themeAttribute(view.theme)}>
11728
+ <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
11644
11729
  <head>
11645
11730
  <meta charset="utf-8">
11646
11731
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -11659,7 +11744,8 @@ ${renderContextBar("Activity", view.notifications.length === 0 ? "" : renderRead
11659
11744
  <div class="prb-shell">
11660
11745
  ${renderWorkspaceRail({
11661
11746
  current: "notifications",
11662
- unread: view.unreadNotifications ?? 0
11747
+ unread: view.unreadNotifications ?? 0,
11748
+ sidebar: view.sidebar
11663
11749
  })}
11664
11750
  <main class="prb-index prb-notifications-page">
11665
11751
  ${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 +13130,7 @@ function renderRunsPage(view) {
13044
13130
  const nothing = inProgress.length === 0 && recent.length === 0;
13045
13131
  const notResponding = recent.some((entry) => entry.status === "not-responding");
13046
13132
  return `<!doctype html>
13047
- <html lang="en"${themeAttribute(view.theme)}>
13133
+ <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
13048
13134
  <head>
13049
13135
  <meta charset="utf-8">
13050
13136
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -13063,7 +13149,8 @@ ${renderContextBar("Runs", nothing ? "" : renderControls(notResponding))}
13063
13149
  <div class="prb-shell">
13064
13150
  ${renderWorkspaceRail({
13065
13151
  current: "runs",
13066
- unread: view.unreadNotifications ?? 0
13152
+ unread: view.unreadNotifications ?? 0,
13153
+ sidebar: view.sidebar
13067
13154
  })}
13068
13155
  <main class="prb-index prb-runs">
13069
13156
  <p class="prb-runs-lede">Monitor review executions and recent activity.</p>
@@ -14999,6 +15086,7 @@ async function handle(req, res, options) {
14999
15086
  if (url.pathname === "/notifications/live") return handleLiveNotifications(req, res, url.searchParams, options.indexToken);
15000
15087
  if (url.pathname === "/pin") return handlePin(req, res, options.indexToken);
15001
15088
  if (url.pathname === "/sections") return handleSections(req, res, options.indexToken);
15089
+ if (url.pathname === "/sidebar") return handleSidebar(req, res, options.indexToken);
15002
15090
  if (url.pathname === "/queue") return handleQueue(req, res, options.indexToken);
15003
15091
  if (url.pathname === "/collections") return handleCollections(req, res, options.indexToken);
15004
15092
  if (url.pathname === "/collections/members") return handleCollectionMembers(req, res, options.indexToken);
@@ -15148,6 +15236,7 @@ function indexView() {
15148
15236
  unreadNotifications: unreadCount$1(),
15149
15237
  driving: isDriving(),
15150
15238
  theme: readPreferences().theme,
15239
+ sidebar: readPreferences().sidebar,
15151
15240
  sectionsCollapsed: readPreferences().reviewSections
15152
15241
  };
15153
15242
  }
@@ -15238,7 +15327,8 @@ async function handleSettings(req, res, params, indexToken) {
15238
15327
  sendHtml(res, 200, renderSettingsPage({
15239
15328
  settings: readSettings(),
15240
15329
  unreadNotifications: unreadCount$1(),
15241
- driving: isDriving()
15330
+ driving: isDriving(),
15331
+ sidebar: readPreferences().sidebar
15242
15332
  }), indexCookieHeader(indexToken));
15243
15333
  }
15244
15334
  /**
@@ -15250,7 +15340,8 @@ function handleHelp(req, res, params, indexToken) {
15250
15340
  sendHtml(res, 200, renderHelpPage({
15251
15341
  unreadNotifications: unreadCount$1(),
15252
15342
  driving: isDriving(),
15253
- theme: readPreferences().theme
15343
+ theme: readPreferences().theme,
15344
+ sidebar: readPreferences().sidebar
15254
15345
  }), indexCookieHeader(indexToken));
15255
15346
  }
15256
15347
  /**
@@ -15365,7 +15456,8 @@ async function handleNotifications(req, res, params, indexToken) {
15365
15456
  gone: new Set(notifications.filter((entry) => !loadWorkspace(entry.reviewId)).map((entry) => entry.id)),
15366
15457
  unreadNotifications: unreadCount$1(),
15367
15458
  driving: isDriving(),
15368
- theme: readPreferences().theme
15459
+ theme: readPreferences().theme,
15460
+ sidebar: readPreferences().sidebar
15369
15461
  }), indexCookieHeader(indexToken));
15370
15462
  }
15371
15463
  /**
@@ -15386,6 +15478,7 @@ function handleRuns(req, res, params, indexToken) {
15386
15478
  unreadNotifications: unreadCount$1(),
15387
15479
  driving: isDriving(),
15388
15480
  theme: preferences.theme,
15481
+ sidebar: preferences.sidebar,
15389
15482
  sectionsCollapsed: preferences.runsSections
15390
15483
  }), indexCookieHeader(indexToken));
15391
15484
  }
@@ -15799,6 +15892,18 @@ async function handleRunsSections(req, res, indexToken) {
15799
15892
  writeRunsSectionCollapsed(body.section, body.collapsed);
15800
15893
  send(res, 200, { ok: true });
15801
15894
  }
15895
+ /**
15896
+ * The sidebar, from a workspace page that is not a review, with the same credential as `/sections`.
15897
+ * A review page sends the same field to its own `/prefs`, since it may hold no index cookie.
15898
+ */
15899
+ async function handleSidebar(req, res, indexToken) {
15900
+ if (req.method !== "POST") return send(res, 405, { error: "Use POST." });
15901
+ if (!indexToken || !secretMatches(indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
15902
+ const sidebar = readSidebarPatch((await readJson(req))?.sidebar);
15903
+ if (!sidebar) return send(res, 400, { error: "Not a sidebar setting." });
15904
+ writeSidebar(sidebar);
15905
+ send(res, 200, { ok: true });
15906
+ }
15802
15907
  async function handleSections(req, res, indexToken) {
15803
15908
  if (req.method !== "POST") return send(res, 405, { error: "Use POST." });
15804
15909
  if (!indexToken || !secretMatches(indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
@@ -16205,6 +16310,7 @@ async function sendWorkspacePage(res, workspace) {
16205
16310
  repoPath: workspace.repoPath,
16206
16311
  askWidth: readPreferences().askWidth,
16207
16312
  theme: readPreferences().theme,
16313
+ sidebar: readPreferences().sidebar,
16208
16314
  dismissedNotices: readPreferences().dismissedNotices,
16209
16315
  promptsCollapsed: readPreferences().promptsCollapsed === true,
16210
16316
  reviewLayout: readPreferences().reviewLayout ?? {},
@@ -17008,6 +17114,8 @@ async function handlePrefs(req, res) {
17008
17114
  if (typeof askWidth === "number") writeAskWidth(askWidth);
17009
17115
  else if (askWidth === null) clearAskWidth();
17010
17116
  if (typeof body?.promptsCollapsed === "boolean") writePromptsCollapsed(body.promptsCollapsed);
17117
+ const sidebar = readSidebarPatch(body?.sidebar);
17118
+ if (sidebar) writeSidebar(sidebar);
17011
17119
  const layout = readReviewLayoutPatch(body?.reviewLayout);
17012
17120
  if (layout) writeReviewLayout(layout);
17013
17121
  if (body?.changeMapRegister === "simple" || body?.changeMapRegister === "technical") writeChangeMapRegister(body.changeMapRegister);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.82.4",
3
+ "version": "0.83.3",
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",
@@ -1 +1 @@
1
- import{dt as e}from"./chunk-CmdZYpyj.js";var t=`button:not([disabled])`;function n(e){return e.querySelector(`[data-menu-panel]`)}function r(e){return e.querySelector(`[data-menu-trigger]`)}function i(e){let t=n(e);return t?[...t.querySelectorAll(`input:not([disabled]), textarea:not([disabled]), button:not([disabled])`)].filter(e=>!e.closest(`[hidden]`)):[]}function a(e){let r=n(e);return r?[...r.querySelectorAll(t)].filter(e=>!e.closest(`[hidden]`)):[]}var o=`data-menu-placement`;function s(e){let t=e.ownerDocument.defaultView,n=0,r=t?.innerHeight??1/0;for(let i=e.parentElement;i&&t;i=i.parentElement){let e=t.getComputedStyle(i).overflowY;if(e===`visible`||e===``)continue;let a=i.getBoundingClientRect();n=Math.max(n,a.top),r=Math.min(r,a.bottom)}return{top:n,bottom:r}}var c=8,l=120;function u(e){let t=n(e),i=r(e);if(!t||!i)return;t.removeAttribute(o),t.style.removeProperty(`max-height`),t.style.removeProperty(`overflow-y`),t.style.removeProperty(`box-sizing`),t.scrollTop=0;let a=t.getBoundingClientRect(),u=i.getBoundingClientRect(),d=s(e),f=Math.max(0,a.top-u.bottom),p=d.bottom-u.bottom-f-c,m=u.top-d.top-f-c,h=a.height,g=h<=p?`below`:h<=m||m>p?`above`:`below`;g===`above`&&t.setAttribute(o,`above`);let _=g===`above`?m:p;h<=_||(t.style.boxSizing=`border-box`,t.style.maxHeight=`${Math.max(Math.floor(_),l)}px`,t.style.overflowY=`auto`)}function d(e){return!e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.shiftKey?e.key===`j`?`ArrowDown`:e.key===`k`?`ArrowUp`:e.key:e.key}function f(t,o={}){let s=null,c=null,l=o.within?`${o.within} [data-menu]`:`[data-menu]`,f=e=>e&&(!o.within||e.closest(o.within))?e:null,p=(e,t={})=>{let i=n(e);if(!i||i.hidden)return;i.hidden=!0;let a=r(e);a?.setAttribute(`aria-expanded`,`false`);let l=s===e?c:null;s===e&&(s=null,c=null),o.onClose?.(e),t.returnFocus&&(l??(()=>a?.focus()))()},m=()=>{for(let e of t.querySelectorAll(l))p(e)},h=(e,t,i=null)=>{let l=n(e);l&&(m(),c=i,o.onOpen?.(e),l.hidden=!1,u(e),r(e)?.setAttribute(`aria-expanded`,`true`),s=e,t&&a(e)[0]?.focus())};return t.addEventListener(`click`,e=>{let t=e.target,r=f(t?.closest?.(`[data-menu]`)??null);if(!r){m();return}if(t?.closest(`[data-menu-trigger]`)){let t=e.detail===0;n(r)?.hidden?h(r,t):p(r);return}s&&s!==r&&p(s)}),t.addEventListener(`keydown`,t=>{if(s&&!s.isConnected&&(s=null),!s)return;let n=s;if(t.key===`Escape`){t.preventDefault(),p(n,{returnFocus:!0});return}if(!n.contains(t.target))return;if(t.key===`Tab`){if(n.hasAttribute(`data-menu-form`)){let e=i(n),r=e.indexOf(t.target);if(e.length>0){t.preventDefault(),e[(r+(t.shiftKey?-1:1)+e.length)%e.length].focus();return}}p(n);return}if(e(t.target))return;let r=a(n);if(r.length===0)return;let o=r.indexOf(t.target),c=d(t),l=c===`ArrowDown`?r[(o+1)%r.length]:c===`ArrowUp`?r[(o-1+r.length)%r.length]:c===`Home`?r[0]:c===`End`?r[r.length-1]:null;l&&(t.preventDefault(),l.focus(),l.scrollIntoView({block:`nearest`}))}),{open:(e,t={})=>h(e,t.focusFirst===!0,t.returnFocus??null),close:p,closeAll:m}}function p(e){return e<3e4?1e3:e<12e4?3e3:e<6e5?8e3:15e3}export{f as n,p as t};
1
+ import{dt as e}from"./chunk-DwdwtB9r.js";var t=`button:not([disabled])`;function n(e){return e.querySelector(`[data-menu-panel]`)}function r(e){return e.querySelector(`[data-menu-trigger]`)}function i(e){let t=n(e);return t?[...t.querySelectorAll(`input:not([disabled]), textarea:not([disabled]), button:not([disabled])`)].filter(e=>!e.closest(`[hidden]`)):[]}function a(e){let r=n(e);return r?[...r.querySelectorAll(t)].filter(e=>!e.closest(`[hidden]`)):[]}var o=`data-menu-placement`;function s(e){let t=e.ownerDocument.defaultView,n=0,r=t?.innerHeight??1/0;for(let i=e.parentElement;i&&t;i=i.parentElement){let e=t.getComputedStyle(i).overflowY;if(e===`visible`||e===``)continue;let a=i.getBoundingClientRect();n=Math.max(n,a.top),r=Math.min(r,a.bottom)}return{top:n,bottom:r}}var c=8,l=120;function u(e){let t=n(e),i=r(e);if(!t||!i)return;t.removeAttribute(o),t.style.removeProperty(`max-height`),t.style.removeProperty(`overflow-y`),t.style.removeProperty(`box-sizing`),t.scrollTop=0;let a=t.getBoundingClientRect(),u=i.getBoundingClientRect(),d=s(e),f=Math.max(0,a.top-u.bottom),p=d.bottom-u.bottom-f-c,m=u.top-d.top-f-c,h=a.height,g=h<=p?`below`:h<=m||m>p?`above`:`below`;g===`above`&&t.setAttribute(o,`above`);let _=g===`above`?m:p;h<=_||(t.style.boxSizing=`border-box`,t.style.maxHeight=`${Math.max(Math.floor(_),l)}px`,t.style.overflowY=`auto`)}function d(e){return!e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.shiftKey?e.key===`j`?`ArrowDown`:e.key===`k`?`ArrowUp`:e.key:e.key}function f(t,o={}){let s=null,c=null,l=o.within?`${o.within} [data-menu]`:`[data-menu]`,f=e=>e&&(!o.within||e.closest(o.within))?e:null,p=(e,t={})=>{let i=n(e);if(!i||i.hidden)return;i.hidden=!0;let a=r(e);a?.setAttribute(`aria-expanded`,`false`);let l=s===e?c:null;s===e&&(s=null,c=null),o.onClose?.(e),t.returnFocus&&(l??(()=>a?.focus()))()},m=()=>{for(let e of t.querySelectorAll(l))p(e)},h=(e,t,i=null)=>{let l=n(e);l&&(m(),c=i,o.onOpen?.(e),l.hidden=!1,u(e),r(e)?.setAttribute(`aria-expanded`,`true`),s=e,t&&a(e)[0]?.focus())};return t.addEventListener(`click`,e=>{let t=e.target,r=f(t?.closest?.(`[data-menu]`)??null);if(!r){m();return}if(t?.closest(`[data-menu-trigger]`)){let t=e.detail===0;n(r)?.hidden?h(r,t):p(r);return}s&&s!==r&&p(s)}),t.addEventListener(`keydown`,t=>{if(s&&!s.isConnected&&(s=null),!s)return;let n=s;if(t.key===`Escape`){t.preventDefault(),p(n,{returnFocus:!0});return}if(!n.contains(t.target))return;if(t.key===`Tab`){if(n.hasAttribute(`data-menu-form`)){let e=i(n),r=e.indexOf(t.target);if(e.length>0){t.preventDefault(),e[(r+(t.shiftKey?-1:1)+e.length)%e.length].focus();return}}p(n);return}if(e(t.target))return;let r=a(n);if(r.length===0)return;let o=r.indexOf(t.target),c=d(t),l=c===`ArrowDown`?r[(o+1)%r.length]:c===`ArrowUp`?r[(o-1+r.length)%r.length]:c===`Home`?r[0]:c===`End`?r[r.length-1]:null;l&&(t.preventDefault(),l.focus(),l.scrollIntoView({block:`nearest`}))}),{open:(e,t={})=>h(e,t.focusFirst===!0,t.returnFocus??null),close:p,closeAll:m}}function p(e){return e<3e4?1e3:e<12e4?3e3:e<6e5?8e3:15e3}export{f as n,p as t};
@@ -1 +1 @@
1
- import{F as e}from"./chunk-CmdZYpyj.js";function t(e=document){for(let t of e.querySelectorAll(`.prb-index-status`))t.addEventListener(`click`,e=>{let n=t.querySelector(`select`);if(!(!n||e.target instanceof Node&&n.contains(e.target))){e.preventDefault(),n.focus();try{n.showPicker()}catch{}}})}function n(t,n){r(t,t=>e(t.dataset.section)?t.dataset.section:void 0,n)}function r(e,t,n){let r=Promise.resolve();for(let i of e){let e=t(i);if(e===void 0)continue;let a=i.open;i.addEventListener(`toggle`,()=>{if(i.open===a)return;a=i.open;let t=!i.open;r=r.then(()=>n(e,t)).catch(()=>{})})}}export{n,t as r,r as t};
1
+ import{F as e}from"./chunk-DwdwtB9r.js";function t(e=document){for(let t of e.querySelectorAll(`.prb-index-status`))t.addEventListener(`click`,e=>{let n=t.querySelector(`select`);if(!(!n||e.target instanceof Node&&n.contains(e.target))){e.preventDefault(),n.focus();try{n.showPicker()}catch{}}})}function n(t,n){r(t,t=>e(t.dataset.section)?t.dataset.section:void 0,n)}function r(e,t,n){let r=Promise.resolve();for(let i of e){let e=t(i);if(e===void 0)continue;let a=i.open;i.addEventListener(`toggle`,()=>{if(i.open===a)return;a=i.open;let t=!i.open;r=r.then(()=>n(e,t)).catch(()=>{})})}}export{n,t as r,r as t};