@vintasoftware/pr-review-canvas 0.3.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.
- package/README.md +79 -31
- package/docs/reference.md +396 -101
- package/package.json +9 -4
- package/pr-review.config.example.yml +44 -4
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +123 -26
- package/prompts/generation-strict-incremental.md +53 -0
- package/prompts/generation-strict.md +1 -27
- package/prompts/generation-surfacing-incremental.md +56 -0
- package/prompts/generation-surfacing.md +1 -58
- package/prompts/judging-strict.md +27 -0
- package/prompts/judging-surfacing.md +58 -0
- package/skills/pr-review-canvas/SKILL.md +81 -42
- package/src/acpx/acpx.ts +98 -5
- package/src/acpx/models.ts +43 -0
- package/src/canvas/comment.ts +24 -0
- package/src/canvas/import.ts +30 -9
- package/src/canvas/name.ts +1 -0
- package/src/canvas/zip.ts +21 -1
- package/src/chat/chat-manager.ts +67 -40
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +70 -8
- package/src/commands.ts +50 -17
- package/src/config.ts +14 -26
- package/src/contract/api.ts +58 -2
- package/src/contract/canvas-manifest.ts +7 -0
- package/src/contract/comments.ts +5 -0
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +76 -2
- package/src/contract/keys.ts +1 -0
- package/src/contract/pending.ts +49 -0
- package/src/contract/review-artifact.ts +50 -7
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +26 -0
- package/src/contract/settings.ts +7 -0
- package/src/contract/state.ts +53 -22
- package/src/contract/validation.ts +1 -0
- package/src/git/environment.mjs +27 -0
- package/src/git/git.ts +109 -9
- package/src/git/local-target.ts +138 -0
- package/src/git/patch-lines.ts +34 -2
- package/src/git/pr-refs.ts +36 -0
- package/src/github/attachments.ts +9 -257
- package/src/github/canvas-comment.ts +22 -0
- package/src/github/capabilities.ts +3 -41
- package/src/github/comments.ts +3 -24
- package/src/github/post-comment.ts +3 -36
- package/src/github/post-review.ts +64 -23
- package/src/github/pr.ts +6 -87
- package/src/github/threads.ts +2 -2
- package/src/gitlab/attachments.ts +40 -0
- package/src/gitlab/canvas-comment.ts +26 -0
- package/src/gitlab/capabilities.ts +64 -0
- package/src/gitlab/comments.ts +164 -0
- package/src/gitlab/mr.ts +115 -0
- package/src/gitlab/post-comment.ts +111 -0
- package/src/gitlab/post-review.ts +93 -0
- package/src/gitlab/project.ts +13 -0
- package/src/gitlab/publish-drafts.ts +69 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +246 -0
- package/src/host/host.ts +154 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +26 -0
- package/src/review/carry-marks.ts +131 -0
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +82 -38
- package/src/review/incremental.ts +107 -0
- package/src/review/normalize.ts +14 -4
- package/src/review/prepare.ts +111 -10
- package/src/review/prompt.ts +112 -5
- package/src/review/publish.ts +62 -10
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/test-paths.ts +44 -4
- package/src/review/validate-folds.ts +349 -24
- package/src/review/validate.ts +14 -5
- package/src/server/bundle.ts +324 -111
- package/src/server/context.ts +10 -8
- package/src/server/errors.ts +30 -8
- package/src/server/html.ts +35 -11
- package/src/server/routes/api.ts +63 -26
- package/src/server/routes/chat-routes.ts +91 -44
- package/src/server/routes/pages.ts +26 -9
- package/src/server/routes/review-routes.ts +278 -67
- package/src/store/canvas-store.ts +93 -55
- package/src/store/data-dir.ts +2 -1
- package/src/store/derived-store.ts +41 -27
- package/src/store/pr-store.ts +23 -14
- package/src/store/settings-store.ts +9 -1
- package/src/store/state-store.ts +105 -36
- package/src/upgrade.ts +338 -0
- package/static/js/api.js +87 -25
- package/static/js/app.js +50 -15
- package/static/js/chat-panel.js +32 -9
- package/static/js/chat.js +30 -6
- package/static/js/code-folds.js +171 -44
- package/static/js/composer.js +132 -17
- package/static/js/contract-types.d.ts +7 -0
- package/static/js/diff-decorations.js +67 -1
- package/static/js/diff-renderer.js +1 -1
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +102 -18
- package/static/js/errors.js +22 -6
- package/static/js/fold-levels.js +176 -0
- package/static/js/header.js +65 -15
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +274 -46
- package/static/js/keyboard.js +4 -1
- package/static/js/keys.js +12 -0
- package/static/js/layers.js +295 -32
- package/static/js/links.js +3 -3
- package/static/js/markdown.js +28 -1
- package/static/js/nav.js +22 -4
- package/static/js/pending.js +161 -0
- package/static/js/points.js +70 -9
- package/static/js/progress.js +4 -5
- package/static/js/quick-questions.js +15 -2
- package/static/js/reading-level.js +97 -0
- package/static/js/review-session.js +113 -29
- package/static/js/settings.js +55 -24
- package/static/js/signoff.js +79 -10
- package/static/js/skin.js +2 -2
- package/static/styles/chat-panel.css +22 -24
- package/static/styles/chat.css +4 -0
- package/static/styles/commands.css +6 -0
- package/static/styles/header.css +21 -0
- package/static/styles/panels.css +4 -0
- package/static/styles/pending.css +102 -0
- package/static/styles/review-actions.css +1 -0
- package/static/styles/review.css +4 -0
- package/static/styles/skin-github.css +7 -1
- package/static/styles.css +1 -0
- package/src/github/gh.ts +0 -211
|
@@ -17,32 +17,48 @@ import { replacePostButton } from './comment-link.js'
|
|
|
17
17
|
import {
|
|
18
18
|
applyCapabilityGating,
|
|
19
19
|
closeComposers,
|
|
20
|
+
composerBody,
|
|
20
21
|
composerHtml,
|
|
21
22
|
composerInput,
|
|
23
|
+
composerPendingInput,
|
|
22
24
|
composerRowHtml,
|
|
25
|
+
concealForComposer,
|
|
23
26
|
focusComposer,
|
|
27
|
+
revealConcealed,
|
|
28
|
+
refreshComposerCommands,
|
|
24
29
|
} from './composer.js'
|
|
25
30
|
import { commentHtml, setThreadCollapsed, threadRowHtml } from './diff-decorations.js'
|
|
26
31
|
import { flash, scrollIntoViewSafe } from './dom.js'
|
|
32
|
+
import { isFoldLevel, nextFoldLevel } from './fold-levels.js'
|
|
27
33
|
import { refreshProgress } from './header.js'
|
|
28
34
|
import { keyAction, openHelpDialog } from './keyboard.js'
|
|
29
|
-
import { pointAnchorId,
|
|
35
|
+
import { pointAnchorId, reviewedId } from './keys.js'
|
|
30
36
|
import {
|
|
37
|
+
canvasHiddenLines,
|
|
38
|
+
cardOf,
|
|
39
|
+
getFoldLevel,
|
|
31
40
|
getRenderContext,
|
|
32
41
|
pathSet,
|
|
42
|
+
refreshCardDecorations,
|
|
43
|
+
refreshFolds,
|
|
44
|
+
setCardCollapsed,
|
|
33
45
|
setCardRenderedHook,
|
|
46
|
+
setFoldLevel,
|
|
34
47
|
setRenderContext,
|
|
35
48
|
updateRenderState,
|
|
36
49
|
} from './layers.js'
|
|
37
50
|
import { buildNavOrder, layerOf, nextFile, nextLayer, prevFile, prevLayer } from './nav.js'
|
|
38
51
|
import { issueCommentHtml } from './overview.js'
|
|
52
|
+
import { pendingCount, refreshPendingBar } from './pending.js'
|
|
39
53
|
import { applyDismissed, pointToMarkdown, postedUrls } from './points.js'
|
|
40
54
|
import { layerProgress } from './progress.js'
|
|
55
|
+
import { FOLD_LEVEL_SELECT_ID, hiddenLabel, refreshFoldLevel } from './reading-level.js'
|
|
41
56
|
import { lineRefFromEvent, markSelection, selectionReducer } from './selection.js'
|
|
42
57
|
import {
|
|
43
58
|
fillSignoffDialog,
|
|
44
59
|
openSignoffDialog,
|
|
45
60
|
showSignoffError,
|
|
61
|
+
setSignoffFolds,
|
|
46
62
|
showSignoffResult,
|
|
47
63
|
signoffBody,
|
|
48
64
|
} from './signoff.js'
|
|
@@ -51,6 +67,16 @@ import {
|
|
|
51
67
|
|
|
52
68
|
const NO_CHAT_NOTE = 'the AI Chat pane is off; press ? for the key map'
|
|
53
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The event a sign-off dialog is set to. An attribute that names none reads as a comment-only
|
|
72
|
+
* review, the one of the three that claims nothing.
|
|
73
|
+
* @param {string | null} raw
|
|
74
|
+
* @returns {import('./contract-types.js').ReviewEvent}
|
|
75
|
+
*/
|
|
76
|
+
export function signoffEvent(raw) {
|
|
77
|
+
return raw === 'APPROVE' || raw === 'REQUEST_CHANGES' ? raw : 'COMMENT'
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
/** @type {WeakMap<Element, ReturnType<typeof setTimeout>>} */
|
|
55
81
|
const toastTimers = new WeakMap()
|
|
56
82
|
|
|
@@ -103,37 +129,6 @@ export function toast(root, message) {
|
|
|
103
129
|
return box
|
|
104
130
|
}
|
|
105
131
|
|
|
106
|
-
/**
|
|
107
|
-
* The card a command belongs to, and the part of it that collapses.
|
|
108
|
-
* @param {Element} el
|
|
109
|
-
* @returns {{ card: HTMLElement, body: HTMLElement, chevron: Element | null } | null}
|
|
110
|
-
*/
|
|
111
|
-
export function cardOf(el) {
|
|
112
|
-
const card = el.closest('article.file, section.layer')
|
|
113
|
-
const body = card?.querySelector(':scope > .file-body, :scope > .layer-body')
|
|
114
|
-
if (!(card instanceof HTMLElement && body instanceof HTMLElement)) {
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
return { card, body, chevron: card.querySelector(':scope > .file-h > .chev, :scope > .layer-h > .chev') }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Opens or collapses one card. Nothing else on the card changes, so clicking the chevron never
|
|
122
|
-
* touches the reviewed box and the other way round.
|
|
123
|
-
* @param {Element} el an element inside the card
|
|
124
|
-
* @param {boolean} [collapsed] the state to set; the opposite of the current one when omitted
|
|
125
|
-
*/
|
|
126
|
-
export function setCardCollapsed(el, collapsed) {
|
|
127
|
-
const parts = cardOf(el)
|
|
128
|
-
if (parts === null) {
|
|
129
|
-
return null
|
|
130
|
-
}
|
|
131
|
-
const next = collapsed ?? !parts.body.hidden
|
|
132
|
-
parts.body.toggleAttribute('hidden', next)
|
|
133
|
-
parts.chevron?.setAttribute('aria-expanded', next ? 'false' : 'true')
|
|
134
|
-
return next
|
|
135
|
-
}
|
|
136
|
-
|
|
137
132
|
/**
|
|
138
133
|
* The layer or file card a reviewed id belongs to.
|
|
139
134
|
* @param {ParentNode} root
|
|
@@ -160,8 +155,7 @@ export function nextUnreviewedTarget(root, session, fromId, kind) {
|
|
|
160
155
|
if (item === undefined || item.kind !== kind || item.other) {
|
|
161
156
|
continue
|
|
162
157
|
}
|
|
163
|
-
const id =
|
|
164
|
-
item.kind === 'file' ? `layer:${item.layerId}/file:${sanitizeKey(item.path)}` : `layer:${item.layerId}`
|
|
158
|
+
const id = item.kind === 'file' ? reviewedId(item.layerKey, item.path) : reviewedId(item.layerKey)
|
|
165
159
|
if (!session.isReviewed(id)) {
|
|
166
160
|
return root.querySelector(`#${cssEscape(item.id)}`)
|
|
167
161
|
}
|
|
@@ -236,6 +230,18 @@ export function wireReview(root, session, opts = {}) {
|
|
|
236
230
|
getRenderContext()?.files.find(f => f.path === path)?.key ?? null
|
|
237
231
|
/** The time the page was drawn, which the comments it adds are timed against. */
|
|
238
232
|
const renderNow = () => getRenderContext()?.now ?? new Date()
|
|
233
|
+
/** @param {import('./contract-types.js').FoldLevel} level */
|
|
234
|
+
const hiddenAt = level =>
|
|
235
|
+
canvasHiddenLines(
|
|
236
|
+
getRenderContext() ?? {
|
|
237
|
+
artifact: session.artifact,
|
|
238
|
+
files: [],
|
|
239
|
+
comments: [],
|
|
240
|
+
state: session.state,
|
|
241
|
+
headSha: session.headSha,
|
|
242
|
+
},
|
|
243
|
+
level
|
|
244
|
+
)
|
|
239
245
|
|
|
240
246
|
/**
|
|
241
247
|
* Whether a card counts as reviewed, read the same way the page was first drawn: a layer is
|
|
@@ -247,13 +253,39 @@ export function wireReview(root, session, opts = {}) {
|
|
|
247
253
|
if (id.includes('/file:')) {
|
|
248
254
|
return state.reviewed[id] === true
|
|
249
255
|
}
|
|
250
|
-
const layer = session.artifact.layers.find(l =>
|
|
256
|
+
const layer = session.artifact.layers.find(l => reviewedId(l.key) === id)
|
|
251
257
|
return layer === undefined ? state.reviewed[id] === true : layerProgress(layer, state) === 'done'
|
|
252
258
|
}
|
|
253
259
|
|
|
260
|
+
let drawnPending = session.pending
|
|
261
|
+
let drawnComments = session.submittedComments
|
|
254
262
|
/** Draws everything the local state decides, after it changed. */
|
|
255
263
|
const onState = (/** @type {PrState} */ state) => {
|
|
256
264
|
updateRenderState(state)
|
|
265
|
+
const ctx = getRenderContext()
|
|
266
|
+
if (ctx !== null) {
|
|
267
|
+
const changedPaths = new Set()
|
|
268
|
+
const previous = new Map(drawnPending.map(p => [p.id, p]))
|
|
269
|
+
for (const draft of state.pending) {
|
|
270
|
+
const old = previous.get(draft.id)
|
|
271
|
+
if (old === undefined || old.body !== draft.body) changedPaths.add(draft.path)
|
|
272
|
+
previous.delete(draft.id)
|
|
273
|
+
}
|
|
274
|
+
for (const draft of previous.values()) changedPaths.add(draft.path)
|
|
275
|
+
const added = session.submittedComments.filter(c => !drawnComments.some(old => old.id === c.id))
|
|
276
|
+
for (const comment of added) changedPaths.add(comment.path)
|
|
277
|
+
const next = { ...ctx, comments: [...ctx.comments, ...added] }
|
|
278
|
+
setRenderContext(next)
|
|
279
|
+
refreshCardDecorations(root, next, changedPaths, added)
|
|
280
|
+
if (changedPaths.size > 0) {
|
|
281
|
+
// A thread a submitted review adds, or a draft saved or deleted, changes what keeps a
|
|
282
|
+
// card's code open, as a posted comment does.
|
|
283
|
+
refreshFolds(root)
|
|
284
|
+
refreshFoldLevel(root, getFoldLevel(), hiddenAt(getFoldLevel()))
|
|
285
|
+
}
|
|
286
|
+
drawnPending = state.pending
|
|
287
|
+
drawnComments = session.submittedComments
|
|
288
|
+
}
|
|
257
289
|
refreshProgress(root, session.artifact, state)
|
|
258
290
|
applyDismissed(root, session.artifact.points, state, {
|
|
259
291
|
paths: paths(),
|
|
@@ -274,6 +306,8 @@ export function wireReview(root, session, opts = {}) {
|
|
|
274
306
|
setCardCollapsed(parts.card, reviewed)
|
|
275
307
|
}
|
|
276
308
|
}
|
|
309
|
+
refreshPendingBar(root, state, session.headSha)
|
|
310
|
+
refreshComposerCommands(root, state.pending.length > 0)
|
|
277
311
|
applyCapabilityGating(root, session.capabilities)
|
|
278
312
|
}
|
|
279
313
|
const unsubscribe = session.subscribe(onState)
|
|
@@ -287,6 +321,9 @@ export function wireReview(root, session, opts = {}) {
|
|
|
287
321
|
...ctx,
|
|
288
322
|
comments: [...ctx.comments.filter(c => c.id !== answer.comment.id), answer.comment],
|
|
289
323
|
})
|
|
324
|
+
// A new thread keeps its card's code open at every level, and the counters say so.
|
|
325
|
+
refreshFolds(root)
|
|
326
|
+
refreshFoldLevel(root, getFoldLevel(), hiddenAt(getFoldLevel()))
|
|
290
327
|
onState(session.state)
|
|
291
328
|
}
|
|
292
329
|
return answer
|
|
@@ -348,6 +385,7 @@ export function wireReview(root, session, opts = {}) {
|
|
|
348
385
|
*/
|
|
349
386
|
const openComposer = (anchor, options, shape) => {
|
|
350
387
|
closeComposers(root)
|
|
388
|
+
options = { ...options, pendingActive: pendingCount(session.state) > 0 }
|
|
351
389
|
const node =
|
|
352
390
|
shape === 'row' ? rowFrom(doc, composerRowHtml(options)) : nodeFrom(doc, composerHtml(options))
|
|
353
391
|
if (node === null) {
|
|
@@ -523,9 +561,12 @@ export function wireReview(root, session, opts = {}) {
|
|
|
523
561
|
)
|
|
524
562
|
}
|
|
525
563
|
|
|
526
|
-
/** @param {HTMLElement} button @param {'
|
|
564
|
+
/** @param {HTMLElement} button @param {import('./contract-types.js').ReviewEvent} event */
|
|
527
565
|
const openSignoff = (button, event) => {
|
|
528
566
|
const dialog = openSignoffDialog(root, { event })
|
|
567
|
+
const level = getFoldLevel()
|
|
568
|
+
const counts = hiddenAt(level)
|
|
569
|
+
setSignoffFolds(dialog, counts.hidden === 0 ? '' : `Read at the ${level} level · ${hiddenLabel(counts)}`)
|
|
529
570
|
applyCapabilityGating(root, session.capabilities)
|
|
530
571
|
signoffOpening += 1
|
|
531
572
|
const opening = signoffOpening
|
|
@@ -556,19 +597,138 @@ export function wireReview(root, session, opts = {}) {
|
|
|
556
597
|
if (!(dialog instanceof HTMLDialogElement)) {
|
|
557
598
|
return
|
|
558
599
|
}
|
|
559
|
-
const event = dialog.getAttribute('data-event')
|
|
600
|
+
const event = signoffEvent(dialog.getAttribute('data-event'))
|
|
560
601
|
const body = signoffBody(dialog)
|
|
561
602
|
void runCommand(
|
|
562
603
|
button,
|
|
563
604
|
async () => {
|
|
564
|
-
const review = await session.postReview(
|
|
605
|
+
const { review, submitted, warnings } = await session.postReview(
|
|
606
|
+
event,
|
|
607
|
+
body === '' ? undefined : body
|
|
608
|
+
)
|
|
565
609
|
showSignoffResult(dialog, review)
|
|
566
|
-
|
|
610
|
+
for (const warning of warnings) {
|
|
611
|
+
dialog.querySelector('.signoff-result')?.append(document.createTextNode(` ${warning}`))
|
|
612
|
+
}
|
|
613
|
+
const verdict =
|
|
614
|
+
review.state === 'APPROVED'
|
|
615
|
+
? 'approved'
|
|
616
|
+
: review.state === 'CHANGES_REQUESTED'
|
|
617
|
+
? 'changes requested'
|
|
618
|
+
: 'review posted'
|
|
619
|
+
toast(
|
|
620
|
+
root,
|
|
621
|
+
`${verdict}${submitted > 0 ? ` with ${submitted} comment${submitted === 1 ? '' : 's'}` : ''}`
|
|
622
|
+
)
|
|
567
623
|
},
|
|
568
624
|
{ pendingLabel: 'posting…' }
|
|
569
625
|
)
|
|
570
626
|
}
|
|
571
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Switches how much code the page hides, from the control or from the `f` key. The hint under
|
|
630
|
+
* the control says what the new level hides and how much of the diff that is.
|
|
631
|
+
* @param {string} value
|
|
632
|
+
*/
|
|
633
|
+
const applyFoldLevel = value => {
|
|
634
|
+
if (!isFoldLevel(value) || value === getFoldLevel()) {
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
setFoldLevel(root, value)
|
|
638
|
+
refreshFoldLevel(root, value, hiddenAt(value))
|
|
639
|
+
toast(root, `hiding code: ${value}`)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Adds what a composer holds to the pending review instead of posting it.
|
|
644
|
+
* @param {HTMLElement} button
|
|
645
|
+
* @param {Element} box
|
|
646
|
+
*/
|
|
647
|
+
const queueFromComposer = (button, box) => {
|
|
648
|
+
const input = composerPendingInput(box)
|
|
649
|
+
if (input === null) {
|
|
650
|
+
showCommandError(button, 'write something first')
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
box.setAttribute('data-posting', '1')
|
|
654
|
+
void runCommand(
|
|
655
|
+
button,
|
|
656
|
+
async () => {
|
|
657
|
+
await session.addPending(input)
|
|
658
|
+
box.closest('tr.composer')?.remove()
|
|
659
|
+
box.remove()
|
|
660
|
+
toast(root, 'comment added to your review')
|
|
661
|
+
},
|
|
662
|
+
{ pendingLabel: 'adding…' }
|
|
663
|
+
).finally(() => box.removeAttribute('data-posting'))
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Saves an edit to a draft that is already in the review.
|
|
668
|
+
* @param {HTMLElement} button
|
|
669
|
+
* @param {Element} box
|
|
670
|
+
*/
|
|
671
|
+
const savePending = (button, box) => {
|
|
672
|
+
const id = box.getAttribute('data-pending-id')
|
|
673
|
+
const body = composerBody(box)
|
|
674
|
+
if (id === null) {
|
|
675
|
+
return
|
|
676
|
+
}
|
|
677
|
+
if (body === '') {
|
|
678
|
+
showCommandError(button, 'write something first')
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
void runCommand(
|
|
682
|
+
button,
|
|
683
|
+
async () => {
|
|
684
|
+
await session.editPending(id, body)
|
|
685
|
+
const host = box.parentElement
|
|
686
|
+
box.remove()
|
|
687
|
+
if (host !== null) revealConcealed(host.parentNode ?? root)
|
|
688
|
+
toast(root, 'draft updated')
|
|
689
|
+
},
|
|
690
|
+
{ pendingLabel: 'saving…' }
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Puts an attention point's text in the pending review instead of posting it. The point is
|
|
696
|
+
* tagged with its fingerprint, so once the review lands the point shows the comment it became.
|
|
697
|
+
* @param {HTMLElement} button
|
|
698
|
+
* @param {Point} point
|
|
699
|
+
*/
|
|
700
|
+
const queuePoint = (button, point) => {
|
|
701
|
+
void runCommand(
|
|
702
|
+
button,
|
|
703
|
+
async () => {
|
|
704
|
+
await session.addPending({
|
|
705
|
+
path: point.path,
|
|
706
|
+
line: point.line,
|
|
707
|
+
side: point.side ?? 'new',
|
|
708
|
+
body: pointToMarkdown(point),
|
|
709
|
+
pointFingerprint: point.fingerprint,
|
|
710
|
+
})
|
|
711
|
+
toast(root, 'attention point added to your review')
|
|
712
|
+
},
|
|
713
|
+
{ pendingLabel: 'adding…' }
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @param {HTMLElement} button
|
|
719
|
+
* @param {string} id
|
|
720
|
+
*/
|
|
721
|
+
const deletePending = (button, id) => {
|
|
722
|
+
void runCommand(
|
|
723
|
+
button,
|
|
724
|
+
async () => {
|
|
725
|
+
await session.deletePending(id)
|
|
726
|
+
toast(root, 'draft deleted')
|
|
727
|
+
},
|
|
728
|
+
{ pendingLabel: 'deleting…' }
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
572
732
|
/** @type {Record<string, (el: HTMLElement, event: MouseEvent) => void>} */
|
|
573
733
|
const actions = {
|
|
574
734
|
'toggle-card': el => {
|
|
@@ -596,6 +756,12 @@ export function wireReview(root, session, opts = {}) {
|
|
|
596
756
|
postPoint(el, point)
|
|
597
757
|
}
|
|
598
758
|
},
|
|
759
|
+
'point-queue': el => {
|
|
760
|
+
const point = pointById(el.getAttribute('data-point') ?? '')
|
|
761
|
+
if (point !== undefined) {
|
|
762
|
+
queuePoint(el, point)
|
|
763
|
+
}
|
|
764
|
+
},
|
|
599
765
|
'comment-line': el => {
|
|
600
766
|
const key = el.getAttribute('data-key') ?? ''
|
|
601
767
|
const path = session.pathForKey(key)
|
|
@@ -616,8 +782,65 @@ export function wireReview(root, session, opts = {}) {
|
|
|
616
782
|
postFromComposer(el, box)
|
|
617
783
|
}
|
|
618
784
|
},
|
|
619
|
-
'
|
|
620
|
-
|
|
785
|
+
'composer-queue': el => {
|
|
786
|
+
const box = el.closest('.composer-box')
|
|
787
|
+
if (box !== null) {
|
|
788
|
+
queueFromComposer(el, box)
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
'pending-edit': el => {
|
|
792
|
+
const id = el.getAttribute('data-pending-id')
|
|
793
|
+
const draft = session.pending.find(p => p.id === id)
|
|
794
|
+
const host = el.closest('.pending-cmt')
|
|
795
|
+
if (draft === undefined || host === null) {
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
composerSeq += 1
|
|
799
|
+
// The box replaces the draft it edits, so the reader sees one of the two at a time.
|
|
800
|
+
const node = openComposer(
|
|
801
|
+
host,
|
|
802
|
+
{
|
|
803
|
+
id: `composer-${composerSeq}`,
|
|
804
|
+
label: `Edit your comment on ${draft.path}:${draft.line}`,
|
|
805
|
+
kind: 'inline',
|
|
806
|
+
body: draft.body,
|
|
807
|
+
pendingId: draft.id,
|
|
808
|
+
},
|
|
809
|
+
'block'
|
|
810
|
+
)
|
|
811
|
+
if (node !== null) {
|
|
812
|
+
concealForComposer(host)
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
'pending-save': el => {
|
|
816
|
+
const box = el.closest('.composer-box')
|
|
817
|
+
if (box !== null) {
|
|
818
|
+
savePending(el, box)
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
'pending-delete': el => {
|
|
822
|
+
const id = el.getAttribute('data-pending-id')
|
|
823
|
+
if (id !== null) {
|
|
824
|
+
deletePending(el, id)
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
'pending-finish': el => {
|
|
828
|
+
// Finishing a review is the same dialog the sign-off commands open; a review being written
|
|
829
|
+
// usually has something to say, so it opens on the verdict that claims nothing.
|
|
830
|
+
openSignoff(el, 'COMMENT')
|
|
831
|
+
},
|
|
832
|
+
'pending-discard': el => {
|
|
833
|
+
void runCommand(
|
|
834
|
+
el,
|
|
835
|
+
async () => {
|
|
836
|
+
const count = pendingCount(session.state)
|
|
837
|
+
await session.discardPending()
|
|
838
|
+
toast(root, `${count} pending comment${count === 1 ? '' : 's'} discarded`)
|
|
839
|
+
},
|
|
840
|
+
{ pendingLabel: 'discarding…' }
|
|
841
|
+
)
|
|
842
|
+
},
|
|
843
|
+
'markdown-toggle': el => toggleMarkdownPreview(el),
|
|
621
844
|
'composer-cancel': () => {
|
|
622
845
|
closeComposers(root)
|
|
623
846
|
},
|
|
@@ -686,7 +909,7 @@ export function wireReview(root, session, opts = {}) {
|
|
|
686
909
|
}
|
|
687
910
|
},
|
|
688
911
|
signoff: el => {
|
|
689
|
-
openSignoff(el, el.getAttribute('data-event')
|
|
912
|
+
openSignoff(el, signoffEvent(el.getAttribute('data-event')))
|
|
690
913
|
},
|
|
691
914
|
'signoff-post': el => postSignoff(el),
|
|
692
915
|
'signoff-close': el => {
|
|
@@ -744,6 +967,10 @@ export function wireReview(root, session, opts = {}) {
|
|
|
744
967
|
/** @param {Event} event */
|
|
745
968
|
const onChange = event => {
|
|
746
969
|
const input = event.target
|
|
970
|
+
if (input instanceof HTMLSelectElement && input.id === FOLD_LEVEL_SELECT_ID) {
|
|
971
|
+
applyFoldLevel(input.value)
|
|
972
|
+
return
|
|
973
|
+
}
|
|
747
974
|
if (!(input instanceof HTMLInputElement)) {
|
|
748
975
|
return
|
|
749
976
|
}
|
|
@@ -849,10 +1076,7 @@ export function wireReview(root, session, opts = {}) {
|
|
|
849
1076
|
if (found === null || found === undefined || found.kind === 'overview') {
|
|
850
1077
|
break
|
|
851
1078
|
}
|
|
852
|
-
const id =
|
|
853
|
-
found.kind === 'file'
|
|
854
|
-
? `layer:${found.layerId}/file:${sanitizeKey(found.path)}`
|
|
855
|
-
: `layer:${found.layerId}`
|
|
1079
|
+
const id = found.kind === 'file' ? reviewedId(found.layerKey, found.path) : reviewedId(found.layerKey)
|
|
856
1080
|
const parts = cardForReviewedId(root, id)
|
|
857
1081
|
const box = parts?.card.querySelector('input[data-reviewed-id]')
|
|
858
1082
|
if (box instanceof HTMLElement) {
|
|
@@ -878,6 +1102,10 @@ export function wireReview(root, session, opts = {}) {
|
|
|
878
1102
|
}
|
|
879
1103
|
break
|
|
880
1104
|
}
|
|
1105
|
+
case 'fold-level':
|
|
1106
|
+
// The key steps through the levels, so the reader can open the code up without the mouse.
|
|
1107
|
+
applyFoldLevel(nextFoldLevel(getFoldLevel()))
|
|
1108
|
+
break
|
|
881
1109
|
case 'overview':
|
|
882
1110
|
focusPointId = null
|
|
883
1111
|
focusItem('overview')
|
package/static/js/keyboard.js
CHANGED
|
@@ -6,7 +6,7 @@ import { esc } from './dom.js'
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {'next-layer' | 'prev-layer' | 'next-file' | 'prev-file' | 'next-point' | 'prev-point'
|
|
8
8
|
* | 'toggle' | 'reviewed-file' | 'reviewed-layer' | 'comment' | 'dismiss' | 'overview'
|
|
9
|
-
* | 'help' | 'escape' | 'ask' | 'focus-chat'} KeyAction
|
|
9
|
+
* | 'help' | 'escape' | 'ask' | 'focus-chat' | 'fold-level'} KeyAction
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export const KEY_HELP = [
|
|
@@ -18,6 +18,7 @@ export const KEY_HELP = [
|
|
|
18
18
|
{ keys: 'R', what: 'mark the layer in focus reviewed and move on' },
|
|
19
19
|
{ keys: 'c', what: 'comment on the line in focus or on the selection' },
|
|
20
20
|
{ keys: 'd', what: 'dismiss the attention point in focus' },
|
|
21
|
+
{ keys: 'f', what: 'step through how much code is hidden: light, moderate, aggressive' },
|
|
21
22
|
{ keys: 'g o', what: 'go to the overview' },
|
|
22
23
|
{ keys: '?', what: 'this help' },
|
|
23
24
|
{ keys: 'Esc', what: 'clear the selection, close a composer or a dialog' },
|
|
@@ -84,6 +85,8 @@ export function keyAction(event, opts = {}) {
|
|
|
84
85
|
return { action: 'comment', pendingG: false }
|
|
85
86
|
case 'd':
|
|
86
87
|
return { action: 'dismiss', pendingG: false }
|
|
88
|
+
case 'f':
|
|
89
|
+
return { action: 'fold-level', pendingG: false }
|
|
87
90
|
case 'g':
|
|
88
91
|
return { action: null, pendingG: true }
|
|
89
92
|
case '?':
|
package/static/js/keys.js
CHANGED
|
@@ -95,3 +95,15 @@ export function layerAnchorId(layerKey) {
|
|
|
95
95
|
export function pointAnchorId(pointId) {
|
|
96
96
|
return `point-${pointId}`
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The id of a reviewed mark: one layer, or one file of it. The layer's key names it, so a
|
|
101
|
+
* regenerated canvas that reorders its layers still points a reviewer's progress at the same
|
|
102
|
+
* concern. The server (`src/contract/keys.ts`) and the page both call this, so the two sides
|
|
103
|
+
* cannot drift; `REVIEWED_ID_RE` in the state store validates this grammar at the route.
|
|
104
|
+
* @param {string} layerKey
|
|
105
|
+
* @param {string} [path]
|
|
106
|
+
*/
|
|
107
|
+
export function reviewedId(layerKey, path) {
|
|
108
|
+
return path === undefined ? `layer:${layerKey}` : `layer:${layerKey}/file:${sanitizeKey(path)}`
|
|
109
|
+
}
|