@vintasoftware/pr-review-canvas 0.1.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/LICENSE +202 -0
- package/README.md +192 -0
- package/bin/pr-review.mjs +5 -0
- package/docs/reference.md +340 -0
- package/package.json +74 -0
- package/pr-review.config.example.yml +68 -0
- package/prompts/chat-seed.md +64 -0
- package/prompts/generation-format.md +255 -0
- package/prompts/generation-strict.md +34 -0
- package/prompts/generation-surfacing.md +67 -0
- package/prompts/layers-default.md +13 -0
- package/prompts/quality-standards.md +32 -0
- package/skills/pr-review-canvas/SKILL.md +177 -0
- package/src/acpx/acpx.ts +530 -0
- package/src/acpx/agents.ts +85 -0
- package/src/acpx/events.ts +216 -0
- package/src/acpx/ndjson.ts +69 -0
- package/src/acpx/preflight.ts +44 -0
- package/src/canvas/export.ts +95 -0
- package/src/canvas/import.ts +138 -0
- package/src/canvas/name.ts +55 -0
- package/src/canvas/zip.ts +123 -0
- package/src/chat/chat-manager.ts +389 -0
- package/src/chat/context.ts +160 -0
- package/src/chat/seed.ts +71 -0
- package/src/chat/threads.ts +114 -0
- package/src/cli.ts +199 -0
- package/src/commands.ts +424 -0
- package/src/config.ts +142 -0
- package/src/contract/api.ts +190 -0
- package/src/contract/canvas-manifest.ts +29 -0
- package/src/contract/chat.ts +76 -0
- package/src/contract/comments.ts +96 -0
- package/src/contract/discovery.ts +20 -0
- package/src/contract/generation-context.ts +77 -0
- package/src/contract/keys.ts +14 -0
- package/src/contract/links.ts +5 -0
- package/src/contract/mermaid-fences.ts +4 -0
- package/src/contract/review-artifact.ts +324 -0
- package/src/contract/settings.ts +144 -0
- package/src/contract/state.ts +46 -0
- package/src/contract/validation.ts +43 -0
- package/src/git/diff-collector.ts +151 -0
- package/src/git/git.ts +115 -0
- package/src/git/lang.ts +1 -0
- package/src/git/materialize.ts +79 -0
- package/src/git/patch-lines.ts +60 -0
- package/src/github/attachments.ts +288 -0
- package/src/github/capabilities.ts +112 -0
- package/src/github/comments.ts +132 -0
- package/src/github/gh.ts +196 -0
- package/src/github/post-comment.ts +104 -0
- package/src/github/post-review.ts +44 -0
- package/src/github/pr.ts +133 -0
- package/src/github/review-body.ts +72 -0
- package/src/github/threads.ts +63 -0
- package/src/paths.ts +10 -0
- package/src/project-config.ts +219 -0
- package/src/prompt-files.ts +26 -0
- package/src/review/diagram-nodes.ts +227 -0
- package/src/review/doctor.ts +139 -0
- package/src/review/glob.ts +33 -0
- package/src/review/install-skill.ts +107 -0
- package/src/review/normalize.ts +209 -0
- package/src/review/prepare.ts +165 -0
- package/src/review/prompt.ts +233 -0
- package/src/review/publish.ts +209 -0
- package/src/review/skill-command.ts +4 -0
- package/src/review/test-paths.ts +32 -0
- package/src/review/text-length.ts +15 -0
- package/src/review/trim-caps.ts +114 -0
- package/src/review/validate-folds.ts +110 -0
- package/src/review/validate.ts +520 -0
- package/src/server/app.ts +46 -0
- package/src/server/bundle.ts +266 -0
- package/src/server/capped-body.ts +62 -0
- package/src/server/context.ts +174 -0
- package/src/server/env.ts +7 -0
- package/src/server/errors.ts +65 -0
- package/src/server/html.ts +140 -0
- package/src/server/node-server.ts +42 -0
- package/src/server/routes/api.ts +256 -0
- package/src/server/routes/chat-routes.ts +221 -0
- package/src/server/routes/pages.ts +64 -0
- package/src/server/routes/review-routes.ts +245 -0
- package/src/server/routes/static.ts +114 -0
- package/src/server/security.ts +104 -0
- package/src/server/sse.ts +67 -0
- package/src/store/atomic-json.ts +68 -0
- package/src/store/canvas-store.ts +120 -0
- package/src/store/data-dir.ts +29 -0
- package/src/store/derived-store.ts +93 -0
- package/src/store/pr-store.ts +69 -0
- package/src/store/settings-store.ts +152 -0
- package/src/store/state-store.ts +121 -0
- package/static/js/anchors.js +141 -0
- package/static/js/api.js +542 -0
- package/static/js/app.js +418 -0
- package/static/js/ask.js +35 -0
- package/static/js/chat-context.js +137 -0
- package/static/js/chat-scroll.js +114 -0
- package/static/js/chat.js +843 -0
- package/static/js/code-folds.js +200 -0
- package/static/js/commands.js +110 -0
- package/static/js/comment-link.js +37 -0
- package/static/js/composer.js +241 -0
- package/static/js/contract-types.d.ts +59 -0
- package/static/js/deep-link.js +160 -0
- package/static/js/diagram.js +582 -0
- package/static/js/diff-decorations.js +204 -0
- package/static/js/diff-renderer.js +860 -0
- package/static/js/dom.js +145 -0
- package/static/js/download.js +52 -0
- package/static/js/empty-state.js +161 -0
- package/static/js/errors.js +135 -0
- package/static/js/fences.js +90 -0
- package/static/js/header.js +134 -0
- package/static/js/hunks.js +62 -0
- package/static/js/import-zone.js +95 -0
- package/static/js/interactions.js +952 -0
- package/static/js/keyboard.js +131 -0
- package/static/js/keys.js +97 -0
- package/static/js/lang.js +54 -0
- package/static/js/layers.js +596 -0
- package/static/js/links.js +150 -0
- package/static/js/markdown.js +232 -0
- package/static/js/mermaid-fences.js +55 -0
- package/static/js/nav.js +91 -0
- package/static/js/overview.js +85 -0
- package/static/js/points.js +247 -0
- package/static/js/progress.js +49 -0
- package/static/js/proposed-comment.js +133 -0
- package/static/js/quick-questions.js +216 -0
- package/static/js/regenerate.js +69 -0
- package/static/js/review-session.js +257 -0
- package/static/js/scroll-spy.js +66 -0
- package/static/js/selection.js +193 -0
- package/static/js/settings.js +206 -0
- package/static/js/signoff.js +171 -0
- package/static/js/skin.js +56 -0
- package/static/js/store.js +35 -0
- package/static/js/theme.js +56 -0
- package/static/js/threads.js +78 -0
- package/static/js/vendor.d.ts +15 -0
- package/static/styles/base.css +223 -0
- package/static/styles/chat-tools.css +130 -0
- package/static/styles/chat.css +140 -0
- package/static/styles/commands.css +156 -0
- package/static/styles/diff.css +258 -0
- package/static/styles/header.css +114 -0
- package/static/styles/layout.css +123 -0
- package/static/styles/panels.css +152 -0
- package/static/styles/responsive.css +80 -0
- package/static/styles/review-actions.css +124 -0
- package/static/styles/review.css +473 -0
- package/static/styles/skin-github.css +356 -0
- package/static/styles.css +14 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// The key map of the review page. `keyAction` is the whole decision: which key does what, when
|
|
3
|
+
// the page is listening at all, and how the two-key `g o` sequence works.
|
|
4
|
+
import { esc } from './dom.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {'next-layer' | 'prev-layer' | 'next-file' | 'prev-file' | 'next-point' | 'prev-point'
|
|
8
|
+
* | 'toggle' | 'reviewed-file' | 'reviewed-layer' | 'comment' | 'dismiss' | 'overview'
|
|
9
|
+
* | 'help' | 'escape' | 'ask' | 'focus-chat'} KeyAction
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const KEY_HELP = [
|
|
13
|
+
{ keys: 'j / k', what: 'next / previous layer' },
|
|
14
|
+
{ keys: 'n / p', what: 'next / previous file' },
|
|
15
|
+
{ keys: '] / [', what: 'next / previous attention point' },
|
|
16
|
+
{ keys: 'o', what: 'open or collapse the card in focus' },
|
|
17
|
+
{ keys: 'r', what: 'mark the file in focus reviewed' },
|
|
18
|
+
{ keys: 'R', what: 'mark the layer in focus reviewed and move on' },
|
|
19
|
+
{ keys: 'c', what: 'comment on the line in focus or on the selection' },
|
|
20
|
+
{ keys: 'd', what: 'dismiss the attention point in focus' },
|
|
21
|
+
{ keys: 'g o', what: 'go to the overview' },
|
|
22
|
+
{ keys: '?', what: 'this help' },
|
|
23
|
+
{ keys: 'Esc', what: 'clear the selection, close a composer or a dialog' },
|
|
24
|
+
{ keys: 'a', what: 'ask AI Chat about the card, point, or selection in focus' },
|
|
25
|
+
{ keys: '/', what: 'put the cursor in the AI Chat box' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True while the reader is typing, when no key of this map should fire.
|
|
30
|
+
* @param {EventTarget | null} target
|
|
31
|
+
*/
|
|
32
|
+
export function isTypingTarget(target) {
|
|
33
|
+
if (!(target instanceof Element)) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
const tag = target.tagName.toLowerCase()
|
|
37
|
+
return (
|
|
38
|
+
tag === 'input' || tag === 'textarea' || tag === 'select' || target.closest('[contenteditable="true"]') !== null
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {KeyboardEvent} event
|
|
44
|
+
* @param {{ pendingG?: boolean }} [opts]
|
|
45
|
+
* @returns {{ action: KeyAction | null, pendingG: boolean }}
|
|
46
|
+
*/
|
|
47
|
+
export function keyAction(event, opts = {}) {
|
|
48
|
+
const none = { action: null, pendingG: false }
|
|
49
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
50
|
+
return none
|
|
51
|
+
}
|
|
52
|
+
if (event.key === 'Escape') {
|
|
53
|
+
return { action: 'escape', pendingG: false }
|
|
54
|
+
}
|
|
55
|
+
if (isTypingTarget(event.target)) {
|
|
56
|
+
return none
|
|
57
|
+
}
|
|
58
|
+
if (opts.pendingG === true) {
|
|
59
|
+
return event.key === 'o' ? { action: 'overview', pendingG: false } : none
|
|
60
|
+
}
|
|
61
|
+
switch (event.key) {
|
|
62
|
+
case 'j':
|
|
63
|
+
return { action: 'next-layer', pendingG: false }
|
|
64
|
+
case 'k':
|
|
65
|
+
return { action: 'prev-layer', pendingG: false }
|
|
66
|
+
case 'n':
|
|
67
|
+
return { action: 'next-file', pendingG: false }
|
|
68
|
+
case 'p':
|
|
69
|
+
return { action: 'prev-file', pendingG: false }
|
|
70
|
+
case ']':
|
|
71
|
+
return { action: 'next-point', pendingG: false }
|
|
72
|
+
case '[':
|
|
73
|
+
return { action: 'prev-point', pendingG: false }
|
|
74
|
+
case 'o':
|
|
75
|
+
return { action: 'toggle', pendingG: false }
|
|
76
|
+
case 'r':
|
|
77
|
+
return { action: 'reviewed-file', pendingG: false }
|
|
78
|
+
case 'R':
|
|
79
|
+
return { action: 'reviewed-layer', pendingG: false }
|
|
80
|
+
case 'c':
|
|
81
|
+
return { action: 'comment', pendingG: false }
|
|
82
|
+
case 'd':
|
|
83
|
+
return { action: 'dismiss', pendingG: false }
|
|
84
|
+
case 'g':
|
|
85
|
+
return { action: null, pendingG: true }
|
|
86
|
+
case '?':
|
|
87
|
+
return { action: 'help', pendingG: false }
|
|
88
|
+
case 'a':
|
|
89
|
+
return { action: 'ask', pendingG: false }
|
|
90
|
+
case '/':
|
|
91
|
+
return { action: 'focus-chat', pendingG: false }
|
|
92
|
+
default:
|
|
93
|
+
return none
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const HELP_DIALOG_ID = 'help-dialog'
|
|
98
|
+
|
|
99
|
+
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('')
|
|
101
|
+
return (
|
|
102
|
+
`<dialog id="${HELP_DIALOG_ID}" class="help" aria-labelledby="help-h"><form method="dialog">` +
|
|
103
|
+
'<h2 id="help-h">Keyboard</h2>' +
|
|
104
|
+
`<table class="keys"><tbody>${rows}</tbody></table>` +
|
|
105
|
+
'<div class="dialog-actions"><button class="cmd" type="submit" value="close">close</button></div>' +
|
|
106
|
+
'</form></dialog>'
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {HTMLElement} root
|
|
112
|
+
* @returns {HTMLDialogElement}
|
|
113
|
+
*/
|
|
114
|
+
export function openHelpDialog(root) {
|
|
115
|
+
let dialog = root.querySelector(`#${HELP_DIALOG_ID}`)
|
|
116
|
+
if (!(dialog instanceof HTMLDialogElement)) {
|
|
117
|
+
const t = document.createElement('template')
|
|
118
|
+
t.innerHTML = helpDialogHtml()
|
|
119
|
+
root.appendChild(t.content)
|
|
120
|
+
dialog = root.querySelector(`#${HELP_DIALOG_ID}`)
|
|
121
|
+
}
|
|
122
|
+
if (!(dialog instanceof HTMLDialogElement)) {
|
|
123
|
+
throw new Error('help dialog did not render')
|
|
124
|
+
}
|
|
125
|
+
if (typeof dialog.showModal === 'function' && !dialog.open) {
|
|
126
|
+
dialog.showModal()
|
|
127
|
+
} else {
|
|
128
|
+
dialog.setAttribute('open', '')
|
|
129
|
+
}
|
|
130
|
+
return dialog
|
|
131
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// File keys and DOM anchor ids. One implementation for the server (re-exported by
|
|
3
|
+
// src/contract/keys.ts) and the browser, so hunk ids and line anchors never drift.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Turns a repo path into a key safe for ids and file names: every character that is not a
|
|
7
|
+
* letter or digit becomes "_". Two paths can collide (`a-b.ts` and `a_b.ts`); the diff collector
|
|
8
|
+
* adds a numeric suffix through `uniqueKey` when that happens.
|
|
9
|
+
* @param {string} path
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeKey(path) {
|
|
13
|
+
return path.replace(/[^a-zA-Z0-9]/g, '_')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns `base` when unused, otherwise `base_2`, `base_3`, and so on.
|
|
18
|
+
* @param {string} base
|
|
19
|
+
* @param {Set<string>} used
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function uniqueKey(base, used) {
|
|
23
|
+
let key = base
|
|
24
|
+
let n = 2
|
|
25
|
+
while (used.has(key)) {
|
|
26
|
+
key = `${base}_${n}`
|
|
27
|
+
n += 1
|
|
28
|
+
}
|
|
29
|
+
used.add(key)
|
|
30
|
+
return key
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} key
|
|
35
|
+
* @param {number} n 1-based position of the hunk in the file's patch
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function hunkId(key, n) {
|
|
39
|
+
return `${key}#${n}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} id
|
|
44
|
+
* @returns {{ key: string, n: number } | null}
|
|
45
|
+
*/
|
|
46
|
+
export function parseHunkId(id) {
|
|
47
|
+
const m = /^(.+)#(\d+)$/.exec(id)
|
|
48
|
+
if (!m || m[1] === undefined || m[2] === undefined) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
return { key: m[1], n: Number(m[2]) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} key
|
|
56
|
+
* @param {'new' | 'old'} side
|
|
57
|
+
* @param {number} line
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function buildLineId(key, side, line) {
|
|
61
|
+
return `L-${key}-${side}-${line}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} id
|
|
66
|
+
* @returns {{ key: string, side: 'new' | 'old', line: number } | null}
|
|
67
|
+
*/
|
|
68
|
+
export function parseLineId(id) {
|
|
69
|
+
const m = /^L-(.+)-(new|old)-(\d+)$/.exec(id)
|
|
70
|
+
if (!m || m[1] === undefined || m[3] === undefined) {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
return { key: m[1], side: m[2] === 'old' ? 'old' : 'new', line: Number(m[3]) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {string} key */
|
|
77
|
+
export function fileAnchorId(key) {
|
|
78
|
+
return `file-${key}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} key
|
|
83
|
+
* @param {number} n
|
|
84
|
+
*/
|
|
85
|
+
export function hunkAnchorId(key, n) {
|
|
86
|
+
return `hunk-${key}-${n}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {string} layerKey */
|
|
90
|
+
export function layerAnchorId(layerKey) {
|
|
91
|
+
return `layer-${layerKey}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @param {string} pointId */
|
|
95
|
+
export function pointAnchorId(pointId) {
|
|
96
|
+
return `point-${pointId}`
|
|
97
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// highlight.js language per file extension. The server stores the result in `files[].lang`
|
|
3
|
+
// (src/git/lang.ts re-exports this) and the browser uses it to pick the grammar.
|
|
4
|
+
|
|
5
|
+
/** @type {Record<string, string>} */
|
|
6
|
+
export const LANG_BY_EXT = {
|
|
7
|
+
ts: 'typescript',
|
|
8
|
+
tsx: 'typescript',
|
|
9
|
+
mts: 'typescript',
|
|
10
|
+
cts: 'typescript',
|
|
11
|
+
js: 'javascript',
|
|
12
|
+
jsx: 'javascript',
|
|
13
|
+
mjs: 'javascript',
|
|
14
|
+
cjs: 'javascript',
|
|
15
|
+
json: 'json',
|
|
16
|
+
jsonc: 'json',
|
|
17
|
+
css: 'css',
|
|
18
|
+
scss: 'scss',
|
|
19
|
+
less: 'less',
|
|
20
|
+
md: 'markdown',
|
|
21
|
+
mdx: 'markdown',
|
|
22
|
+
yml: 'yaml',
|
|
23
|
+
yaml: 'yaml',
|
|
24
|
+
sh: 'bash',
|
|
25
|
+
bash: 'bash',
|
|
26
|
+
zsh: 'bash',
|
|
27
|
+
html: 'xml',
|
|
28
|
+
xml: 'xml',
|
|
29
|
+
svg: 'xml',
|
|
30
|
+
py: 'python',
|
|
31
|
+
rb: 'ruby',
|
|
32
|
+
go: 'go',
|
|
33
|
+
rs: 'rust',
|
|
34
|
+
java: 'java',
|
|
35
|
+
kt: 'kotlin',
|
|
36
|
+
sql: 'sql',
|
|
37
|
+
toml: 'ini',
|
|
38
|
+
ini: 'ini',
|
|
39
|
+
graphql: 'graphql',
|
|
40
|
+
gql: 'graphql',
|
|
41
|
+
prisma: 'prisma',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} path
|
|
46
|
+
* @returns {string | undefined}
|
|
47
|
+
*/
|
|
48
|
+
export function langForPath(path) {
|
|
49
|
+
const m = /\.([a-zA-Z0-9]+)$/.exec(path)
|
|
50
|
+
if (!m || m[1] === undefined) {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
return LANG_BY_EXT[m[1].toLowerCase()]
|
|
54
|
+
}
|