@vintasoftware/pr-review-canvas 0.2.0 → 0.3.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 +9 -3
- package/docs/reference.md +99 -99
- package/package.json +12 -3
- package/pr-review.config.example.yml +4 -4
- package/skills/pr-review-canvas/SKILL.md +8 -1
- package/src/acpx/acpx.ts +21 -4
- package/src/acpx/events.ts +5 -1
- package/src/acpx/ndjson.ts +3 -1
- package/src/acpx/preflight.ts +5 -1
- package/src/canvas/export.ts +11 -2
- package/src/canvas/zip.ts +4 -1
- package/src/chat/chat-manager.ts +16 -3
- package/src/chat/context.ts +10 -3
- package/src/chat/seed.ts +3 -1
- package/src/cli.ts +12 -2
- package/src/commands.ts +41 -9
- package/src/config.ts +15 -3
- package/src/contract/comments.ts +3 -1
- package/src/contract/generation-context.ts +3 -1
- package/src/contract/review-artifact.ts +14 -8
- package/src/contract/state.ts +3 -1
- package/src/git/diff-collector.ts +2 -1
- package/src/git/git.ts +9 -4
- package/src/github/capabilities.ts +5 -1
- package/src/github/comments.ts +6 -3
- package/src/github/gh.ts +18 -3
- package/src/github/post-comment.ts +4 -1
- package/src/github/pr.ts +4 -1
- package/src/github/threads.ts +5 -1
- package/src/project-config.ts +12 -9
- package/src/prompt-files.ts +4 -3
- package/src/review/doctor.ts +9 -2
- package/src/review/normalize.ts +3 -1
- package/src/review/prepare.ts +24 -8
- package/src/review/prompt.ts +8 -2
- package/src/review/publish.ts +15 -3
- package/src/review/trim-caps.ts +10 -6
- package/src/review/validate.ts +65 -13
- package/src/server/app.ts +12 -4
- package/src/server/bundle.ts +17 -4
- package/src/server/context.ts +14 -2
- package/src/server/errors.ts +26 -2
- package/src/server/html.ts +2 -2
- package/src/server/node-server.ts +4 -2
- package/src/server/routes/api.ts +29 -5
- package/src/server/routes/chat-routes.ts +30 -9
- package/src/server/routes/pages.ts +3 -1
- package/src/server/routes/review-routes.ts +19 -4
- package/src/server/sse.ts +3 -1
- package/src/store/atomic-json.ts +5 -1
- package/src/store/canvas-store.ts +20 -6
- package/src/store/derived-store.ts +5 -1
- package/src/store/pr-store.ts +2 -1
- package/src/store/state-store.ts +2 -1
- package/static/brand.svg +19 -0
- package/static/js/api.js +15 -3
- package/static/js/app.js +8 -2
- package/static/js/chat-panel.js +93 -0
- package/static/js/chat.js +38 -15
- package/static/js/composer.js +6 -3
- package/static/js/diagram.js +2 -1
- package/static/js/diff-decorations.js +5 -3
- package/static/js/diff-renderer.js +6 -1
- package/static/js/dom.js +5 -7
- package/static/js/empty-state.js +3 -1
- package/static/js/header.js +1 -1
- package/static/js/interactions.js +55 -15
- package/static/js/keyboard.js +7 -2
- package/static/js/layers.js +23 -11
- package/static/js/links.js +8 -2
- package/static/js/nav.js +5 -2
- package/static/js/overview.js +32 -4
- package/static/js/points.js +3 -2
- package/static/js/progress.js +2 -1
- package/static/js/proposed-comment.js +4 -1
- package/static/js/quick-questions.js +2 -1
- package/static/js/regenerate.js +4 -1
- package/static/js/signoff.js +6 -2
- package/static/styles/base.css +16 -6
- package/static/styles/chat-panel.css +81 -0
- package/static/styles/chat-tools.css +28 -0
- package/static/styles/chat.css +1 -1
- package/static/styles/commands.css +4 -4
- package/static/styles/diff.css +1 -1
- package/static/styles/header.css +18 -4
- package/static/styles/layout.css +4 -4
- package/static/styles/responsive.css +1 -15
- package/static/styles/review.css +24 -3
- package/static/styles/skin-github.css +93 -100
- package/static/styles.css +13 -12
|
@@ -27,13 +27,25 @@ import { flash, scrollIntoViewSafe } from './dom.js'
|
|
|
27
27
|
import { refreshProgress } from './header.js'
|
|
28
28
|
import { keyAction, openHelpDialog } from './keyboard.js'
|
|
29
29
|
import { pointAnchorId, sanitizeKey } from './keys.js'
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getRenderContext,
|
|
32
|
+
pathSet,
|
|
33
|
+
setCardRenderedHook,
|
|
34
|
+
setRenderContext,
|
|
35
|
+
updateRenderState,
|
|
36
|
+
} from './layers.js'
|
|
31
37
|
import { buildNavOrder, layerOf, nextFile, nextLayer, prevFile, prevLayer } from './nav.js'
|
|
32
38
|
import { issueCommentHtml } from './overview.js'
|
|
33
39
|
import { applyDismissed, pointToMarkdown, postedUrls } from './points.js'
|
|
34
40
|
import { layerProgress } from './progress.js'
|
|
35
41
|
import { lineRefFromEvent, markSelection, selectionReducer } from './selection.js'
|
|
36
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
fillSignoffDialog,
|
|
44
|
+
openSignoffDialog,
|
|
45
|
+
showSignoffError,
|
|
46
|
+
showSignoffResult,
|
|
47
|
+
signoffBody,
|
|
48
|
+
} from './signoff.js'
|
|
37
49
|
|
|
38
50
|
/** @typedef {NonNullable<ReturnType<typeof import('./chat.js').wireChat>>} ChatHandle */
|
|
39
51
|
|
|
@@ -148,7 +160,8 @@ export function nextUnreviewedTarget(root, session, fromId, kind) {
|
|
|
148
160
|
if (item === undefined || item.kind !== kind || item.other) {
|
|
149
161
|
continue
|
|
150
162
|
}
|
|
151
|
-
const id =
|
|
163
|
+
const id =
|
|
164
|
+
item.kind === 'file' ? `layer:${item.layerId}/file:${sanitizeKey(item.path)}` : `layer:${item.layerId}`
|
|
152
165
|
if (!session.isReviewed(id)) {
|
|
153
166
|
return root.querySelector(`#${cssEscape(item.id)}`)
|
|
154
167
|
}
|
|
@@ -167,7 +180,13 @@ export function nextUnreviewedTarget(root, session, fromId, kind) {
|
|
|
167
180
|
*/
|
|
168
181
|
export function askTargetFor(root, focusId, focusPointId, selection) {
|
|
169
182
|
if (selection !== null) {
|
|
170
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
kind: 'lines',
|
|
185
|
+
path: selection.path,
|
|
186
|
+
side: selection.side,
|
|
187
|
+
start: selection.start,
|
|
188
|
+
end: selection.end,
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
191
|
if (focusPointId !== null) {
|
|
173
192
|
const el = root.querySelector(`[data-point="${cssEscape(focusPointId)}"] [data-act="ask"]`)
|
|
@@ -213,7 +232,8 @@ export function wireReview(root, session, opts = {}) {
|
|
|
213
232
|
|
|
214
233
|
const paths = () => pathSet(getRenderContext()?.files ?? [])
|
|
215
234
|
/** The diff key of a path, which is what the row ids are built from. */
|
|
216
|
-
const keyForPath = (/** @type {string} */ path) =>
|
|
235
|
+
const keyForPath = (/** @type {string} */ path) =>
|
|
236
|
+
getRenderContext()?.files.find(f => f.path === path)?.key ?? null
|
|
217
237
|
/** The time the page was drawn, which the comments it adds are timed against. */
|
|
218
238
|
const renderNow = () => getRenderContext()?.now ?? new Date()
|
|
219
239
|
|
|
@@ -263,7 +283,10 @@ export function wireReview(root, session, opts = {}) {
|
|
|
263
283
|
const answer = await session.postComment(input)
|
|
264
284
|
const ctx = getRenderContext()
|
|
265
285
|
if (answer.kind === 'review' && ctx !== null) {
|
|
266
|
-
setRenderContext({
|
|
286
|
+
setRenderContext({
|
|
287
|
+
...ctx,
|
|
288
|
+
comments: [...ctx.comments.filter(c => c.id !== answer.comment.id), answer.comment],
|
|
289
|
+
})
|
|
267
290
|
onState(session.state)
|
|
268
291
|
}
|
|
269
292
|
return answer
|
|
@@ -325,7 +348,8 @@ export function wireReview(root, session, opts = {}) {
|
|
|
325
348
|
*/
|
|
326
349
|
const openComposer = (anchor, options, shape) => {
|
|
327
350
|
closeComposers(root)
|
|
328
|
-
const node =
|
|
351
|
+
const node =
|
|
352
|
+
shape === 'row' ? rowFrom(doc, composerRowHtml(options)) : nodeFrom(doc, composerHtml(options))
|
|
329
353
|
if (node === null) {
|
|
330
354
|
return null
|
|
331
355
|
}
|
|
@@ -364,7 +388,10 @@ export function wireReview(root, session, opts = {}) {
|
|
|
364
388
|
}
|
|
365
389
|
|
|
366
390
|
/** The inline comment that a posted review comment becomes on the page. */
|
|
367
|
-
const insertPostedReviewComment = (
|
|
391
|
+
const insertPostedReviewComment = (
|
|
392
|
+
/** @type {Element} */ composerNode,
|
|
393
|
+
/** @type {ReviewComment} */ comment
|
|
394
|
+
) => {
|
|
368
395
|
const thread = root.querySelector(
|
|
369
396
|
`tr.thread[data-thread="${cssEscape(String(comment.inReplyToId ?? comment.id))}"]`
|
|
370
397
|
)
|
|
@@ -461,9 +488,9 @@ export function wireReview(root, session, opts = {}) {
|
|
|
461
488
|
* @param {string} id
|
|
462
489
|
* @param {boolean} reviewed
|
|
463
490
|
* @param {'layer' | 'file'} kind
|
|
464
|
-
* @param {{ quiet?: boolean }} [
|
|
491
|
+
* @param {{ quiet?: boolean }} [reviewOptions] `quiet` keeps a checkbox label intact while it runs
|
|
465
492
|
*/
|
|
466
|
-
const markReviewed = (button, id, reviewed, kind,
|
|
493
|
+
const markReviewed = (button, id, reviewed, kind, reviewOptions = {}) => {
|
|
467
494
|
const run = async () => {
|
|
468
495
|
await session.setReviewed(id, reviewed)
|
|
469
496
|
const parts = cardForReviewedId(root, id)
|
|
@@ -479,7 +506,7 @@ export function wireReview(root, session, opts = {}) {
|
|
|
479
506
|
}
|
|
480
507
|
toast(root, reviewed ? `${kind} marked reviewed` : `${kind} reopened`)
|
|
481
508
|
}
|
|
482
|
-
void (
|
|
509
|
+
void (reviewOptions.quiet === true
|
|
483
510
|
? runControl(button, run)
|
|
484
511
|
: runCommand(button, run, { pendingLabel: reviewed ? 'marking…' : 'unmarking…' }))
|
|
485
512
|
}
|
|
@@ -641,7 +668,11 @@ export function wireReview(root, session, opts = {}) {
|
|
|
641
668
|
return
|
|
642
669
|
}
|
|
643
670
|
composerSeq += 1
|
|
644
|
-
openComposer(
|
|
671
|
+
openComposer(
|
|
672
|
+
full,
|
|
673
|
+
{ id: `composer-${composerSeq}`, label: 'Reply', kind: 'reply', inReplyToId: id },
|
|
674
|
+
'block'
|
|
675
|
+
)
|
|
645
676
|
},
|
|
646
677
|
'pr-comment': () => {
|
|
647
678
|
const host = root.querySelector('.conversation .pr-composer-host')
|
|
@@ -733,7 +764,10 @@ export function wireReview(root, session, opts = {}) {
|
|
|
733
764
|
}
|
|
734
765
|
event.preventDefault()
|
|
735
766
|
setSelection(
|
|
736
|
-
selectionReducer(
|
|
767
|
+
selectionReducer(
|
|
768
|
+
selection,
|
|
769
|
+
event.shiftKey ? { type: 'shift-click', target } : { type: 'click', target }
|
|
770
|
+
)
|
|
737
771
|
)
|
|
738
772
|
if (selection !== null && !event.shiftKey) {
|
|
739
773
|
setSelection(selectionReducer(selection, { type: 'drag-start', target }))
|
|
@@ -807,12 +841,18 @@ export function wireReview(root, session, opts = {}) {
|
|
|
807
841
|
const item = order.find(i => i.id === focusId)
|
|
808
842
|
const wanted = decided.action === 'reviewed-file' ? 'file' : 'layer'
|
|
809
843
|
const found =
|
|
810
|
-
item?.kind === wanted
|
|
844
|
+
item?.kind === wanted
|
|
845
|
+
? item
|
|
846
|
+
: wanted === 'layer' && focusId !== null
|
|
847
|
+
? layerOf(order, focusId)
|
|
848
|
+
: null
|
|
811
849
|
if (found === null || found === undefined || found.kind === 'overview') {
|
|
812
850
|
break
|
|
813
851
|
}
|
|
814
852
|
const id =
|
|
815
|
-
found.kind === 'file'
|
|
853
|
+
found.kind === 'file'
|
|
854
|
+
? `layer:${found.layerId}/file:${sanitizeKey(found.path)}`
|
|
855
|
+
: `layer:${found.layerId}`
|
|
816
856
|
const parts = cardForReviewedId(root, id)
|
|
817
857
|
const box = parts?.card.querySelector('input[data-reviewed-id]')
|
|
818
858
|
if (box instanceof HTMLElement) {
|
package/static/js/keyboard.js
CHANGED
|
@@ -35,7 +35,10 @@ export function isTypingTarget(target) {
|
|
|
35
35
|
}
|
|
36
36
|
const tag = target.tagName.toLowerCase()
|
|
37
37
|
return (
|
|
38
|
-
tag === 'input' ||
|
|
38
|
+
tag === 'input' ||
|
|
39
|
+
tag === 'textarea' ||
|
|
40
|
+
tag === 'select' ||
|
|
41
|
+
target.closest('[contenteditable="true"]') !== null
|
|
39
42
|
)
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -97,7 +100,9 @@ export function keyAction(event, opts = {}) {
|
|
|
97
100
|
export const HELP_DIALOG_ID = 'help-dialog'
|
|
98
101
|
|
|
99
102
|
export function helpDialogHtml() {
|
|
100
|
-
const rows = KEY_HELP.map(r => `<tr><td class="mono">${esc(r.keys)}</td><td>${esc(r.what)}</td></tr>`).join(
|
|
103
|
+
const rows = KEY_HELP.map(r => `<tr><td class="mono">${esc(r.keys)}</td><td>${esc(r.what)}</td></tr>`).join(
|
|
104
|
+
''
|
|
105
|
+
)
|
|
101
106
|
return (
|
|
102
107
|
`<dialog id="${HELP_DIALOG_ID}" class="help" aria-labelledby="help-h"><form method="dialog">` +
|
|
103
108
|
'<h2 id="help-h">Keyboard</h2>' +
|
package/static/js/layers.js
CHANGED
|
@@ -117,7 +117,8 @@ export function railInnerHtml(artifact, state, opts = {}) {
|
|
|
117
117
|
.map((layer, i) => {
|
|
118
118
|
const progress = layerProgress(layer, state)
|
|
119
119
|
const dotClass = progress === 'done' ? 'dot on' : progress === 'partial' ? 'dot half' : 'dot'
|
|
120
|
-
const dotLabel =
|
|
120
|
+
const dotLabel =
|
|
121
|
+
progress === 'done' ? 'reviewed' : progress === 'partial' ? 'in progress' : 'not started'
|
|
121
122
|
const reviewed = filesReviewed(layer, state)
|
|
122
123
|
const files = layer.files.length
|
|
123
124
|
const meta = reviewed > 0 && reviewed < files ? `${reviewed} of ${files} files` : fileCount(files)
|
|
@@ -206,7 +207,11 @@ export function layerDiagramHtml(layer) {
|
|
|
206
207
|
* @param {ReadonlySet<string>} paths
|
|
207
208
|
*/
|
|
208
209
|
export function judgmentHtml(layer, paths) {
|
|
209
|
-
const block = /** @param {string} cls @param {string} label @param {string | undefined} text */ (
|
|
210
|
+
const block = /** @param {string} cls @param {string} label @param {string | undefined} text */ (
|
|
211
|
+
cls,
|
|
212
|
+
label,
|
|
213
|
+
text
|
|
214
|
+
) =>
|
|
210
215
|
text === undefined
|
|
211
216
|
? ''
|
|
212
217
|
: `<div class="judgment ${cls}"><h3 class="lbl">${label}</h3><div class="prose">${renderMarkdown(text, { paths })}</div></div>`
|
|
@@ -230,7 +235,9 @@ export function layerPointsHtml(layer, points, paths, state, posted) {
|
|
|
230
235
|
return ''
|
|
231
236
|
}
|
|
232
237
|
const active = own.filter(p => state.dismissed[p.fingerprint] === undefined).length
|
|
233
|
-
const cards = own.map(p =>
|
|
238
|
+
const cards = own.map(p =>
|
|
239
|
+
pointCardHtml(p, posted === undefined ? { paths, state } : { paths, state, posted })
|
|
240
|
+
)
|
|
234
241
|
return `<h3 class="lbl sub">Attention points · <span class="point-count">${active}</span></h3><ol class="findings">${cards.join('')}</ol>`
|
|
235
242
|
}
|
|
236
243
|
|
|
@@ -245,14 +252,16 @@ export function renderFileCard(lf, entry, layer, ctx) {
|
|
|
245
252
|
const key = entry?.key ?? sanitizeKey(lf.path)
|
|
246
253
|
const cardReviewedId = `layer:${layer.id}/file:${sanitizeKey(lf.path)}`
|
|
247
254
|
const cardReviewed = ctx.state?.reviewed[cardReviewedId] === true
|
|
248
|
-
const collapsed =
|
|
255
|
+
const collapsed =
|
|
256
|
+
cardReviewed || (lf.collapsed === true && lf.annotations.length === 0 && ctx.keepOpen !== true)
|
|
249
257
|
const isFirst = !ctx.firstCardFor.has(key)
|
|
250
258
|
ctx.firstCardFor.add(key)
|
|
251
259
|
const id = isFirst ? fileAnchorId(key) : `${fileAnchorId(key)}-${layer.key}`
|
|
252
260
|
const pills = entry
|
|
253
261
|
? `<span class="pill add">+${entry.additions}</span><span class="pill del">−${entry.deletions}</span>`
|
|
254
262
|
: ''
|
|
255
|
-
const status =
|
|
263
|
+
const status =
|
|
264
|
+
entry && entry.status !== 'modified' ? `<span class="status">${esc(entry.status)}</span>` : ''
|
|
256
265
|
const path =
|
|
257
266
|
entry?.oldPath !== undefined
|
|
258
267
|
? `<span class="old">${esc(entry.oldPath)} → </span>${esc(lf.path)}`
|
|
@@ -411,7 +420,8 @@ export function hydrateFileCard(card, ctx, opts = {}) {
|
|
|
411
420
|
const entry = ctx.files.find(f => f.key === key)
|
|
412
421
|
const patch = ctx.patches?.[key]
|
|
413
422
|
if (!entry || patch === undefined) {
|
|
414
|
-
host.innerHTML =
|
|
423
|
+
host.innerHTML =
|
|
424
|
+
'<div class="unavailable">Diff not available locally. Fetch the PR head and reload.</div>'
|
|
415
425
|
return { rendered: false, deferred: false, placed: 0, missed: 0 }
|
|
416
426
|
}
|
|
417
427
|
const lines = patchLineCount(patch)
|
|
@@ -432,10 +442,10 @@ export function hydrateFileCard(card, ctx, opts = {}) {
|
|
|
432
442
|
async () => {
|
|
433
443
|
hydrateFileCard(card, ctx, { force: true })
|
|
434
444
|
// The host element now holds a drawn diff, so a later link into it leaves it alone.
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
445
|
+
const fileHost = card.closest('pr-file')
|
|
446
|
+
if (fileHost instanceof PrFileElement) {
|
|
447
|
+
fileHost.rendered = true
|
|
448
|
+
fileHost.deferred = false
|
|
439
449
|
}
|
|
440
450
|
},
|
|
441
451
|
{ pendingLabel: 'drawing…' }
|
|
@@ -450,7 +460,9 @@ export function hydrateFileCard(card, ctx, opts = {}) {
|
|
|
450
460
|
const layer = ctx.artifact.layers.find(l => l.id === layerId)
|
|
451
461
|
const lf = layer?.files.find(f => f.path === entry.path)
|
|
452
462
|
const annotations = lf?.annotations ?? []
|
|
453
|
-
const points = ctx.artifact.points.filter(
|
|
463
|
+
const points = ctx.artifact.points.filter(
|
|
464
|
+
p => p.path === entry.path && hunkIds.has(hunkIdForPoint(p, entry))
|
|
465
|
+
)
|
|
454
466
|
const threads = threadsForHunks(ctx.comments, entry, hunkIds)
|
|
455
467
|
const { placed, missed } = applyDecorations(card, key, {
|
|
456
468
|
annotations,
|
package/static/js/links.js
CHANGED
|
@@ -99,14 +99,20 @@ export function resolveLink(link, targets) {
|
|
|
99
99
|
return { ok: false, message: `${link.path} is not in the diff` }
|
|
100
100
|
}
|
|
101
101
|
if (link.kind === 'hunk' && (link.n < 1 || link.n > file.hunks.length)) {
|
|
102
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
message: `${link.path}#${link.n} does not exist (file has ${file.hunks.length} hunks)`,
|
|
105
|
+
}
|
|
103
106
|
}
|
|
104
107
|
if (link.kind === 'line') {
|
|
105
108
|
const start = hunkForLine(file.hunks, link.side, link.start)
|
|
106
109
|
const end = hunkForLine(file.hunks, link.side, link.end)
|
|
107
110
|
if (start === null || start !== end) {
|
|
108
111
|
const range = link.end === link.start ? `${link.start}` : `${link.start}-${link.end}`
|
|
109
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
message: `${link.path}:${range} (${link.side}) is not inside one hunk of the diff (${hunkLineRanges(file.hunks, link.side)})`,
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
return { ok: true }
|
package/static/js/nav.js
CHANGED
|
@@ -46,7 +46,8 @@ export function buildNavOrder(artifact) {
|
|
|
46
46
|
* @returns {NavItem | null}
|
|
47
47
|
*/
|
|
48
48
|
function step(order, currentId, kind, direction) {
|
|
49
|
-
const from =
|
|
49
|
+
const from =
|
|
50
|
+
currentId === null ? (direction === 1 ? -1 : order.length) : order.findIndex(i => i.id === currentId)
|
|
50
51
|
const start = from === -1 && currentId !== null ? (direction === 1 ? -1 : order.length) : from
|
|
51
52
|
for (let i = start + direction; i >= 0 && i < order.length; i += direction) {
|
|
52
53
|
const item = order[i]
|
|
@@ -87,5 +88,7 @@ export function layerOf(order, id) {
|
|
|
87
88
|
if (!item || item.kind === 'overview') {
|
|
88
89
|
return null
|
|
89
90
|
}
|
|
90
|
-
return item.kind === 'layer'
|
|
91
|
+
return item.kind === 'layer'
|
|
92
|
+
? item
|
|
93
|
+
: (order.find(i => i.kind === 'layer' && i.layerId === item.layerId) ?? null)
|
|
91
94
|
}
|
package/static/js/overview.js
CHANGED
|
@@ -2,11 +2,32 @@
|
|
|
2
2
|
/** @typedef {import('./contract-types.js').PrBundle} PrBundle */
|
|
3
3
|
/** @typedef {import('./contract-types.js').IssueComment} IssueComment */
|
|
4
4
|
|
|
5
|
-
import { commentHtml } from './diff-decorations.js'
|
|
6
5
|
import { viewCommentHtml } from './comment-link.js'
|
|
7
6
|
import { detailsSummaryHtml, esc, avatarHtml, timeAgo } from './dom.js'
|
|
8
7
|
import { renderMarkdown } from './markdown.js'
|
|
9
8
|
import { dismissedListHtml, postedUrls, sevsumHtml } from './points.js'
|
|
9
|
+
import { buildThreads } from './threads.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {PrBundle['comments']['reviewComments']} comments
|
|
13
|
+
* @param {Date} now
|
|
14
|
+
*/
|
|
15
|
+
function outdatedCommentsHtml(comments, now) {
|
|
16
|
+
const threads = [...buildThreads(comments).outdated.values()].flat()
|
|
17
|
+
if (!threads.length) return ''
|
|
18
|
+
const count = threads.reduce((total, thread) => total + 1 + thread.replies.length, 0)
|
|
19
|
+
const entries = threads
|
|
20
|
+
.map(thread => {
|
|
21
|
+
const line = thread.root.originalLine
|
|
22
|
+
return (
|
|
23
|
+
`<div class="body"><p class="muted small">${esc(thread.path)}${line === null ? '' : `:${line} (original)`}${thread.resolved ? ' · resolved' : ''}</p>` +
|
|
24
|
+
[thread.root, ...thread.replies].map(comment => issueCommentHtml(comment, now)).join('') +
|
|
25
|
+
'</div>'
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
.join('')
|
|
29
|
+
return `<details class="outdated-comments">${detailsSummaryHtml(`<span>Outdated comments · ${count}</span>`, 'Toggle outdated comments')}${entries}</details>`
|
|
30
|
+
}
|
|
10
31
|
|
|
11
32
|
/**
|
|
12
33
|
* "What changes": the PR-wide summary as one prose block.
|
|
@@ -61,7 +82,12 @@ export function conversationHtml(comments, now) {
|
|
|
61
82
|
*/
|
|
62
83
|
export function renderOverview(bundle, ctx) {
|
|
63
84
|
const artifact = bundle.artifact
|
|
64
|
-
const
|
|
85
|
+
const reviews = (bundle.comments.reviews ?? []).filter(
|
|
86
|
+
review => review.state !== 'COMMENTED' || review.body.trim().length > 0
|
|
87
|
+
)
|
|
88
|
+
const active = artifact
|
|
89
|
+
? artifact.points.filter(p => bundle.state.dismissed[p.fingerprint] === undefined)
|
|
90
|
+
: []
|
|
65
91
|
const summary = artifact ? summaryHtml(artifact.summary, ctx.paths) : ''
|
|
66
92
|
const description = bundle.pr.body.trim()
|
|
67
93
|
? `<details class="pr-desc">${detailsSummaryHtml('<span>PR description (from GitHub)</span>', 'Toggle PR description')}<div class="body prose">${renderMarkdown(bundle.pr.body, { paths: ctx.paths, github: true })}</div></details>`
|
|
@@ -72,8 +98,10 @@ export function renderOverview(bundle, ctx) {
|
|
|
72
98
|
`<div class="body">${summary}</div>` +
|
|
73
99
|
description +
|
|
74
100
|
conversationHtml(bundle.comments.issueComments, ctx.now) +
|
|
75
|
-
(
|
|
76
|
-
|
|
101
|
+
(reviews.length
|
|
102
|
+
? `<details class="review-history">${detailsSummaryHtml(`<span>Review history · ${reviews.length}</span>`, 'Toggle review history')}${reviews.map(review => `<div class="body review-entry"><span class="pill">${esc(review.state.toLowerCase().replaceAll('_', ' '))}</span>${issueCommentHtml(review, ctx.now)}</div>`).join('')}</details>`
|
|
103
|
+
: '') +
|
|
104
|
+
outdatedCommentsHtml(bundle.comments.reviewComments, ctx.now) +
|
|
77
105
|
(artifact
|
|
78
106
|
? dismissedListHtml(artifact.points, bundle.state, {
|
|
79
107
|
paths: ctx.paths,
|
package/static/js/points.js
CHANGED
|
@@ -190,7 +190,8 @@ export function applyDismissed(root, points, state, ctx) {
|
|
|
190
190
|
}
|
|
191
191
|
const host = root.querySelector('.dismissed-list')
|
|
192
192
|
if (host !== null) {
|
|
193
|
-
const expanded =
|
|
193
|
+
const expanded =
|
|
194
|
+
host.querySelector('[data-act="show-dismissed"]')?.getAttribute('aria-expanded') === 'true'
|
|
194
195
|
const template = document.createElement('template')
|
|
195
196
|
template.innerHTML = dismissedListHtml(points, state, ctx, expanded)
|
|
196
197
|
const next = template.content.firstElementChild
|
|
@@ -219,7 +220,7 @@ export function pointRowHtml(p, ctx) {
|
|
|
219
220
|
const dismissed = ctx.state?.dismissed[p.fingerprint] !== undefined
|
|
220
221
|
const posted = postedFor(p, ctx)
|
|
221
222
|
return (
|
|
222
|
-
`<tr class="ifind ${p.level}" data-point="${esc(p.id)}" data-fingerprint="${esc(p.fingerprint)}"${dismissed ? ' hidden' : ''}><td class="
|
|
223
|
+
`<tr class="ifind ${p.level}" data-point="${esc(p.id)}" data-fingerprint="${esc(p.fingerprint)}"${dismissed ? ' hidden' : ''}><td class="code x" colspan="4">` +
|
|
223
224
|
`<div class="f-title">${squareHtml(p)}<span>${esc(p.title)}</span><span class="pill kind">${esc(p.kind)}</span></div>` +
|
|
224
225
|
`<div class="prose">${renderMarkdown(p.body, { paths: ctx.paths })}</div>` +
|
|
225
226
|
`${pointCommandsHtml(p, { postedUrl: posted })}</td></tr>`
|
package/static/js/progress.js
CHANGED
|
@@ -45,5 +45,6 @@ export function progressSummary(artifact, state) {
|
|
|
45
45
|
* @param {PrState} state
|
|
46
46
|
*/
|
|
47
47
|
export function filesReviewed(layer, state) {
|
|
48
|
-
return layer.files.filter(f => state.reviewed[`layer:${layer.id}/file:${sanitizeKey(f.path)}`] === true)
|
|
48
|
+
return layer.files.filter(f => state.reviewed[`layer:${layer.id}/file:${sanitizeKey(f.path)}`] === true)
|
|
49
|
+
.length
|
|
49
50
|
}
|
|
@@ -63,7 +63,10 @@ export function parseProposedComment(source, targets) {
|
|
|
63
63
|
return { reason: 'side must be "new" or "old"' }
|
|
64
64
|
}
|
|
65
65
|
const side = sideRaw === 'old' ? 'old' : 'new'
|
|
66
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
startRaw !== undefined &&
|
|
68
|
+
(typeof startRaw !== 'number' || !Number.isInteger(startRaw) || startRaw <= 0)
|
|
69
|
+
) {
|
|
67
70
|
return { reason: 'startLine must be a line number' }
|
|
68
71
|
}
|
|
69
72
|
const startLine = typeof startRaw === 'number' ? startRaw : undefined
|
|
@@ -18,7 +18,8 @@ export const ASK_SOMETHING_ELSE = 'ask something else…'
|
|
|
18
18
|
export function quickMenuHtml() {
|
|
19
19
|
const items = [...QUICK_QUESTIONS, ASK_SOMETHING_ELSE]
|
|
20
20
|
.map(
|
|
21
|
-
(q, i) =>
|
|
21
|
+
(q, i) =>
|
|
22
|
+
`<button class="qq-item" type="button" role="menuitem" tabindex="-1" data-qq="${i}">${esc(q)}</button>`
|
|
22
23
|
)
|
|
23
24
|
.join('')
|
|
24
25
|
return `<div class="qq-menu" id="${QQ_MENU_ID}" role="menu" aria-label="Quick questions" hidden>${items}</div>`
|
package/static/js/regenerate.js
CHANGED
|
@@ -65,5 +65,8 @@ export function canvasChanged(current, next) {
|
|
|
65
65
|
if (current.status !== 'ready' || !current.artifact || !current.canvas) {
|
|
66
66
|
return true
|
|
67
67
|
}
|
|
68
|
-
return
|
|
68
|
+
return (
|
|
69
|
+
next.canvas.headSha !== current.canvas.headSha ||
|
|
70
|
+
next.artifact.generatedAt !== current.artifact.generatedAt
|
|
71
|
+
)
|
|
69
72
|
}
|
package/static/js/signoff.js
CHANGED
|
@@ -20,7 +20,9 @@ export const LOADING_REASON = 'the review body is still loading'
|
|
|
20
20
|
* @returns {string | null}
|
|
21
21
|
*/
|
|
22
22
|
export function approveBlockedReason(artifact, state) {
|
|
23
|
-
const [first, ...rest] = artifact.layers.filter(
|
|
23
|
+
const [first, ...rest] = artifact.layers.filter(
|
|
24
|
+
l => l.kind !== 'other' && layerProgress(l, state) !== 'done'
|
|
25
|
+
)
|
|
24
26
|
if (first === undefined) {
|
|
25
27
|
return null
|
|
26
28
|
}
|
|
@@ -75,7 +77,9 @@ export function openSignoffDialog(root, opts) {
|
|
|
75
77
|
if (!(dialog instanceof HTMLDialogElement)) {
|
|
76
78
|
throw new Error('sign-off dialog did not render')
|
|
77
79
|
}
|
|
78
|
-
dialog
|
|
80
|
+
dialog
|
|
81
|
+
.querySelector('[data-act="markdown-write"]')
|
|
82
|
+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
79
83
|
dialog.setAttribute('data-event', opts.event)
|
|
80
84
|
const heading = dialog.querySelector('#signoff-h')
|
|
81
85
|
if (heading !== null) {
|
package/static/styles/base.css
CHANGED
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
--s4: var(--v4);
|
|
23
23
|
--s5: var(--v5);
|
|
24
24
|
--s6: var(--v6);
|
|
25
|
-
--sans:
|
|
26
|
-
|
|
25
|
+
--sans:
|
|
26
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif,
|
|
27
|
+
'Apple Color Emoji', 'Segoe UI Emoji';
|
|
28
|
+
--mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
27
29
|
|
|
28
30
|
--v1: light-dark(#ffd43b, #ffe066);
|
|
29
31
|
--v2: light-dark(#ffa94d, #ffc078);
|
|
@@ -31,7 +33,15 @@
|
|
|
31
33
|
--v4: light-dark(#da77f2, #e599f7);
|
|
32
34
|
--v5: light-dark(#9775fa, #b197fc);
|
|
33
35
|
--v6: light-dark(#748ffc, #91a7ff);
|
|
34
|
-
--stripe-y: linear-gradient(
|
|
36
|
+
--stripe-y: linear-gradient(
|
|
37
|
+
180deg,
|
|
38
|
+
var(--v1) 0 1px,
|
|
39
|
+
var(--v2) 1px 2px,
|
|
40
|
+
var(--v3) 2px 3px,
|
|
41
|
+
var(--v4) 3px 4px,
|
|
42
|
+
var(--v5) 4px 5px,
|
|
43
|
+
var(--v6) 5px 6px
|
|
44
|
+
);
|
|
35
45
|
|
|
36
46
|
--bg: light-dark(#ffffff, #1e1f2b);
|
|
37
47
|
--panel: light-dark(#ffffff, #282a3a);
|
|
@@ -80,10 +90,10 @@
|
|
|
80
90
|
|
|
81
91
|
/* The server writes `data-theme` for every load. `auto` matches neither rule, which leaves the
|
|
82
92
|
`color-scheme: light dark` above to follow the operating system. */
|
|
83
|
-
:root[data-theme=
|
|
93
|
+
:root[data-theme='dark'] {
|
|
84
94
|
color-scheme: dark;
|
|
85
95
|
}
|
|
86
|
-
:root[data-theme=
|
|
96
|
+
:root[data-theme='light'] {
|
|
87
97
|
color-scheme: light;
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -218,6 +228,6 @@ strong {
|
|
|
218
228
|
}
|
|
219
229
|
|
|
220
230
|
/* The hidden attribute takes priority over component layout styles. */
|
|
221
|
-
[hidden]:not([hidden=
|
|
231
|
+
[hidden]:not([hidden='until-found']) {
|
|
222
232
|
display: none !important;
|
|
223
233
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
.chat-launcher,
|
|
2
|
+
.chat-minimize {
|
|
3
|
+
display: none;
|
|
4
|
+
}
|
|
5
|
+
@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
|
+
dialog.chat-dialog {
|
|
29
|
+
position: fixed;
|
|
30
|
+
inset: auto 20px 20px auto;
|
|
31
|
+
width: min(420px, calc(100vw - 40px));
|
|
32
|
+
height: min(680px, calc(100dvh - 40px));
|
|
33
|
+
max-width: none;
|
|
34
|
+
max-height: none;
|
|
35
|
+
margin: 0;
|
|
36
|
+
padding: 0;
|
|
37
|
+
border: 1px solid var(--ink);
|
|
38
|
+
border-radius: 12px;
|
|
39
|
+
background: var(--panel);
|
|
40
|
+
color: var(--fg);
|
|
41
|
+
box-shadow: 0 8px 32px #0004;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
z-index: 11;
|
|
44
|
+
}
|
|
45
|
+
.chat-dialog .chat {
|
|
46
|
+
position: relative;
|
|
47
|
+
top: auto;
|
|
48
|
+
height: 100%;
|
|
49
|
+
padding: 0;
|
|
50
|
+
}
|
|
51
|
+
.chat-dialog .chat-h,
|
|
52
|
+
.chat-dialog .ctx-line,
|
|
53
|
+
.chat-dialog .chat-composer {
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
}
|
|
56
|
+
.chat-dialog .chat-h h2 {
|
|
57
|
+
margin-bottom: 0;
|
|
58
|
+
}
|
|
59
|
+
.chat-dialog .chat-composer {
|
|
60
|
+
margin: 0;
|
|
61
|
+
padding: 12px 14px;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
}
|
|
64
|
+
.chat-dialog textarea {
|
|
65
|
+
max-height: 25dvh;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
@media (max-width: 600px) {
|
|
69
|
+
dialog.chat-dialog {
|
|
70
|
+
inset: auto 8px max(8px, env(safe-area-inset-bottom)) 8px;
|
|
71
|
+
width: auto;
|
|
72
|
+
height: calc(100dvh - 24px - env(safe-area-inset-bottom));
|
|
73
|
+
}
|
|
74
|
+
.chat-launcher {
|
|
75
|
+
right: 16px;
|
|
76
|
+
bottom: max(16px, env(safe-area-inset-bottom));
|
|
77
|
+
}
|
|
78
|
+
html:has(.chat-dialog:modal) {
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
/* Chat context, proposed comments, quick questions, and settings. */
|
|
2
|
+
.chat-tool-calls > summary {
|
|
3
|
+
padding: 4px 0;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
align-items: baseline;
|
|
6
|
+
overflow-wrap: anywhere;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
}
|
|
9
|
+
.chat-tool-calls > summary::before {
|
|
10
|
+
content: '';
|
|
11
|
+
width: 6px;
|
|
12
|
+
height: 6px;
|
|
13
|
+
flex: 0 0 6px;
|
|
14
|
+
border-right: 1.5px solid currentColor;
|
|
15
|
+
border-bottom: 1.5px solid currentColor;
|
|
16
|
+
transform: rotate(-45deg);
|
|
17
|
+
}
|
|
18
|
+
.chat-tool-calls[open] > summary::before {
|
|
19
|
+
transform: rotate(45deg);
|
|
20
|
+
}
|
|
21
|
+
.chat-tool-calls > ul {
|
|
22
|
+
margin: 4px 0 8px;
|
|
23
|
+
padding-left: 14px;
|
|
24
|
+
list-style: none;
|
|
25
|
+
}
|
|
26
|
+
.chat-tool-calls > ul > li {
|
|
27
|
+
margin: 4px 0;
|
|
28
|
+
overflow-wrap: anywhere;
|
|
29
|
+
}
|
|
2
30
|
.ctx-line {
|
|
3
31
|
display: flex;
|
|
4
32
|
align-items: baseline;
|