@vintasoftware/pr-review-canvas 0.4.0 → 0.5.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.
Files changed (83) hide show
  1. package/README.md +27 -8
  2. package/docs/reference.md +247 -62
  3. package/package.json +1 -1
  4. package/pr-review.config.example.yml +38 -4
  5. package/prompts/generation-format.md +120 -26
  6. package/prompts/generation-strict-incremental.md +53 -0
  7. package/prompts/generation-strict.md +1 -27
  8. package/prompts/generation-surfacing-incremental.md +56 -0
  9. package/prompts/generation-surfacing.md +1 -58
  10. package/prompts/judging-strict.md +27 -0
  11. package/prompts/judging-surfacing.md +58 -0
  12. package/skills/pr-review-canvas/SKILL.md +13 -4
  13. package/src/acpx/acpx.ts +98 -5
  14. package/src/acpx/models.ts +43 -0
  15. package/src/chat/chat-manager.ts +27 -1
  16. package/src/cli.ts +58 -1
  17. package/src/commands.ts +5 -1
  18. package/src/contract/api.ts +26 -1
  19. package/src/contract/canvas-manifest.ts +5 -0
  20. package/src/contract/generation-context.ts +52 -1
  21. package/src/contract/keys.ts +1 -0
  22. package/src/contract/pending.ts +49 -0
  23. package/src/contract/review-artifact.ts +50 -7
  24. package/src/contract/reviews.ts +10 -1
  25. package/src/contract/settings.ts +5 -0
  26. package/src/contract/state.ts +23 -12
  27. package/src/contract/validation.ts +1 -0
  28. package/src/github/post-review.ts +62 -6
  29. package/src/gitlab/post-review.ts +48 -9
  30. package/src/gitlab/publish-drafts.ts +69 -0
  31. package/src/host/client.ts +3 -2
  32. package/src/host/host.ts +23 -5
  33. package/src/project-config.ts +15 -2
  34. package/src/review/carry-marks.ts +131 -0
  35. package/src/review/doctor.ts +60 -26
  36. package/src/review/incremental.ts +107 -0
  37. package/src/review/normalize.ts +14 -4
  38. package/src/review/prepare.ts +47 -0
  39. package/src/review/prompt.ts +112 -5
  40. package/src/review/publish.ts +1 -0
  41. package/src/review/test-paths.ts +44 -4
  42. package/src/review/validate-folds.ts +348 -23
  43. package/src/review/validate.ts +10 -1
  44. package/src/server/bundle.ts +14 -2
  45. package/src/server/html.ts +4 -4
  46. package/src/server/routes/chat-routes.ts +15 -6
  47. package/src/server/routes/pages.ts +4 -1
  48. package/src/server/routes/review-routes.ts +202 -42
  49. package/src/store/canvas-store.ts +3 -0
  50. package/src/store/settings-store.ts +9 -1
  51. package/src/store/state-store.ts +69 -4
  52. package/src/upgrade.ts +338 -0
  53. package/static/js/api.js +55 -1
  54. package/static/js/app.js +28 -7
  55. package/static/js/chat-panel.js +32 -9
  56. package/static/js/chat.js +27 -4
  57. package/static/js/code-folds.js +171 -44
  58. package/static/js/composer.js +109 -4
  59. package/static/js/contract-types.d.ts +4 -0
  60. package/static/js/diff-decorations.js +67 -1
  61. package/static/js/empty-state.js +17 -0
  62. package/static/js/fold-levels.js +176 -0
  63. package/static/js/header.js +36 -9
  64. package/static/js/interactions.js +273 -44
  65. package/static/js/keyboard.js +4 -1
  66. package/static/js/keys.js +12 -0
  67. package/static/js/layers.js +292 -29
  68. package/static/js/nav.js +22 -4
  69. package/static/js/pending.js +161 -0
  70. package/static/js/points.js +69 -9
  71. package/static/js/progress.js +4 -5
  72. package/static/js/quick-questions.js +15 -2
  73. package/static/js/reading-level.js +97 -0
  74. package/static/js/review-session.js +106 -27
  75. package/static/js/settings.js +53 -23
  76. package/static/js/signoff.js +75 -5
  77. package/static/js/skin.js +2 -2
  78. package/static/styles/chat-panel.css +22 -24
  79. package/static/styles/chat.css +4 -0
  80. package/static/styles/header.css +21 -0
  81. package/static/styles/pending.css +102 -0
  82. package/static/styles/review.css +4 -0
  83. package/static/styles.css +1 -0
@@ -7,6 +7,7 @@
7
7
  import { previewControlsHtml, setDisabledReason, setMarkdownPreview } from './composer.js'
8
8
  import { esc } from './dom.js'
9
9
  import { hostLabel } from './host.js'
10
+ import { pendingLabel } from './pending.js'
10
11
  import { layerProgress } from './progress.js'
11
12
 
12
13
  export const SIGNOFF_DIALOG_ID = 'signoff-dialog'
@@ -14,6 +15,9 @@ export const SIGNOFF_DIALOG_ID = 'signoff-dialog'
14
15
  /** Why the post command waits: the body the server writes is not there yet. */
15
16
  export const LOADING_REASON = 'the review body is still loading'
16
17
 
18
+ /** Why it waits after the body failed to load: there is nothing to post. */
19
+ export const FAILED_REASON = 'the review body could not be loaded'
20
+
17
21
  /**
18
22
  * Why approve is not allowed yet, or null when it is. Other changes never counts.
19
23
  * @param {ReviewArtifact} artifact
@@ -33,22 +37,47 @@ export function approveBlockedReason(artifact, state) {
33
37
  }
34
38
 
35
39
  /**
36
- * @param {'APPROVE' | 'REQUEST_CHANGES'} event
40
+ * @param {import('./contract-types.js').ReviewEvent} event
37
41
  */
38
42
  export function signoffTitle(event) {
39
- return event === 'APPROVE' ? `Approve on ${hostLabel()}` : `Request changes on ${hostLabel()}`
43
+ switch (event) {
44
+ case 'APPROVE':
45
+ return `Approve on ${hostLabel()}`
46
+ case 'REQUEST_CHANGES':
47
+ return `Request changes on ${hostLabel()}`
48
+ case 'COMMENT':
49
+ return `Comment on ${hostLabel()}`
50
+ }
40
51
  }
41
52
 
42
53
  /**
43
- * @param {{ event: 'APPROVE' | 'REQUEST_CHANGES' }} opts
54
+ * What the dialog says this event does, under its heading.
55
+ * @param {import('./contract-types.js').ReviewEvent} event
56
+ */
57
+ export function signoffHint(event) {
58
+ const where = `on ${hostLabel()} as you`
59
+ switch (event) {
60
+ case 'APPROVE':
61
+ return `This posts an approving review ${where}. Edit the body first if you want to.`
62
+ case 'REQUEST_CHANGES':
63
+ return `This posts a review requesting changes ${where}. Edit the body first if you want to.`
64
+ case 'COMMENT':
65
+ return `This posts a review with no verdict ${where}: nothing is approved or rejected. Edit the body first if you want to.`
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @param {{ event: import('./contract-types.js').ReviewEvent }} opts
44
71
  * @returns {string}
45
72
  */
46
73
  export function signoffDialogHtml(opts) {
47
74
  return (
48
75
  `<dialog id="${SIGNOFF_DIALOG_ID}" class="signoff-dialog" aria-labelledby="signoff-h">` +
49
76
  `<h2 id="signoff-h">${esc(signoffTitle(opts.event))}</h2>` +
50
- `<p class="hint">This posts a review on ${esc(hostLabel())} as you. Edit the body first if you want to.</p>` +
77
+ `<p class="hint">${esc(signoffHint(opts.event))}</p>` +
51
78
  '<p class="signoff-target muted mono"></p>' +
79
+ '<p class="signoff-folds muted"></p>' +
80
+ '<p class="signoff-pending"></p>' +
52
81
  '<label class="sr" for="signoff-body">Review body</label>' +
53
82
  previewControlsHtml() +
54
83
  '<textarea id="signoff-body" rows="12" aria-busy="true"></textarea>' +
@@ -64,7 +93,7 @@ export function signoffDialogHtml(opts) {
64
93
  * Creates the dialog on first use, points it at this event, and opens it. The body arrives
65
94
  * afterwards through `fillSignoffDialog`.
66
95
  * @param {HTMLElement} root
67
- * @param {{ event: 'APPROVE' | 'REQUEST_CHANGES' }} opts
96
+ * @param {{ event: import('./contract-types.js').ReviewEvent }} opts
68
97
  * @returns {HTMLDialogElement}
69
98
  */
70
99
  export function openSignoffDialog(root, opts) {
@@ -84,6 +113,14 @@ export function openSignoffDialog(root, opts) {
84
113
  if (heading !== null) {
85
114
  heading.textContent = signoffTitle(opts.event)
86
115
  }
116
+ const hint = dialog.querySelector('.hint')
117
+ if (hint !== null) {
118
+ hint.textContent = signoffHint(opts.event)
119
+ }
120
+ const pending = dialog.querySelector('.signoff-pending')
121
+ if (pending !== null) {
122
+ pending.textContent = ''
123
+ }
87
124
  const result = dialog.querySelector('.signoff-result')
88
125
  if (result !== null) {
89
126
  result.textContent = ''
@@ -103,6 +140,20 @@ export function openSignoffDialog(root, opts) {
103
140
  return dialog
104
141
  }
105
142
 
143
+ /**
144
+ * Records how much code the reader had hidden while reviewing, so the sign-off says what was
145
+ * read at a glance and what was read line by line.
146
+ * @param {HTMLDialogElement} dialog
147
+ * @param {string} note
148
+ */
149
+ export function setSignoffFolds(dialog, note) {
150
+ const box = dialog.querySelector('.signoff-folds')
151
+ if (box !== null) {
152
+ box.textContent = note
153
+ }
154
+ return dialog
155
+ }
156
+
106
157
  /**
107
158
  * Puts the generated body and the target commit in the dialog.
108
159
  * @param {HTMLDialogElement} dialog
@@ -120,6 +171,17 @@ export function fillSignoffDialog(dialog, preview) {
120
171
  if (target !== null) {
121
172
  target.textContent = `on commit ${preview.headSha.slice(0, 7)}`
122
173
  }
174
+ // The drafts go out with the review, so the dialog says so before anything is posted.
175
+ const pending = dialog.querySelector('.signoff-pending')
176
+ if (pending !== null) {
177
+ pending.textContent =
178
+ preview.pending > 0 ? `${pendingLabel(preview.pending)} will be posted with this review.` : ''
179
+ pending.classList.toggle('has-pending', preview.pending > 0)
180
+ }
181
+ const post = dialog.querySelector('[data-act="signoff-post"]')
182
+ if (post !== null) {
183
+ post.textContent = preview.pending > 0 ? `post review · ${pendingLabel(preview.pending)}` : 'post review'
184
+ }
123
185
  return dialog
124
186
  }
125
187
 
@@ -164,6 +226,14 @@ export function showSignoffError(dialog, message) {
164
226
  if (result !== null) {
165
227
  result.textContent = message
166
228
  }
229
+ // The load this reports is over, so the box stops saying it is busy. Posting stays out of
230
+ // reach, but for the reason that is true now rather than the one that was true before.
231
+ const area = dialog.querySelector('textarea')
232
+ if (area instanceof HTMLTextAreaElement && area.getAttribute('aria-busy') === 'true') {
233
+ area.removeAttribute('aria-busy')
234
+ area.placeholder = ''
235
+ setDisabledReason(dialog.querySelector('[data-act="signoff-post"]'), FAILED_REASON)
236
+ }
167
237
  return result
168
238
  }
169
239
 
package/static/js/skin.js CHANGED
@@ -11,7 +11,7 @@ export const SKINS = /** @type {const} */ (['terminal', 'github'])
11
11
  /** @typedef {(typeof SKINS)[number]} Skin */
12
12
 
13
13
  /** The look a page wears when the settings file says nothing. */
14
- export const DEFAULT_SKIN = /** @type {Skin} */ ('terminal')
14
+ export const DEFAULT_SKIN = /** @type {Skin} */ ('github')
15
15
 
16
16
  /**
17
17
  * @param {unknown} value
@@ -37,7 +37,7 @@ export function readSkin(root) {
37
37
  */
38
38
  export function nextSkin(skin) {
39
39
  const next = SKINS[SKINS.indexOf(skin) + 1]
40
- return next ?? DEFAULT_SKIN
40
+ return next ?? SKINS[0]
41
41
  }
42
42
 
43
43
  /**
@@ -1,30 +1,28 @@
1
- .chat-launcher,
2
- .chat-minimize {
1
+ /* The launcher stands in for the chat at every width; the script hides it while the chat is up. */
2
+ .chat-launcher {
3
+ display: block;
4
+ position: fixed;
5
+ right: 20px;
6
+ bottom: 20px;
7
+ z-index: 10;
8
+ padding: 14px 20px;
9
+ border: 1px solid var(--ink);
10
+ border-radius: 999px;
11
+ background: var(--accent);
12
+ color: var(--bg);
13
+ box-shadow: 0 4px 20px #0003;
14
+ font: inherit;
15
+ font-weight: 600;
16
+ cursor: pointer;
17
+ }
18
+ .chat-launcher[hidden] {
3
19
  display: none;
4
20
  }
21
+ /* Always the last thing on its row, whether or not the header wraps. */
22
+ .chat-minimize {
23
+ margin-left: auto;
24
+ }
5
25
  @media (max-width: 1360px) {
6
- .chat-launcher {
7
- display: block;
8
- position: fixed;
9
- right: 20px;
10
- bottom: 20px;
11
- z-index: 10;
12
- padding: 14px 20px;
13
- border: 1px solid var(--ink);
14
- border-radius: 999px;
15
- background: var(--accent);
16
- color: var(--bg);
17
- box-shadow: 0 4px 20px #0003;
18
- font: inherit;
19
- font-weight: 600;
20
- cursor: pointer;
21
- }
22
- .chat-launcher[hidden] {
23
- display: none;
24
- }
25
- .chat-minimize {
26
- display: inline-block;
27
- }
28
26
  dialog.chat-dialog {
29
27
  position: fixed;
30
28
  inset: auto 20px 20px auto;
@@ -7,6 +7,10 @@
7
7
  height: 100vh;
8
8
  background: transparent;
9
9
  }
10
+ /* Minimized on a wide screen: the pane stays mounted with its draft, out of the grid. */
11
+ .chat[hidden] {
12
+ display: none;
13
+ }
10
14
  .handle {
11
15
  position: absolute;
12
16
  left: -1px;
@@ -46,6 +46,27 @@
46
46
  gap: 4px 16px;
47
47
  align-items: baseline;
48
48
  }
49
+ /* How much code is hidden: a setting on the reading, so it sits with the progress line and not
50
+ among the commands, and says what it hides rather than leaving the reader to find out. */
51
+ .reading {
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ gap: 4px 10px;
55
+ align-items: baseline;
56
+ margin-top: 14px;
57
+ }
58
+ .reading label {
59
+ font-size: 12px;
60
+ color: var(--fg-muted);
61
+ }
62
+ .reading select {
63
+ padding: 2px 6px;
64
+ font-size: 13px;
65
+ }
66
+ .fold-hint {
67
+ font-size: 12px;
68
+ color: var(--fg-muted);
69
+ }
49
70
  .stripe {
50
71
  height: 6px;
51
72
  background: var(--stripe-y);
@@ -0,0 +1,102 @@
1
+ /* The pending review: the bar that says one is open, and the drafts drawn on the diff. */
2
+
3
+ .pending-bar-host:empty {
4
+ display: none;
5
+ }
6
+
7
+ .pending-bar {
8
+ display: flex;
9
+ flex-wrap: wrap;
10
+ gap: 10px;
11
+ align-items: center;
12
+ margin: 10px 0 0;
13
+ padding: 10px 12px;
14
+ color: var(--fg);
15
+ background: var(--warn-bg);
16
+ /* The heavy left edge is what carries across the page: a review is open and unsent. */
17
+ border: 1px solid var(--warn);
18
+ border-left: 4px solid var(--warn);
19
+ }
20
+
21
+ /* The dot pulses so an open review is noticed on a page the reader has scrolled far down. */
22
+ .pending-mark {
23
+ flex: none;
24
+ width: 10px;
25
+ height: 10px;
26
+ background: var(--warn);
27
+ border-radius: 50%;
28
+ animation: pending-pulse 2s ease-in-out infinite;
29
+ }
30
+
31
+ @keyframes pending-pulse {
32
+ 0%,
33
+ 100% {
34
+ opacity: 1;
35
+ }
36
+ 50% {
37
+ opacity: 0.35;
38
+ }
39
+ }
40
+
41
+ @media (prefers-reduced-motion: reduce) {
42
+ .pending-mark {
43
+ animation: none;
44
+ }
45
+ }
46
+
47
+ .pending-bar .pending-text {
48
+ flex: 1 1 260px;
49
+ }
50
+
51
+ .pending-bar .pending-actions {
52
+ display: flex;
53
+ flex: none;
54
+ gap: 6px;
55
+ }
56
+
57
+ /* A draft on the diff reads as unfinished: dashed edge, and the badge that names it. */
58
+ .cmt.pending-cmt {
59
+ padding: 8px;
60
+ background: var(--warn-bg);
61
+ border: 1px dashed var(--warn);
62
+ }
63
+
64
+ .pending-av {
65
+ color: var(--fg-muted);
66
+ border-color: var(--warn);
67
+ }
68
+
69
+ /* The avatar column covers the byline and the body, so the commands clear both. */
70
+ .cmt.pending-cmt .tbtns {
71
+ display: flex;
72
+ grid-column: 1 / -1;
73
+ gap: 6px;
74
+ margin-top: 6px;
75
+ }
76
+
77
+ .pill.pending {
78
+ color: var(--bg);
79
+ background: var(--warn);
80
+ }
81
+
82
+ .signoff-pending:empty {
83
+ display: none;
84
+ }
85
+
86
+ .signoff-pending.has-pending {
87
+ padding: 6px 8px;
88
+ background: var(--warn-bg);
89
+ border-left: 3px solid var(--warn);
90
+ }
91
+
92
+ .earlier-pending {
93
+ margin-top: 8px;
94
+ padding: 8px 12px;
95
+ border: 1px solid var(--warn);
96
+ }
97
+
98
+ .earlier-draft + .earlier-draft {
99
+ margin-top: 12px;
100
+ padding-top: 12px;
101
+ border-top: 1px dashed var(--warn);
102
+ }
@@ -65,6 +65,10 @@
65
65
  border-bottom: 1px solid var(--line);
66
66
  color: var(--fg);
67
67
  }
68
+ .fold-count {
69
+ color: var(--fg-muted);
70
+ font-weight: 400;
71
+ }
68
72
  .summary,
69
73
  .rationale,
70
74
  .prose {
package/static/styles.css CHANGED
@@ -10,6 +10,7 @@
10
10
  @import './styles/panels.css';
11
11
  @import './styles/responsive.css';
12
12
  @import './styles/review-actions.css';
13
+ @import './styles/pending.css';
13
14
  @import './styles/chat-tools.css';
14
15
  @import './styles/skin-github.css';
15
16
  @import './styles/chat-panel.css';