@vintasoftware/pr-review-canvas 0.3.0 → 0.4.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 +52 -23
- package/docs/reference.md +154 -44
- package/package.json +9 -4
- package/pr-review.config.example.yml +6 -0
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +3 -0
- package/skills/pr-review-canvas/SKILL.md +69 -39
- 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 +40 -39
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +12 -7
- package/src/commands.ts +45 -16
- package/src/config.ts +14 -26
- package/src/contract/api.ts +32 -1
- package/src/contract/canvas-manifest.ts +2 -0
- package/src/contract/comments.ts +5 -0
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +24 -1
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +17 -0
- package/src/contract/settings.ts +2 -0
- package/src/contract/state.ts +41 -21
- 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 +4 -19
- 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 +54 -0
- package/src/gitlab/project.ts +13 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +245 -0
- package/src/host/host.ts +136 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +13 -0
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +22 -12
- package/src/review/prepare.ts +64 -10
- package/src/review/publish.ts +61 -10
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/validate-folds.ts +2 -2
- package/src/review/validate.ts +4 -4
- package/src/server/bundle.ts +312 -111
- package/src/server/context.ts +10 -8
- package/src/server/errors.ts +30 -8
- package/src/server/html.ts +34 -10
- package/src/server/routes/api.ts +63 -26
- package/src/server/routes/chat-routes.ts +76 -38
- package/src/server/routes/pages.ts +22 -8
- package/src/server/routes/review-routes.ts +84 -33
- package/src/store/canvas-store.ts +90 -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/state-store.ts +40 -36
- package/static/js/api.js +32 -24
- package/static/js/app.js +24 -10
- package/static/js/chat.js +3 -2
- package/static/js/composer.js +24 -14
- package/static/js/contract-types.d.ts +3 -0
- package/static/js/diff-renderer.js +1 -1
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +85 -18
- package/static/js/errors.js +22 -6
- package/static/js/header.js +31 -8
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +1 -2
- package/static/js/layers.js +3 -3
- package/static/js/links.js +3 -3
- package/static/js/markdown.js +28 -1
- package/static/js/points.js +2 -1
- package/static/js/review-session.js +7 -2
- package/static/js/settings.js +2 -1
- package/static/js/signoff.js +6 -7
- package/static/styles/commands.css +6 -0
- package/static/styles/panels.css +4 -0
- package/static/styles/review-actions.css +1 -0
- package/static/styles/skin-github.css +7 -1
- package/src/github/gh.ts +0 -211
package/src/store/state-store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import type { ReviewKey } from '../contract/review-key.js'
|
|
2
3
|
import { emptyState, type PrState, PrStateSchema } from '../contract/state.js'
|
|
3
4
|
import { readJsonOrDefault, writeJsonAtomic } from './atomic-json.js'
|
|
4
5
|
import type { PrStore } from './pr-store.js'
|
|
@@ -9,23 +10,24 @@ export interface PostedEntry {
|
|
|
9
10
|
pointFingerprint?: string
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
/** Per-
|
|
13
|
+
/** Per-target local state: reviewed cards, dismissed points, hidden threads, posted comments. */
|
|
13
14
|
export interface StateStore {
|
|
14
15
|
/** Defaults when the file is missing or does not match the schema (an older tool version wrote it). */
|
|
15
|
-
read(
|
|
16
|
+
read(key: ReviewKey): Promise<PrState>
|
|
16
17
|
/**
|
|
17
|
-
* Read, change, write. Calls for the same
|
|
18
|
+
* Read, change, write. Calls for the same target run one after another, so two requests that
|
|
18
19
|
* arrive together both land instead of one overwriting the other.
|
|
19
20
|
*/
|
|
20
|
-
update(
|
|
21
|
+
update(key: ReviewKey, mutate: (state: PrState) => PrState): Promise<PrState>
|
|
21
22
|
/**
|
|
22
|
-
* Marks one layer or file. `
|
|
23
|
-
* the one the marks describe, the old marks are dropped, because they were about
|
|
23
|
+
* Marks one layer or file. `canvasSha` is the commit of the canvas the page was showing: when it
|
|
24
|
+
* differs from the one the marks describe, the old marks are dropped, because they were about
|
|
25
|
+
* other code.
|
|
24
26
|
*/
|
|
25
|
-
setReviewed(
|
|
26
|
-
setDismissed(
|
|
27
|
-
setThreadHidden(
|
|
28
|
-
addPosted(
|
|
27
|
+
setReviewed(key: ReviewKey, id: string, reviewed: boolean, canvasSha?: string): Promise<PrState>
|
|
28
|
+
setDismissed(key: ReviewKey, fingerprint: string, dismissed: boolean, reason?: string): Promise<PrState>
|
|
29
|
+
setThreadHidden(key: ReviewKey, rootCommentId: number, hidden: boolean): Promise<PrState>
|
|
30
|
+
addPosted(key: ReviewKey, entry: PostedEntry): Promise<PrState>
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** `layer:<id>` or `layer:<id>/file:<key>`, with the ids and keys the artifact uses. */
|
|
@@ -42,25 +44,25 @@ function layerOf(id: string): string {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export function createStateStore(prs: PrStore, now: () => Date): StateStore {
|
|
45
|
-
const file = (
|
|
46
|
-
const read = (
|
|
47
|
-
readJsonOrDefault(file(
|
|
47
|
+
const file = (key: ReviewKey): string => path.join(prs.prDir(key), 'state.json')
|
|
48
|
+
const read = (key: ReviewKey): Promise<PrState> =>
|
|
49
|
+
readJsonOrDefault(file(key), PrStateSchema, () => emptyState(now().toISOString()))
|
|
48
50
|
|
|
49
|
-
/** One chain per
|
|
50
|
-
const chains = new Map<
|
|
51
|
+
/** One chain per target; each update waits for the one before it. */
|
|
52
|
+
const chains = new Map<ReviewKey, Promise<unknown>>()
|
|
51
53
|
|
|
52
|
-
const update = (
|
|
54
|
+
const update = (key: ReviewKey, mutate: (state: PrState) => PrState): Promise<PrState> => {
|
|
53
55
|
const run = async (): Promise<PrState> => {
|
|
54
|
-
const current = await read(
|
|
56
|
+
const current = await read(key)
|
|
55
57
|
// Every write counts up, so the page can tell which of two answers was written later.
|
|
56
58
|
const next = { ...mutate(current), rev: (current.rev ?? 0) + 1, updatedAt: now().toISOString() }
|
|
57
|
-
await writeJsonAtomic(file(
|
|
59
|
+
await writeJsonAtomic(file(key), next)
|
|
58
60
|
return next
|
|
59
61
|
}
|
|
60
|
-
const chained = (chains.get(
|
|
62
|
+
const chained = (chains.get(key) ?? Promise.resolve()).then(run, run)
|
|
61
63
|
// The chain only orders the calls, so a failed update never blocks the next one.
|
|
62
64
|
chains.set(
|
|
63
|
-
|
|
65
|
+
key,
|
|
64
66
|
chained.catch(() => undefined)
|
|
65
67
|
)
|
|
66
68
|
return chained
|
|
@@ -69,28 +71,30 @@ export function createStateStore(prs: PrStore, now: () => Date): StateStore {
|
|
|
69
71
|
return {
|
|
70
72
|
read,
|
|
71
73
|
update,
|
|
72
|
-
setReviewed: (
|
|
73
|
-
update(
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
setReviewed: (key, id, reviewed, canvasSha) =>
|
|
75
|
+
update(key, state => {
|
|
76
|
+
const sameCanvas =
|
|
77
|
+
canvasSha === undefined ||
|
|
78
|
+
state.reviewedCanvasSha === undefined ||
|
|
79
|
+
state.reviewedCanvasSha === canvasSha
|
|
80
|
+
const next = sameCanvas ? { ...state.reviewed } : {}
|
|
77
81
|
if (reviewed) {
|
|
78
82
|
next[id] = true
|
|
79
83
|
} else {
|
|
80
|
-
for (const
|
|
84
|
+
for (const marked of Object.keys(next)) {
|
|
81
85
|
// Reopening a layer reopens its files, and reopening a file reopens its layer:
|
|
82
86
|
// both readings of "reviewed" have to agree.
|
|
83
|
-
if (
|
|
84
|
-
delete next[
|
|
87
|
+
if (marked === id || marked.startsWith(`${id}/`) || marked === layerOf(id)) {
|
|
88
|
+
delete next[marked]
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
|
-
return
|
|
92
|
+
return canvasSha === undefined
|
|
89
93
|
? { ...state, reviewed: next }
|
|
90
|
-
: { ...state, reviewed: next,
|
|
94
|
+
: { ...state, reviewed: next, reviewedCanvasSha: canvasSha }
|
|
91
95
|
}),
|
|
92
|
-
setDismissed: (
|
|
93
|
-
update(
|
|
96
|
+
setDismissed: (key, fingerprint, dismissed, reason) =>
|
|
97
|
+
update(key, state => {
|
|
94
98
|
const next = { ...state.dismissed }
|
|
95
99
|
if (dismissed) {
|
|
96
100
|
next[fingerprint] =
|
|
@@ -100,8 +104,8 @@ export function createStateStore(prs: PrStore, now: () => Date): StateStore {
|
|
|
100
104
|
}
|
|
101
105
|
return { ...state, dismissed: next }
|
|
102
106
|
}),
|
|
103
|
-
setThreadHidden: (
|
|
104
|
-
update(
|
|
107
|
+
setThreadHidden: (key, rootCommentId, hidden) =>
|
|
108
|
+
update(key, state => {
|
|
105
109
|
const next = { ...state.hiddenThreads }
|
|
106
110
|
if (hidden) {
|
|
107
111
|
next[String(rootCommentId)] = { at: now().toISOString() }
|
|
@@ -110,8 +114,8 @@ export function createStateStore(prs: PrStore, now: () => Date): StateStore {
|
|
|
110
114
|
}
|
|
111
115
|
return { ...state, hiddenThreads: next }
|
|
112
116
|
}),
|
|
113
|
-
addPosted: (
|
|
114
|
-
update(
|
|
117
|
+
addPosted: (key, entry) =>
|
|
118
|
+
update(key, state => {
|
|
115
119
|
if (state.posted.some(p => p.commentId === entry.commentId)) {
|
|
116
120
|
return state
|
|
117
121
|
}
|
package/static/js/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/** @typedef {import('./contract-types.js').ErrorEnvelope} ErrorEnvelope */
|
|
3
|
+
/** @typedef {import('./contract-types.js').ReviewKey} ReviewKey */
|
|
3
4
|
/** @typedef {import('./contract-types.js').PrBundle} PrBundle */
|
|
4
5
|
/** @typedef {import('./contract-types.js').PatchesResponse} PatchesResponse */
|
|
5
6
|
|
|
@@ -69,17 +70,20 @@ export async function fetchJson(url, opts = {}) {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
73
|
+
* The bundle for one target. `refresh` asks the server to read the forge, or the working tree,
|
|
74
|
+
* again; `poll` says this is the background poller, which is answered from the head the page was
|
|
75
|
+
* opened with rather than by snapshotting the working tree every few seconds.
|
|
76
|
+
* @param {ReviewKey} prNumber
|
|
77
|
+
* @param {{ refresh?: boolean, poll?: boolean, fetchImpl?: typeof fetch | undefined }} [opts]
|
|
74
78
|
* @returns {Promise<PrBundle>}
|
|
75
79
|
*/
|
|
76
80
|
export function fetchBundle(prNumber, opts = {}) {
|
|
77
|
-
const q = opts.refresh ? '?refresh=1' : ''
|
|
81
|
+
const q = opts.refresh ? '?refresh=1' : opts.poll ? '?poll=1' : ''
|
|
78
82
|
return fetchJson(`/api/prs/${prNumber}${q}`, { fetchImpl: opts.fetchImpl })
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
|
-
* @param {
|
|
86
|
+
* @param {ReviewKey} prNumber
|
|
83
87
|
* @param {{ headSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
84
88
|
* @returns {Promise<PatchesResponse>}
|
|
85
89
|
*/
|
|
@@ -89,7 +93,7 @@ export function fetchPatches(prNumber, opts = {}) {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
|
-
* @param {
|
|
96
|
+
* @param {ReviewKey} prNumber
|
|
93
97
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
94
98
|
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
95
99
|
*/
|
|
@@ -98,14 +102,18 @@ export function fetchState(prNumber, opts = {}) {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/**
|
|
101
|
-
* @param {
|
|
105
|
+
* @param {ReviewKey} prNumber
|
|
102
106
|
* @param {string} id `layer:<id>` or `layer:<id>/file:<key>`
|
|
103
107
|
* @param {boolean} reviewed
|
|
104
|
-
* @param {{ headSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
108
|
+
* @param {{ headSha?: string, canvasSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
105
109
|
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
106
110
|
*/
|
|
107
111
|
export function putReviewed(prNumber, id, reviewed, opts = {}) {
|
|
108
|
-
const body =
|
|
112
|
+
const body = {
|
|
113
|
+
reviewed,
|
|
114
|
+
...(opts.headSha === undefined ? {} : { headSha: opts.headSha }),
|
|
115
|
+
...(opts.canvasSha === undefined ? {} : { canvasSha: opts.canvasSha }),
|
|
116
|
+
}
|
|
109
117
|
return fetchJson(`/api/prs/${prNumber}/reviewed/${id}`, {
|
|
110
118
|
method: 'PUT',
|
|
111
119
|
body,
|
|
@@ -114,7 +122,7 @@ export function putReviewed(prNumber, id, reviewed, opts = {}) {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
|
-
* @param {
|
|
125
|
+
* @param {ReviewKey} prNumber
|
|
118
126
|
* @param {string} fingerprint
|
|
119
127
|
* @param {boolean} dismissed
|
|
120
128
|
* @param {{ reason?: string, fetchImpl?: typeof fetch }} [opts]
|
|
@@ -130,7 +138,7 @@ export function putDismissed(prNumber, fingerprint, dismissed, opts = {}) {
|
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/**
|
|
133
|
-
* @param {
|
|
141
|
+
* @param {ReviewKey} prNumber
|
|
134
142
|
* @param {number} rootCommentId
|
|
135
143
|
* @param {boolean} hidden
|
|
136
144
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
@@ -145,7 +153,7 @@ export function putThreadHidden(prNumber, rootCommentId, hidden, opts = {}) {
|
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
/**
|
|
148
|
-
* @param {
|
|
156
|
+
* @param {ReviewKey} prNumber
|
|
149
157
|
* @param {{ refresh?: boolean, fetchImpl?: typeof fetch }} [opts]
|
|
150
158
|
* @returns {Promise<import('./contract-types.js').Capabilities>}
|
|
151
159
|
*/
|
|
@@ -155,7 +163,7 @@ export function fetchCapabilities(prNumber, opts = {}) {
|
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
/**
|
|
158
|
-
* @param {
|
|
166
|
+
* @param {ReviewKey} prNumber
|
|
159
167
|
* @param {import('./contract-types.js').PostCommentInput} input
|
|
160
168
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
161
169
|
* @returns {Promise<import('./contract-types.js').PostCommentResponse>}
|
|
@@ -169,7 +177,7 @@ export function postComment(prNumber, input, opts = {}) {
|
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
/**
|
|
172
|
-
* @param {
|
|
180
|
+
* @param {ReviewKey} prNumber
|
|
173
181
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
174
182
|
* @returns {Promise<import('./contract-types.js').ReviewBodyResponse>}
|
|
175
183
|
*/
|
|
@@ -178,7 +186,7 @@ export function fetchReviewBody(prNumber, opts = {}) {
|
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
/**
|
|
181
|
-
* @param {
|
|
189
|
+
* @param {ReviewKey} prNumber
|
|
182
190
|
* @param {{ event: 'APPROVE' | 'REQUEST_CHANGES', body?: string, headSha?: string }} input
|
|
183
191
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
184
192
|
* @returns {Promise<import('./contract-types.js').PostReviewResponse>}
|
|
@@ -210,7 +218,7 @@ export const POLL_SLOW_INTERVAL_MS = 15_000
|
|
|
210
218
|
* After five minutes of waiting the gap grows to 15 s, because a generation that has not finished
|
|
211
219
|
* by then takes minutes more. A failed poll is reported and polling goes on; the page never
|
|
212
220
|
* blocks on it.
|
|
213
|
-
* @param {
|
|
221
|
+
* @param {ReviewKey} prNumber
|
|
214
222
|
* @param {PollOptions} [opts]
|
|
215
223
|
* @returns {{ stop: () => void }}
|
|
216
224
|
*/
|
|
@@ -230,7 +238,7 @@ export function pollBundle(prNumber, opts = {}) {
|
|
|
230
238
|
/** @type {PrBundle} */
|
|
231
239
|
let bundle
|
|
232
240
|
try {
|
|
233
|
-
bundle = await fetchBundle(prNumber, { fetchImpl: opts.fetchImpl })
|
|
241
|
+
bundle = await fetchBundle(prNumber, { poll: true, fetchImpl: opts.fetchImpl })
|
|
234
242
|
} catch (err) {
|
|
235
243
|
if (!stopped) {
|
|
236
244
|
opts.onError?.(err)
|
|
@@ -306,7 +314,7 @@ export function uploadForm(url, form, opts = {}) {
|
|
|
306
314
|
}
|
|
307
315
|
|
|
308
316
|
/**
|
|
309
|
-
* @param {
|
|
317
|
+
* @param {ReviewKey} prNumber
|
|
310
318
|
* @param {File} file
|
|
311
319
|
* @param {{ force?: boolean, onProgress?: (fraction: number) => void, xhrImpl?: () => XMLHttpRequest }} [opts]
|
|
312
320
|
* @returns {Promise<import('./contract-types.js').ImportResult>}
|
|
@@ -321,7 +329,7 @@ export function importCanvas(prNumber, file, opts = {}) {
|
|
|
321
329
|
}
|
|
322
330
|
|
|
323
331
|
/**
|
|
324
|
-
* @param {
|
|
332
|
+
* @param {ReviewKey} prNumber
|
|
325
333
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
326
334
|
* @returns {Promise<import('./contract-types.js').SharedCanvasFetchResponse>}
|
|
327
335
|
*/
|
|
@@ -331,7 +339,7 @@ export function fetchSharedCanvas(prNumber, opts = {}) {
|
|
|
331
339
|
|
|
332
340
|
/**
|
|
333
341
|
* The zip as a blob plus the name the server chose, ready to hand to the browser.
|
|
334
|
-
* @param {
|
|
342
|
+
* @param {ReviewKey} prNumber
|
|
335
343
|
* @param {{ headSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
336
344
|
* @returns {Promise<{ blob: Blob, filename: string }>}
|
|
337
345
|
*/
|
|
@@ -378,7 +386,7 @@ export function saveAppearance(input, opts = {}) {
|
|
|
378
386
|
/* ---- AI Chat and its settings ---- */
|
|
379
387
|
|
|
380
388
|
/**
|
|
381
|
-
* @param {
|
|
389
|
+
* @param {ReviewKey} prNumber
|
|
382
390
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
383
391
|
* @returns {Promise<import('./contract-types.js').ChatThreadsResponse>}
|
|
384
392
|
*/
|
|
@@ -387,7 +395,7 @@ export function fetchThreads(prNumber, opts = {}) {
|
|
|
387
395
|
}
|
|
388
396
|
|
|
389
397
|
/**
|
|
390
|
-
* @param {
|
|
398
|
+
* @param {ReviewKey} prNumber
|
|
391
399
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
392
400
|
* @returns {Promise<import('./contract-types.js').ChatThreadsResponse>}
|
|
393
401
|
*/
|
|
@@ -400,7 +408,7 @@ export function createThread(prNumber, opts = {}) {
|
|
|
400
408
|
}
|
|
401
409
|
|
|
402
410
|
/**
|
|
403
|
-
* @param {
|
|
411
|
+
* @param {ReviewKey} prNumber
|
|
404
412
|
* @param {string} name
|
|
405
413
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
406
414
|
* @returns {Promise<import('./contract-types.js').ChatHistoryResponse>}
|
|
@@ -412,7 +420,7 @@ export function fetchThreadHistory(prNumber, name, opts = {}) {
|
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
/**
|
|
415
|
-
* @param {
|
|
423
|
+
* @param {ReviewKey} prNumber
|
|
416
424
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
417
425
|
* @returns {Promise<{ cancelled: boolean }>}
|
|
418
426
|
*/
|
|
@@ -501,7 +509,7 @@ export function readSseFrames(buffer, chunk) {
|
|
|
501
509
|
/**
|
|
502
510
|
* Sends one chat message and calls `onEvent` for every frame until the turn ends. Rejects with
|
|
503
511
|
* an ApiError when the server refuses the message (a busy chat, a context it cannot resolve).
|
|
504
|
-
* @param {
|
|
512
|
+
* @param {ReviewKey} prNumber
|
|
505
513
|
* @param {{ message: string, context: import('./chat-context.js').ChatContext, thread?: string }} input
|
|
506
514
|
* @param {{
|
|
507
515
|
* onEvent: (event: { event: string, data: unknown }) => void,
|
package/static/js/app.js
CHANGED
|
@@ -19,7 +19,7 @@ import { initDeepLinks } from './deep-link.js'
|
|
|
19
19
|
import { initDiagrams } from './diagram.js'
|
|
20
20
|
import { esc, qs } from './dom.js'
|
|
21
21
|
import { exportCanvasZip } from './download.js'
|
|
22
|
-
import { renderEmptyState, renderStaleState, staleBarHtml } from './empty-state.js'
|
|
22
|
+
import { carriedOverBarHtml, renderEmptyState, renderStaleState, staleBarHtml } from './empty-state.js'
|
|
23
23
|
import { errorCardHtml } from './errors.js'
|
|
24
24
|
import { renderHeader } from './header.js'
|
|
25
25
|
import { wireDropZone } from './import-zone.js'
|
|
@@ -33,8 +33,10 @@ import { initScrollSpy } from './scroll-spy.js'
|
|
|
33
33
|
import { openSettingsDialog } from './settings.js'
|
|
34
34
|
import { applySkin, nextSkin, readSkin, skinLabel } from './skin.js'
|
|
35
35
|
import { applyTheme, nextTheme, readTheme, themeLabel } from './theme.js'
|
|
36
|
+
import { hostLabel, setHost } from './host.js'
|
|
36
37
|
|
|
37
|
-
/** @typedef {
|
|
38
|
+
/** @typedef {import('./contract-types.js').ReviewKey} ReviewKey */
|
|
39
|
+
/** @typedef {{ prNumber: ReviewKey, owner: string, repo: string, version: string, host: import('./contract-types.js').PublicHost }} Bootstrap */
|
|
38
40
|
|
|
39
41
|
/** @returns {Bootstrap | null} */
|
|
40
42
|
function readBootstrap() {
|
|
@@ -57,7 +59,7 @@ function footerHtml(version, bundle) {
|
|
|
57
59
|
const canvas = bundle?.canvas
|
|
58
60
|
? `<span>canvas <span class="mono">${esc(bundle.canvas.headSha.slice(0, 7))}</span> · ${esc(bundle.canvas.source)}</span>`
|
|
59
61
|
: ''
|
|
60
|
-
return `<footer><span>pr-review ${esc(version)}</span>${canvas}<span>localhost only · nothing leaves this machine except
|
|
62
|
+
return `<footer><span>pr-review ${esc(version)}</span>${canvas}<span>localhost only · nothing leaves this machine except ${esc(hostLabel())} posts you confirm</span></footer>`
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** @param {ReadonlyArray<string>} warnings */
|
|
@@ -83,7 +85,7 @@ function toEnvelopeError(err) {
|
|
|
83
85
|
/**
|
|
84
86
|
* The patches for the bundle. The first visit builds derived/ while the bundle resolves, so the
|
|
85
87
|
* parallel fetch can land before the patches exist; one more try after the bundle is enough.
|
|
86
|
-
* @param {
|
|
88
|
+
* @param {ReviewKey} prNumber
|
|
87
89
|
* @param {Promise<Record<string, string> | null>} started
|
|
88
90
|
* @param {string} [headSha] the canvas commit, when it is not the PR head
|
|
89
91
|
*/
|
|
@@ -155,6 +157,7 @@ export class PrAppElement extends HTMLElement {
|
|
|
155
157
|
this.innerHTML = errorCardHtml({ code: 'INTERNAL', message: 'missing bootstrap data' })
|
|
156
158
|
return
|
|
157
159
|
}
|
|
160
|
+
setHost(this.bootstrap.host)
|
|
158
161
|
this.theme = readTheme(document.documentElement)
|
|
159
162
|
this.skin = readSkin(document.documentElement)
|
|
160
163
|
const patchesPromise = fetchPatches(this.bootstrap.prNumber).then(
|
|
@@ -224,7 +227,12 @@ export class PrAppElement extends HTMLElement {
|
|
|
224
227
|
now,
|
|
225
228
|
})
|
|
226
229
|
defineLayerElements()
|
|
227
|
-
const staleBar =
|
|
230
|
+
const staleBar =
|
|
231
|
+
bundle.status === 'stale' && bundle.stale
|
|
232
|
+
? staleBarHtml(bundle.stale, bundle.local)
|
|
233
|
+
: bundle.carriedOver
|
|
234
|
+
? carriedOverBarHtml(bundle.carriedOver)
|
|
235
|
+
: ''
|
|
228
236
|
this.innerHTML =
|
|
229
237
|
header +
|
|
230
238
|
bannerHtml(bundle.warnings) +
|
|
@@ -249,6 +257,8 @@ export class PrAppElement extends HTMLElement {
|
|
|
249
257
|
state: bundle.state,
|
|
250
258
|
capabilities,
|
|
251
259
|
headSha: bundle.pr.headSha,
|
|
260
|
+
// The marks are keyed to the canvas on the page, so every mark names it.
|
|
261
|
+
...(bundle.canvas === undefined ? {} : { canvasSha: bundle.canvas.headSha }),
|
|
252
262
|
})
|
|
253
263
|
const interactions = wireReview(this, session, {
|
|
254
264
|
chat: () => this.chat,
|
|
@@ -371,11 +381,15 @@ export class PrAppElement extends HTMLElement {
|
|
|
371
381
|
this.viewStale = true
|
|
372
382
|
void this.render(bundle)
|
|
373
383
|
})
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
384
|
+
// Importing a canvas a teammate attached is a pull request thing; local work has no thread
|
|
385
|
+
// to attach one to, and the local screens draw no drop zone.
|
|
386
|
+
if (typeof boot.prNumber === 'number') {
|
|
387
|
+
wireDropZone(this, {
|
|
388
|
+
prNumber: boot.prNumber,
|
|
389
|
+
importImpl: importCanvas,
|
|
390
|
+
onImported: () => void this.reload(),
|
|
391
|
+
})
|
|
392
|
+
}
|
|
379
393
|
}
|
|
380
394
|
|
|
381
395
|
/**
|
package/static/js/chat.js
CHANGED
|
@@ -23,6 +23,7 @@ import { postedCommentUrl, viewCommentHtml } from './comment-link.js'
|
|
|
23
23
|
import { esc, qs } from './dom.js'
|
|
24
24
|
import { getRenderContext } from './layers.js'
|
|
25
25
|
import { renderMarkdown } from './markdown.js'
|
|
26
|
+
import { postToLabel } from './host.js'
|
|
26
27
|
import { splitChatAnswer, targetsFromFiles } from './proposed-comment.js'
|
|
27
28
|
|
|
28
29
|
export const CHAT_WIDTH_KEY = 'pr-review.chat-width'
|
|
@@ -113,7 +114,7 @@ export const QUICK_QUESTIONS = [
|
|
|
113
114
|
/**
|
|
114
115
|
* @typedef {{
|
|
115
116
|
* root: HTMLElement,
|
|
116
|
-
* prNumber:
|
|
117
|
+
* prNumber: import('./contract-types.js').ReviewKey,
|
|
117
118
|
* session: ReviewSession,
|
|
118
119
|
* storage?: Storage | null,
|
|
119
120
|
* api?: Partial<ChatApi>,
|
|
@@ -179,7 +180,7 @@ export function proposedCommentHtml(comment, id, postedUrl) {
|
|
|
179
180
|
`<div class="prose">${renderMarkdown(comment.body)}</div>` +
|
|
180
181
|
'<span class="tbtns">' +
|
|
181
182
|
(postedUrl === undefined
|
|
182
|
-
? `<button class="cmd fill" type="button" data-act="proposed-post" data-proposed="${esc(id)}" data-needs-post
|
|
183
|
+
? `<button class="cmd fill" type="button" data-act="proposed-post" data-proposed="${esc(id)}" data-needs-post>${postToLabel()}</button>`
|
|
183
184
|
: viewCommentHtml(postedUrl, true)) +
|
|
184
185
|
`<button class="cmd" type="button" data-act="proposed-edit" data-proposed="${esc(id)}" data-needs-post>edit</button>` +
|
|
185
186
|
`<button class="cmd" type="button" data-copy="${esc(comment.body)}">copy</button>` +
|
package/static/js/composer.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { cssEscape } from './anchors.js'
|
|
7
7
|
import { renderMarkdown } from './markdown.js'
|
|
8
8
|
import { esc } from './dom.js'
|
|
9
|
+
import { noPostingTitle, postToLabel } from './host.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {{
|
|
@@ -50,7 +51,7 @@ export function composerHtml(opts) {
|
|
|
50
51
|
`<label class="sr" for="${esc(opts.id)}-t">${esc(opts.label)}</label>` +
|
|
51
52
|
previewControlsHtml() +
|
|
52
53
|
`<textarea id="${esc(opts.id)}-t" rows="3" placeholder="${esc(opts.label)}">${esc(opts.body ?? '')}</textarea>` +
|
|
53
|
-
|
|
54
|
+
`<div class="composer-actions"><button class="cmd fill" type="button" data-act="composer-post" data-needs-post>${postToLabel()}</button>` +
|
|
54
55
|
'<button class="cmd" type="button" data-act="composer-cancel">cancel</button></div></div>'
|
|
55
56
|
)
|
|
56
57
|
}
|
|
@@ -144,12 +145,9 @@ export function closeComposers(root) {
|
|
|
144
145
|
return closed
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
/** What a command says when this GitHub login may not post. */
|
|
148
|
-
export const NO_POSTING_TITLE = 'this GitHub login cannot post on this repository'
|
|
149
|
-
|
|
150
148
|
/**
|
|
151
149
|
* Disables everything that posts when the probe said no, and enables it otherwise. A token
|
|
152
|
-
* whose rights cannot be read ('unknown') stays enabled:
|
|
150
|
+
* whose rights cannot be read ('unknown') stays enabled: the host answers for itself.
|
|
153
151
|
*
|
|
154
152
|
* A command can be disabled for a reason of its own (the approve command before every layer is
|
|
155
153
|
* read, a command whose request is still running). Those keep their state: this only adds and
|
|
@@ -159,7 +157,7 @@ export const NO_POSTING_TITLE = 'this GitHub login cannot post on this repositor
|
|
|
159
157
|
*/
|
|
160
158
|
export function applyCapabilityGating(root, capabilities) {
|
|
161
159
|
const blocked = capabilities.canComment === false
|
|
162
|
-
const reason = capabilities.reason ??
|
|
160
|
+
const reason = capabilities.reason ?? noPostingTitle()
|
|
163
161
|
for (const el of Array.from(root.querySelectorAll('[data-needs-post]'))) {
|
|
164
162
|
if (!(el instanceof HTMLButtonElement) || el.getAttribute('aria-busy') === 'true') {
|
|
165
163
|
continue
|
|
@@ -222,15 +220,19 @@ export function setDisabledReason(el, reason) {
|
|
|
222
220
|
}
|
|
223
221
|
}
|
|
224
222
|
|
|
223
|
+
/** One command that swaps the box between writing and previewing; its label is the mode it goes to. */
|
|
225
224
|
export function previewControlsHtml() {
|
|
226
|
-
return '<div class="preview-controls"
|
|
225
|
+
return '<div class="preview-controls"><button class="cmd" type="button" data-act="markdown-toggle">preview</button></div><div class="markdown-preview prose" hidden></div>'
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Puts one editor into write or preview mode.
|
|
230
|
+
* @param {Element} host the `.composer-box` or `.signoff-dialog` that holds the editor
|
|
231
|
+
* @param {boolean} preview
|
|
232
|
+
*/
|
|
233
|
+
export function setMarkdownPreview(host, preview) {
|
|
234
|
+
const textarea = host.querySelector('textarea')
|
|
235
|
+
const output = host.querySelector('.markdown-preview')
|
|
234
236
|
if (!(textarea instanceof HTMLTextAreaElement) || !(output instanceof HTMLElement)) return
|
|
235
237
|
if (preview)
|
|
236
238
|
output.innerHTML = textarea.value.trim()
|
|
@@ -238,7 +240,15 @@ export function toggleMarkdownPreview(button, preview) {
|
|
|
238
240
|
: '<p class="muted">Nothing to preview.</p>'
|
|
239
241
|
textarea.hidden = preview
|
|
240
242
|
output.hidden = !preview
|
|
241
|
-
host
|
|
242
|
-
|
|
243
|
+
const toggle = host.querySelector('[data-act="markdown-toggle"]')
|
|
244
|
+
if (toggle !== null) toggle.textContent = preview ? 'write' : 'preview'
|
|
243
245
|
if (!preview) textarea.focus()
|
|
244
246
|
}
|
|
247
|
+
|
|
248
|
+
/** @param {HTMLElement} button */
|
|
249
|
+
export function toggleMarkdownPreview(button) {
|
|
250
|
+
const host = button.closest('.composer-box, .signoff-dialog')
|
|
251
|
+
if (host === null) return
|
|
252
|
+
const output = host.querySelector('.markdown-preview')
|
|
253
|
+
setMarkdownPreview(host, output instanceof HTMLElement && output.hidden)
|
|
254
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Type-only bridge so the browser modules (JSDoc) see the same contract as the server.
|
|
2
2
|
|
|
3
3
|
export type {
|
|
4
|
+
CarriedOverInfo,
|
|
4
5
|
CanvasRelation,
|
|
5
6
|
Capabilities,
|
|
6
7
|
ChatStatus,
|
|
@@ -10,6 +11,7 @@ export type {
|
|
|
10
11
|
PostCommentResponse,
|
|
11
12
|
PostReviewResponse,
|
|
12
13
|
PrBundle,
|
|
14
|
+
PublicHost,
|
|
13
15
|
ReviewBodyResponse,
|
|
14
16
|
ReviewSummary,
|
|
15
17
|
SharedCanvasFetchResponse,
|
|
@@ -23,6 +25,7 @@ export type {
|
|
|
23
25
|
ChatThreadsResponse,
|
|
24
26
|
ChatTurn,
|
|
25
27
|
} from '../../src/contract/chat.js'
|
|
28
|
+
export type { ReviewKey } from '../../src/contract/review-key.js'
|
|
26
29
|
export type {
|
|
27
30
|
CommentsPayload,
|
|
28
31
|
IssueComment,
|
|
@@ -855,7 +855,7 @@ export function renderDiff(file, patch, opts = {}) {
|
|
|
855
855
|
highlightHunk(h, file.lang)
|
|
856
856
|
tables.push(
|
|
857
857
|
`<table class="diff" id="${esc(hunkAnchorId(file.key, n))}" data-hunk="${esc(id)}" data-key="${esc(file.key)}">` +
|
|
858
|
-
`<caption class="sr">
|
|
858
|
+
`<caption class="sr">Chunk ${n} of ${esc(file.path)}</caption>${THEAD}<tbody>${buildRows(h, file.key)}</tbody></table>`
|
|
859
859
|
)
|
|
860
860
|
})
|
|
861
861
|
if (tables.length === 0) {
|
package/static/js/download.js
CHANGED
|
@@ -26,7 +26,7 @@ export function saveBlob(blob, filename, deps = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* @param {
|
|
29
|
+
* @param {import('./contract-types.js').ReviewKey} prNumber
|
|
30
30
|
* @param {{ headSha?: string, fetchImpl?: typeof fetch } & SaveDeps} [opts]
|
|
31
31
|
* @returns {Promise<string>} the file name the browser saved
|
|
32
32
|
*/
|