prreviewbuddy 0.50.0 → 0.50.1

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.50.1
4
+
5
+ **Copy and Ask Buddy look and behave as a pair.** On every issue and question, both icons are the
6
+ same grey at rest and share one soft highlight when you point at them. Copy no longer turns purple on
7
+ hover. Ask Buddy turns purple only while its menu is open. After you copy, the icon turns into a green
8
+ check for a moment and "Copied" appears just below it. It used to appear beside it, on top of Ask
9
+ Buddy. "Copy failed" shows in the same place.
10
+
11
+ The questions you can ask Buddy AI about a finding or a selection, and the wording that tells Buddy
12
+ which theme a question was asked in, now live in the shared review engine, so the browser extension
13
+ offers the same ones.
14
+
15
+ In a short window, the list of questions you can ask about a selection now scrolls instead of being
16
+ cut off at the bottom. Escape closes a menu again after the page has redrawn around it.
17
+
3
18
  ## 0.50.0
4
19
 
5
20
  **Every review says which commits it covers.** Beside the branch at the top of a review is its
package/dist/server.js CHANGED
@@ -208,6 +208,30 @@ function resolveSectionId(run, sectionId) {
208
208
  return run.result.sections.some((section) => section.id === sectionId) ? namespaced(run.id, sectionId) : sectionId;
209
209
  }
210
210
  //#endregion
211
+ //#region ../../packages/review-harness/src/domain/ai_review/ask_scope.ts
212
+ /**
213
+ * Where an earlier question was asked, which the question alone does not say.
214
+ *
215
+ * "What is risky here?" in one theme followed by "and how does that compare?" in another reads, as
216
+ * bare text, as two questions about the same thing. `undefined` is a question recorded before
217
+ * scopes were, and gets no label: calling it "the whole change" would state something nobody
218
+ * recorded. A theme that has since gone, because the review was updated, is said to have gone.
219
+ */
220
+ function askedWhere(sections, sectionId) {
221
+ if (sectionId === void 0) return null;
222
+ if (sectionId === null) return "(asked about the whole change)";
223
+ const section = sections.find((s) => s.id === sectionId);
224
+ return section ? `(asked while reading the "${section.title}" theme)` : "(asked while reading a theme that is no longer in the review)";
225
+ }
226
+ /**
227
+ * The theme the reviewer has open, named rather than used to filter what the model sees: they
228
+ * routinely ask "does this interact with X", where X is in another theme.
229
+ */
230
+ function readingThemeLine(sections, sectionId) {
231
+ const section = sectionId === null ? void 0 : sections.find((s) => s.id === sectionId);
232
+ return section ? `The reviewer is currently reading the "${section.title}" theme. Answer with that in view, but do not ignore the rest of the change if it bears on the question.` : null;
233
+ }
234
+ //#endregion
211
235
  //#region ../../packages/review-harness/src/domain/utils/html.ts
212
236
  function escapeHtml$1(value) {
213
237
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -773,9 +797,10 @@ var FINDING_COPY_CLASS = "pr-helper-finding-copy";
773
797
  * buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
774
798
  * would give a utility the weight of the title beside it. What it does is said by a hover
775
799
  * tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
776
- * accessible name. "Copied" and "Copy failed" still show as words, because a picture cannot say
777
- * that the click did something. The resting name is kept on the button so the reset returns to
778
- * "Copy finding" rather than to a chat reply's "Copy message".
800
+ * accessible name. After a click it shows a green check and says "Copied" (or "Copy failed") in
801
+ * the bubble below it, the words used everywhere else, but not inline: beside it is Ask Buddy. The
802
+ * resting name and tip are kept on the button so the reset returns to "Copy finding" rather than to
803
+ * a chat reply's "Copy message".
779
804
  */
780
805
  function renderFindingCopySource(source, kind) {
781
806
  const name = kind === "issue" ? "Copy finding" : "Copy question";
@@ -788,11 +813,12 @@ function renderFindingCopySource(source, kind) {
788
813
  /** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
789
814
  var IDLE_LABEL_ATTR = "data-copy-idle-label";
790
815
  var IDLE_ARIA_ATTR = "data-copy-idle-aria";
816
+ var IDLE_TIP_ATTR = "data-copy-idle-tip";
791
817
  function button(handle, idle) {
792
818
  const { icon } = STATES$1.idle;
793
819
  const label = idle?.label ?? STATES$1.idle.label;
794
820
  const aria = idle?.aria ?? STATES$1.idle.aria;
795
- const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}"` : "";
821
+ const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}" ${IDLE_TIP_ATTR}="${escapeHtml$1(idle.tip)}"` : "";
796
822
  const control = `<button type="button" class="${idle ? `${MESSAGE_COPY_CLASS} ${FINDING_COPY_CLASS}` : MESSAGE_COPY_CLASS}" ${handle}${remembered}${idle ? ` data-tip="${escapeHtml$1(idle.tip)}"` : ""} aria-label="${escapeHtml$1(aria)}"><span class="${MESSAGE_COPY_CLASS}-icon" aria-hidden="true">${svg(icon)}</span><span class="${MESSAGE_COPY_CLASS}-label"${label ? "" : " hidden"}>${escapeHtml$1(label)}</span></button>`;
797
823
  return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
798
824
  }
@@ -813,8 +839,7 @@ function svg(paths) {
813
839
  * closes and positions exactly as that one does. The panel is empty here and filled when it opens,
814
840
  * because it lists the reviewer's saved prompts and those change without the page re-rendering.
815
841
  *
816
- * Workspace only. The extension renders these cards too, and there is no Buddy pane behind a card
817
- * there for this to open, so `ReviewTabOptions.askBuddy` gates it and the extension never sets it.
842
+ * The host decides where the answer appears; `ReviewTabOptions.askBuddy` says the host has wired it.
818
843
  */
819
844
  var ASK_BUDDY_LABEL = "Ask Buddy";
820
845
  /** Four-point sparkle, the conventional mark for "ask the AI". currentColor, like Copy beside it. */
@@ -8362,7 +8387,8 @@ function extractAnswer(raw) {
8362
8387
  * and wrongly.
8363
8388
  */
8364
8389
  function buildPrompt$2(workspace, question, sectionId, cwd) {
8365
- const section = workspace.session.result?.sections.find((s) => s.id === sectionId);
8390
+ const sections = workspace.session.result?.sections ?? [];
8391
+ const reading = readingThemeLine(sections, sectionId);
8366
8392
  const recent = workspace.transcript.slice(-4);
8367
8393
  return [
8368
8394
  SYSTEM_PROMPT$1,
@@ -8374,29 +8400,19 @@ function buildPrompt$2(workspace, question, sectionId, cwd) {
8374
8400
  "---",
8375
8401
  "DIFF:",
8376
8402
  workspace.diff,
8377
- ...section ? ["---", `The reviewer is currently reading the "${section.title}" theme. Answer with that in view, but do not ignore the rest of the change if it bears on the question.`] : [],
8403
+ ...reading ? ["---", reading] : [],
8378
8404
  ...recent.length > 0 ? [
8379
8405
  "---",
8380
8406
  "EARLIER IN THIS CONVERSATION:",
8381
- recent.map((entry) => `Q ${askedWhere(workspace, entry.sectionId)}: ${entry.question}\nA: ${entry.answer}`).join("\n\n")
8407
+ recent.map((entry) => {
8408
+ const where = askedWhere(sections, entry.sectionId);
8409
+ return `Q${where ? ` ${where}` : ""}: ${entry.question}\nA: ${entry.answer}`;
8410
+ }).join("\n\n")
8382
8411
  ] : [],
8383
8412
  "---",
8384
8413
  `QUESTION: ${question}`
8385
8414
  ].join("\n");
8386
8415
  }
8387
- /**
8388
- * Where an earlier question was asked, which the question alone does not say.
8389
- *
8390
- * "What is risky here?" in one theme followed by "and how does that compare?" in another reads, as
8391
- * bare text, as two questions about the same thing. A theme that has since gone, because the
8392
- * review was updated, is said to have gone rather than dropped: dropping it would present a
8393
- * theme's question as one about the whole change.
8394
- */
8395
- function askedWhere(workspace, sectionId) {
8396
- if (sectionId === null) return "(asked about the whole change)";
8397
- const section = workspace.session.result?.sections.find((s) => s.id === sectionId);
8398
- return section ? `(asked while reading the "${section.title}" theme)` : "(asked while reading a theme that is no longer in the review)";
8399
- }
8400
8416
  //#endregion
8401
8417
  //#region ../../packages/review-harness/src/workspace/assistant_runs.ts
8402
8418
  var RUN_KINDS = ["ask", "diagram"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://prreviewbuddy.com",
package/static/shell.css CHANGED
@@ -5165,10 +5165,13 @@ body .prb-body[data-focus='buddy'] .prb-msg-user { max-width: min(90%, 78ch); }
5165
5165
  width: 24px;
5166
5166
  height: 24px;
5167
5167
  margin-top: -3px;
5168
- color: var(--pane-accent);
5168
+ border-radius: 6px;
5169
+ color: var(--pane-ink-muted);
5169
5170
  }
5170
- .prb-doc .prb-ask-buddy-trigger:hover,
5171
- .prb-doc .prb-ask-buddy-trigger[aria-expanded='true'] { background: var(--pane-bg); color: var(--pane-accent-ink); }
5171
+ /* Copy beside it has the same rest, hover and size (sidebar.css): grey, a soft surface, and purple
5172
+ only while this menu is open, because then it is the AI action in play. */
5173
+ .prb-doc .prb-ask-buddy-trigger:hover { background: rgba(31, 35, 40, 0.07); color: var(--pane-ink-muted); }
5174
+ .prb-doc .prb-ask-buddy-trigger[aria-expanded='true'] { background: rgba(130, 80, 223, 0.1); color: var(--pane-accent-ink); }
5172
5175
  .prb-doc .prb-ask-buddy-trigger:focus-visible { outline: 2px solid var(--pane-accent); outline-offset: -2px; }
5173
5176
 
5174
5177
  /* The tooltip Copy uses beside it, so the two icons explain themselves the same way. */
@@ -653,6 +653,51 @@
653
653
  margin-top: 2px;
654
654
  }
655
655
 
656
+ /* The extension's cards, laid out as the workspace's are: the figure leads and the label sits
657
+ beside it, pinned to the top row so four cards align with or without a note. Only once each
658
+ card has the room: at the sidebar's narrowest a quarter-width card cannot hold "Questions" beside
659
+ its figure, and the stacked card above stays. The grid is the query container because the rule
660
+ only needs its children; nothing else in the panel is contained. Selectors carry the extension
661
+ class, so they outrank the base rules wherever those sit in the file. */
662
+ .pr-helper-extension .pr-helper-overview-stats { container-type: inline-size; }
663
+
664
+ @container (min-width: 400px) {
665
+ .pr-helper-extension .pr-helper-overview-stat {
666
+ display: grid;
667
+ grid-template-columns: auto minmax(0, 1fr);
668
+ grid-template-rows: auto 1fr;
669
+ column-gap: 8px;
670
+ align-items: start;
671
+ padding: 10px 12px;
672
+ text-align: left;
673
+ }
674
+ .pr-helper-extension .pr-helper-overview-stat-value {
675
+ grid-column: 1;
676
+ grid-row: 1 / span 2;
677
+ font-size: 24px;
678
+ font-weight: 300;
679
+ line-height: 1;
680
+ font-variant-numeric: tabular-nums;
681
+ }
682
+ .pr-helper-extension .pr-helper-overview-stat-value-wide { font-size: 15px; }
683
+ .pr-helper-extension .pr-helper-overview-stat-label {
684
+ grid-column: 2;
685
+ grid-row: 1;
686
+ justify-self: start;
687
+ /* Squares the label's cap height with the figure's top. */
688
+ margin-top: 5px;
689
+ }
690
+ .pr-helper-extension .pr-helper-overview-stat-note {
691
+ grid-column: 2;
692
+ grid-row: 2;
693
+ justify-self: start;
694
+ }
695
+ /* Into flow, ahead of the word, now that nothing is centred for it to compensate. */
696
+ .pr-helper-extension .pr-helper-overview-stat-info { position: static; transform: none; }
697
+ /* The whole card lights up as the link, so the arrow only pushed the label off the figure. */
698
+ .pr-helper-extension .pr-helper-overview-stat-arrow { display: none; }
699
+ }
700
+
656
701
  /* Explains a capped review, and only ever appears on one. Sits ahead of the 11px label rather
657
702
  than beside the value, so it's sized to that row — against the bold number it read as a
658
703
  glyph belonging to the count. */
@@ -895,11 +940,30 @@
895
940
  .pr-helper-finding-copy {
896
941
  position: relative;
897
942
  flex-shrink: 0;
898
- margin-top: -1px;
899
943
  opacity: 1;
900
944
  font-size: 12px;
901
945
  }
902
946
 
947
+ /* Copy and Ask Buddy are a pair and speak one interaction language, on both surfaces: the same
948
+ 24px target and 6px corner, neutral grey at rest, and a soft surface on hover with the icon
949
+ colour left alone. Colour is kept for meaning: purple for an Ask Buddy menu that is open, and
950
+ the app's green or red for what Copy just did (.pr-helper-msg-copy.copied / .failed below). Pulled up 3px so the box centres on the title's first line.
951
+ Doubled class: the chat's .pr-helper-msg-copy rules sit later in this file and would win a tie. */
952
+ .pr-helper-msg-copy.pr-helper-finding-copy {
953
+ justify-content: center;
954
+ width: 24px;
955
+ height: 24px;
956
+ margin-top: -3px;
957
+ padding: 0;
958
+ border-radius: 6px;
959
+ color: var(--color-fg-muted, #57606a);
960
+ }
961
+ .pr-helper-finding-copy:hover:not(.copied):not(.failed) {
962
+ background: rgba(31, 35, 40, 0.07);
963
+ color: var(--color-fg-muted, #57606a);
964
+ }
965
+ .pr-helper-finding-copy:focus-visible { outline: 2px solid #9333ea; outline-offset: -2px; }
966
+
903
967
  /* The tooltip grows leftward from the right edge, back over the card rather than out of the pane,
904
968
  and stays on one line. The same look, delay and text reset as `.pr-helper-tip`, which it cannot
905
969
  reuse: that one pins itself to its positioned row, and this control is its own anchor. */
@@ -929,10 +993,82 @@
929
993
  transition-delay: 0.35s;
930
994
  }
931
995
 
932
- /* The button is saying "Copied" or "Copy failed" in words right now; a tooltip offering to copy
933
- would argue with it. */
996
+ /* After a click the bubble carries "Copied" or "Copy failed" and shows without waiting for the
997
+ pointer: below the button rather than beside it, where Ask Buddy sits. */
934
998
  .pr-helper-finding-copy.copied::after,
935
- .pr-helper-finding-copy.failed::after { display: none; }
999
+ .pr-helper-finding-copy.failed::after { opacity: 1; transition-delay: 0s; }
1000
+
1001
+ /* Ask Buddy on a finding card, in the extension: the sparkle beside Copy, and its menu. Scoped to
1002
+ .pr-helper-extension, which only the extension's root carries: the workspace loads this sheet
1003
+ too, its review document is also a .pr-helper-sidebar, and it styles its own menu in shell.css
1004
+ under .prb-doc. The panel matches the selection popover, which offers the same list. */
1005
+ .pr-helper-extension .prb-ask-buddy { position: relative; flex-shrink: 0; }
1006
+ .pr-helper-extension .prb-ask-buddy-trigger {
1007
+ position: relative;
1008
+ display: inline-flex;
1009
+ align-items: center;
1010
+ justify-content: center;
1011
+ width: 24px;
1012
+ height: 24px;
1013
+ margin-top: -3px;
1014
+ padding: 0;
1015
+ border: 0;
1016
+ border-radius: 6px;
1017
+ background: transparent;
1018
+ color: var(--color-fg-muted, #57606a);
1019
+ cursor: pointer;
1020
+ }
1021
+ .pr-helper-extension .prb-ask-buddy-trigger:hover { background: rgba(31, 35, 40, 0.07); }
1022
+ .pr-helper-extension .prb-ask-buddy-trigger[aria-expanded='true'] { background: rgba(147, 51, 234, 0.1); color: #9333ea; }
1023
+ .pr-helper-extension .prb-ask-buddy-trigger:focus-visible { outline: 2px solid #9333ea; outline-offset: -2px; }
1024
+ .pr-helper-extension .prb-ask-buddy-trigger::after {
1025
+ content: attr(data-tip);
1026
+ position: absolute;
1027
+ top: calc(100% + 6px);
1028
+ right: 0;
1029
+ z-index: 3;
1030
+ padding: 5px 8px;
1031
+ border-radius: 6px;
1032
+ background: #24292f;
1033
+ color: #ffffff;
1034
+ font-size: 11px;
1035
+ font-weight: 400;
1036
+ font-family: inherit;
1037
+ line-height: 1.4;
1038
+ white-space: nowrap;
1039
+ opacity: 0;
1040
+ pointer-events: none;
1041
+ transition: opacity 0.1s;
1042
+ }
1043
+ .pr-helper-extension .prb-ask-buddy-trigger:hover::after,
1044
+ .pr-helper-extension .prb-ask-buddy-trigger:focus-visible::after { opacity: 1; transition-delay: 0.35s; }
1045
+ .pr-helper-extension .prb-ask-buddy-trigger[aria-expanded='true']::after { display: none; }
1046
+ .pr-helper-extension .prb-ask-buddy-panel {
1047
+ position: absolute;
1048
+ top: 26px;
1049
+ right: 0;
1050
+ z-index: 5;
1051
+ width: 240px;
1052
+ max-width: calc(100vw - 48px);
1053
+ padding: 4px;
1054
+ border: 1px solid rgba(0, 0, 0, 0.12);
1055
+ border-radius: 8px;
1056
+ background: #ffffff;
1057
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
1058
+ text-align: left;
1059
+ }
1060
+ /* The card whose menu is open comes forward. A dismissed card's opacity makes it its own layer,
1061
+ where the panel's z-index counts only inside the card: the next card's Copy and sparkle painted
1062
+ across the open menu, and the menu was drawn faded. */
1063
+ .pr-helper-extension [data-finding-card]:has(.prb-ask-buddy-trigger[aria-expanded='true']) {
1064
+ position: relative;
1065
+ z-index: 6;
1066
+ opacity: 1;
1067
+ }
1068
+ .pr-helper-extension .prb-ask-buddy-panel .pr-helper-selection-menu-item { display: block; width: 100%; text-align: left; }
1069
+ .pr-helper-extension .prb-ask-buddy-panel .pr-helper-selection-menu-item[disabled] { opacity: 0.5; cursor: default; }
1070
+ /* Arrow keys move focus through these items, so the focus has to show. */
1071
+ .pr-helper-extension .prb-ask-buddy-panel .pr-helper-selection-menu-item:focus-visible { outline: 2px solid #9333ea; outline-offset: -2px; }
936
1072
 
937
1073
  .pr-helper-issue-desc {
938
1074
  font-size: 13px;
@@ -2050,6 +2186,15 @@
2050
2186
 
2051
2187
  .pr-helper-markdown a:hover { text-decoration: underline; }
2052
2188
 
2189
+ /* State, not decoration: it changes what the next question means. Wraps rather than truncates. */
2190
+ .pr-helper-ask-scope {
2191
+ margin: 0 0 6px;
2192
+ font-size: 12px;
2193
+ color: var(--color-fg-muted, #57606a);
2194
+ overflow-wrap: anywhere;
2195
+ }
2196
+ .pr-helper-ask-scope-value { color: var(--color-fg-default, #24292f); }
2197
+
2053
2198
  .pr-helper-chat-input-row {
2054
2199
  display: flex;
2055
2200
  gap: 6px;
@@ -2133,7 +2278,8 @@
2133
2278
 
2134
2279
  /* Deliberately quieter than the chips it reveals: once the reviewer is typing their own
2135
2280
  questions, this is a door they may never open again, not a call to action. */
2136
- .pr-helper-suggestions-toggle {
2281
+ .pr-helper-suggestions-toggle,
2282
+ .pr-helper-saved-prompts-toggle {
2137
2283
  display: flex;
2138
2284
  align-items: center;
2139
2285
  gap: 5px;
@@ -2149,9 +2295,11 @@
2149
2295
  cursor: pointer;
2150
2296
  }
2151
2297
 
2152
- .pr-helper-suggestions-toggle:hover { color: #9333ea; }
2298
+ .pr-helper-suggestions-toggle:hover,
2299
+ .pr-helper-saved-prompts-toggle:hover { color: #9333ea; }
2153
2300
 
2154
- .pr-helper-suggestions-toggle::before {
2301
+ .pr-helper-suggestions-toggle::before,
2302
+ .pr-helper-saved-prompts-toggle::before {
2155
2303
  content: '';
2156
2304
  width: 0;
2157
2305
  height: 0;
@@ -2161,10 +2309,12 @@
2161
2309
  transition: transform 0.15s ease;
2162
2310
  }
2163
2311
 
2164
- .pr-helper-suggestions-toggle[aria-expanded='true']::before { transform: rotate(90deg); }
2312
+ .pr-helper-suggestions-toggle[aria-expanded='true']::before,
2313
+ .pr-helper-saved-prompts-toggle[aria-expanded='true']::before { transform: rotate(90deg); }
2165
2314
 
2166
2315
  /* Open, the toggle is a label for the list right beneath it rather than a lone control. */
2167
- .pr-helper-suggestions-toggle[aria-expanded='true'] { margin-bottom: 8px; }
2316
+ .pr-helper-suggestions-toggle[aria-expanded='true'],
2317
+ .pr-helper-saved-prompts-toggle[aria-expanded='true'] { margin-bottom: 8px; }
2168
2318
 
2169
2319
  .pr-helper-quick-prompts {
2170
2320
  display: flex;
@@ -2187,7 +2337,13 @@
2187
2337
  .pr-helper-quick-prompt-button:hover { border-color: #9333ea; color: #9333ea; }
2188
2338
 
2189
2339
  .pr-helper-quick-prompts-section-group,
2190
- .pr-helper-quick-prompts-saved-group { display: contents; }
2340
+ /* Its own block below the suggestions, not part of their stack: that stack folds away after the
2341
+ first question, and Your prompts stays as the reviewer left it. */
2342
+ .pr-helper-quick-prompts-saved-group { margin-bottom: 8px; }
2343
+ /* .pr-helper-quick-prompts is a flex row, which beats the hidden attribute. */
2344
+ #pr-helper-saved-prompts-list[hidden] { display: none; }
2345
+ /* The count is a quantity, not part of the heading, so it drops the heading's caps and weight. */
2346
+ .pr-helper-saved-prompts-count { font-weight: 600; text-transform: none; letter-spacing: 0; }
2191
2347
 
2192
2348
  /* ── The reviewer's own prompts ──
2193
2349
  The chip and its edit control read as one object, so the border and the hover colour belong to
@@ -2887,7 +3043,9 @@
2887
3043
  transition: opacity 0.12s;
2888
3044
  }
2889
3045
 
2890
- .pr-helper-file-item:hover .pr-helper-file-stats { opacity: 0; }
3046
+ .pr-helper-file-item:hover .pr-helper-file-stats,
3047
+ .pr-helper-file-item:focus-within .pr-helper-file-stats,
3048
+ .pr-helper-file-item:has(.pr-helper-file-actions [aria-expanded='true']) .pr-helper-file-stats { opacity: 0; }
2891
3049
 
2892
3050
  /* ── Hover-reveal action buttons ── */
2893
3051
  .pr-helper-file-actions {
@@ -2902,38 +3060,85 @@
2902
3060
  transition: opacity 0.12s;
2903
3061
  }
2904
3062
 
2905
- /* Opaque too, and for the same reason as the buttons: making only the buttons solid still lets
2906
- text show through the gaps between them. The tray occludes as one block.
3063
+ /* One trigger, the row's "more actions", in the icon language Copy and Ask Buddy use: neutral grey,
3064
+ a soft surface on hover, colour only when it means something. It sits where the +/- stats are,
3065
+ which already give way on hover. Revealed by keyboard focus as well as the pointer, and held
3066
+ while its menu is open, so the menu does not vanish when the pointer leaves the row. */
3067
+ .pr-helper-file-item:hover .pr-helper-file-actions,
3068
+ .pr-helper-file-item:focus-within .pr-helper-file-actions,
3069
+ .pr-helper-file-actions:has([aria-expanded='true']) {
3070
+ opacity: 1;
3071
+ pointer-events: auto;
3072
+ }
2907
3073
 
2908
- The colour tracks the row's own state rather than being a flat white, because the row tints on
2909
- hover — #fbf7fe is that 4% purple composited over white, precomputed because a translucent
2910
- overlay on a translucent row would double the tint.
3074
+ /* Top right, level with the +/- stats it replaces on hover, rather than centred down a tall row.
3075
+ No transform: one would make the tray its own stacking context, and the next row would paint
3076
+ over the open menu. Raised while open for the same reason. */
3077
+ .pr-helper-file-actions[data-menu] {
3078
+ top: 13px;
3079
+ right: 12px;
3080
+ transform: none;
3081
+ }
3082
+ .pr-helper-file-actions:has([aria-expanded='true']) { z-index: 10; }
2911
3083
 
2912
- Bordered and shadowed, because the tray is not part of the row: it is absolutely positioned over
2913
- the top of it, occluding whatever text is underneath. Without an edge, three buttons simply
2914
- appeared mid-sentence with no account of where the words went — the outline plus the shadow say
2915
- "this is one panel, floating above", which is exactly what it is.
3084
+ .pr-helper-file-menu-trigger {
3085
+ display: inline-flex;
3086
+ align-items: center;
3087
+ justify-content: center;
3088
+ width: 24px;
3089
+ height: 24px;
3090
+ padding: 0;
3091
+ border: 0;
3092
+ border-radius: 6px;
3093
+ background: transparent;
3094
+ color: var(--color-fg-muted, #57606a);
3095
+ cursor: pointer;
3096
+ }
3097
+ .pr-helper-file-menu-trigger:hover,
3098
+ .pr-helper-file-menu-trigger[aria-expanded='true'] { background: rgba(31, 35, 40, 0.07); }
3099
+ .pr-helper-file-menu-trigger:focus-visible { outline: 2px solid #9333ea; outline-offset: -2px; }
2916
3100
 
2917
- Radius 8 against the buttons' 4: an outer corner has to be looser than the corners it contains,
2918
- or the nesting reads as a mistake. Padding is symmetric now for the same reason — the old
2919
- `0 … 14px` let the buttons sit flush against the right edge, which is invisible without a border
2920
- and lopsided with one. */
2921
- .pr-helper-file-actions {
3101
+ /* The same panel as the selection popover and a card's Ask Buddy menu. */
3102
+ .pr-helper-file-menu {
3103
+ position: absolute;
3104
+ top: 28px;
3105
+ right: 0;
3106
+ z-index: 5;
3107
+ min-width: 160px;
3108
+ padding: 4px;
3109
+ border: 1px solid rgba(0, 0, 0, 0.12);
3110
+ border-radius: 8px;
3111
+ background: #ffffff;
3112
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
3113
+ }
3114
+ .pr-helper-file-menu .pr-helper-action-btn {
3115
+ display: block;
3116
+ width: 100%;
3117
+ padding: 7px 10px;
3118
+ border: 0;
3119
+ border-radius: 6px;
3120
+ background: transparent;
3121
+ color: var(--color-fg-default, #24292f);
3122
+ font-family: inherit;
3123
+ font-size: 12px;
3124
+ text-align: left;
3125
+ cursor: pointer;
3126
+ }
3127
+ .pr-helper-file-menu .pr-helper-action-btn:hover:not([disabled]) { background: rgba(147, 51, 234, 0.1); color: #9333ea; }
3128
+ .pr-helper-file-menu .pr-helper-action-btn:focus-visible { outline: 2px solid #9333ea; outline-offset: -2px; }
3129
+ .pr-helper-file-menu .pr-helper-action-btn[disabled] { opacity: 0.5; cursor: default; }
3130
+
3131
+ /* The running state keeps the tray: "Analysing…" and Cancel float over the row's text as one
3132
+ panel, and #fbf7fe is the row's 4% purple hover tint composited over white. */
3133
+ .pr-helper-file-actions-running {
2922
3134
  background: #ffffff;
2923
3135
  padding: 4px 6px;
2924
3136
  border: 1px solid rgba(147, 51, 234, 0.25);
2925
3137
  border-radius: 8px;
2926
3138
  box-shadow: 0 2px 6px rgba(31, 35, 40, 0.10);
2927
- /* The tray's default `stretch` left the "Analysing…" label top-aligned in a box the taller
2928
- Cancel button had grown, so the two read as being on different lines. */
2929
3139
  align-items: center;
2930
3140
  }
2931
-
2932
- .pr-helper-file-item:hover .pr-helper-file-actions {
2933
- opacity: 1;
2934
- pointer-events: auto;
2935
- background: #fbf7fe;
2936
- }
3141
+ .pr-helper-file-item:hover .pr-helper-file-actions-running { background: #fbf7fe; }
2937
3142
 
2938
3143
  /* Opts out of the hover gate above, and must: this is the only indication that a follow-up is
2939
3144
  running and the only way to stop it, so a version of it you have to hover to find would leave
@@ -3365,7 +3570,9 @@
3365
3570
  border: 2px solid #9333ea;
3366
3571
  border-radius: 6px;
3367
3572
  pointer-events: none;
3368
- z-index: 999996; /* below the overlay's 999997, above ordinary page content */
3573
+ /* Below every floating control (Save for later is 999996, the overlay 999997): a decoration that
3574
+ shared a number painted its border across Save for later. Still above ordinary page content. */
3575
+ z-index: 999995;
3369
3576
  /* No dimming wash behind this. It used to carry `box-shadow: 0 0 0 2000px rgba(147,51,234,
3370
3577
  0.04)` — a full-page purple tint over everything outside the box, painted above all of
3371
3578
  GitHub's own content. At 4% it read as subtle until something saturated sat under it:
@@ -3912,8 +4119,17 @@
3912
4119
  flex-direction: column;
3913
4120
  min-width: 220px;
3914
4121
  padding: 4px;
4122
+ /* With the saved prompts the list is about 375px. position() keeps it inside the window, but a
4123
+ window shorter than the list would cut it off, so there it scrolls instead. 16px is the two
4124
+ 8px viewport margins position() leaves. */
4125
+ max-height: calc(100vh - 16px);
4126
+ overflow-y: auto;
4127
+ box-sizing: border-box;
3915
4128
  }
3916
4129
 
4130
+ /* Scrolled, not squashed: a flex column shrinks its items to fit a capped height. */
4131
+ .pr-helper-selection-popover-expanded .pr-helper-selection-menu > * { flex-shrink: 0; }
4132
+
3917
4133
  .pr-helper-selection-menu-item {
3918
4134
  padding: 7px 10px;
3919
4135
  border: none;