@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
|
@@ -69,7 +69,11 @@ export function createDerivedStore(canvases: CanvasStore, git: Git, now: () => D
|
|
|
69
69
|
await writeTextAtomic(path.join(dir, 'patches', `${f.key}.diff`), `${labelPatch(f.key, f.patch)}\n`)
|
|
70
70
|
}
|
|
71
71
|
await materialize(git, { headSha, mergeBaseSha, files: collected, outDir: dir })
|
|
72
|
-
await writeJsonAtomic(path.join(dir, 'meta.json'), {
|
|
72
|
+
await writeJsonAtomic(path.join(dir, 'meta.json'), {
|
|
73
|
+
headSha,
|
|
74
|
+
mergeBaseSha,
|
|
75
|
+
builtAt: now().toISOString(),
|
|
76
|
+
})
|
|
73
77
|
return { files, patches }
|
|
74
78
|
},
|
|
75
79
|
read,
|
package/src/store/pr-store.ts
CHANGED
|
@@ -39,7 +39,8 @@ export function createPrStore(repoRoot: string): PrStore {
|
|
|
39
39
|
writeComments: (number, comments) => writeJsonAtomic(path.join(prDir(number), 'comments.json'), comments),
|
|
40
40
|
readDiscovery: number =>
|
|
41
41
|
readJsonOrDefault(path.join(prDir(number), 'discovery.json'), DiscoveryCacheSchema, () => null),
|
|
42
|
-
writeDiscovery: (number, discovery) =>
|
|
42
|
+
writeDiscovery: (number, discovery) =>
|
|
43
|
+
writeJsonAtomic(path.join(prDir(number), 'discovery.json'), discovery),
|
|
43
44
|
listRecent: async limit => {
|
|
44
45
|
let names: string[]
|
|
45
46
|
try {
|
package/src/store/state-store.ts
CHANGED
|
@@ -93,7 +93,8 @@ export function createStateStore(prs: PrStore, now: () => Date): StateStore {
|
|
|
93
93
|
update(number, state => {
|
|
94
94
|
const next = { ...state.dismissed }
|
|
95
95
|
if (dismissed) {
|
|
96
|
-
next[fingerprint] =
|
|
96
|
+
next[fingerprint] =
|
|
97
|
+
reason === undefined ? { at: now().toISOString() } : { at: now().toISOString(), reason }
|
|
97
98
|
} else {
|
|
98
99
|
delete next[fingerprint]
|
|
99
100
|
}
|
package/static/brand.svg
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" shape-rendering="crispEdges">
|
|
2
|
+
<title>PR review canvas</title>
|
|
3
|
+
<path fill="#7661a8" d="M12 7h31v35H12z"/>
|
|
4
|
+
<path fill="#aa96d2" d="M39 9h2v24h-2z"/>
|
|
5
|
+
<path fill="#292b3d" d="M3 3h35v41H3z"/>
|
|
6
|
+
<path fill="#fff0cf" d="M5 12h31v30H5z"/>
|
|
7
|
+
<path fill="#45465c" d="M5 5h31v5H5z"/>
|
|
8
|
+
<path fill="#ffe08a" d="M22 8h3v1h-3z"/>
|
|
9
|
+
<path fill="#ffffff" d="M27 6h3v3h-3zm1 1v1h1V7z" fill-rule="evenodd"/>
|
|
10
|
+
<path fill="#ffb3ab" d="M32 6h1v1h1V6h1v1h-1v1h1v1h-1V8h-1v1h-1V8h1V7h-1z"/>
|
|
11
|
+
<path fill="#b44a45" d="M9 17h9v3H9z"/>
|
|
12
|
+
<path fill="#e8ddc4" d="M20 15h1v19h-1z"/>
|
|
13
|
+
<path fill="#827b70" d="M23 17h10v2H23zM23 26h10v2H23zM9 36h16v2H9z"/>
|
|
14
|
+
<path fill="#437d68" d="M12 23h3v3h3v3h-3v3h-3v-3H9v-3h3z"/>
|
|
15
|
+
<g transform="scale(0.5)" shape-rendering="geometricPrecision">
|
|
16
|
+
<path fill="#292b3d" d="M56 68H58V66H60V64H62V62H68V64H70V66H72V68H76V64H78V60H80V56H82V52H84V50H90V52H92V54H94V56H96V62H94V66H92V70H90V74H88V78H86V82H84V86H82V90H80V92H72V90H70V88H68V86H66V84H64V82H62V80H60V78H58V76H56z"/>
|
|
17
|
+
<path fill="#8bc79a" d="M58 70H60V68H62V66H64V64H66V66H68V68H70V70H72V72H74V74H76V70H78V66H80V62H82V58H84V54H86V52H88V54H90V56H92V58H94V60H92V64H90V68H88V72H86V76H84V80H82V84H80V88H78V90H74V88H72V86H70V84H68V82H66V80H64V78H62V76H60V74H58z"/>
|
|
18
|
+
</g>
|
|
19
|
+
</svg>
|
package/static/js/api.js
CHANGED
|
@@ -161,7 +161,11 @@ export function fetchCapabilities(prNumber, opts = {}) {
|
|
|
161
161
|
* @returns {Promise<import('./contract-types.js').PostCommentResponse>}
|
|
162
162
|
*/
|
|
163
163
|
export function postComment(prNumber, input, opts = {}) {
|
|
164
|
-
return fetchJson(`/api/prs/${prNumber}/comments`, {
|
|
164
|
+
return fetchJson(`/api/prs/${prNumber}/comments`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
body: input,
|
|
167
|
+
fetchImpl: opts.fetchImpl,
|
|
168
|
+
})
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
/**
|
|
@@ -388,7 +392,11 @@ export function fetchThreads(prNumber, opts = {}) {
|
|
|
388
392
|
* @returns {Promise<import('./contract-types.js').ChatThreadsResponse>}
|
|
389
393
|
*/
|
|
390
394
|
export function createThread(prNumber, opts = {}) {
|
|
391
|
-
return fetchJson(`/api/prs/${prNumber}/chat/threads`, {
|
|
395
|
+
return fetchJson(`/api/prs/${prNumber}/chat/threads`, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
body: {},
|
|
398
|
+
fetchImpl: opts.fetchImpl,
|
|
399
|
+
})
|
|
392
400
|
}
|
|
393
401
|
|
|
394
402
|
/**
|
|
@@ -409,7 +417,11 @@ export function fetchThreadHistory(prNumber, name, opts = {}) {
|
|
|
409
417
|
* @returns {Promise<{ cancelled: boolean }>}
|
|
410
418
|
*/
|
|
411
419
|
export function cancelChat(prNumber, opts = {}) {
|
|
412
|
-
return fetchJson(`/api/prs/${prNumber}/chat/cancel`, {
|
|
420
|
+
return fetchJson(`/api/prs/${prNumber}/chat/cancel`, {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
body: {},
|
|
423
|
+
fetchImpl: opts.fetchImpl,
|
|
424
|
+
})
|
|
413
425
|
}
|
|
414
426
|
|
|
415
427
|
/**
|
package/static/js/app.js
CHANGED
|
@@ -62,7 +62,9 @@ function footerHtml(version, bundle) {
|
|
|
62
62
|
|
|
63
63
|
/** @param {ReadonlyArray<string>} warnings */
|
|
64
64
|
function bannerHtml(warnings) {
|
|
65
|
-
return warnings.length === 0
|
|
65
|
+
return warnings.length === 0
|
|
66
|
+
? ''
|
|
67
|
+
: `<div class="banner" role="status">${warnings.map(w => esc(w)).join(' · ')}</div>`
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
@@ -369,7 +371,11 @@ export class PrAppElement extends HTMLElement {
|
|
|
369
371
|
this.viewStale = true
|
|
370
372
|
void this.render(bundle)
|
|
371
373
|
})
|
|
372
|
-
wireDropZone(this, {
|
|
374
|
+
wireDropZone(this, {
|
|
375
|
+
prNumber: boot.prNumber,
|
|
376
|
+
importImpl: importCanvas,
|
|
377
|
+
onImported: () => void this.reload(),
|
|
378
|
+
})
|
|
373
379
|
}
|
|
374
380
|
|
|
375
381
|
/**
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** Keeps the same chat mounted while switching between the sidebar and floating panel.
|
|
4
|
+
* @param {HTMLElement} root
|
|
5
|
+
* @param {HTMLElement} pane
|
|
6
|
+
*/
|
|
7
|
+
export function wireChatPanel(root, pane) {
|
|
8
|
+
const dialog = /** @type {HTMLDialogElement} */ (root.querySelector('#chat-dialog'))
|
|
9
|
+
const launcher = /** @type {HTMLButtonElement} */ (root.querySelector('#chat-launcher'))
|
|
10
|
+
const minimize = /** @type {HTMLButtonElement} */ (root.querySelector('#chat-minimize'))
|
|
11
|
+
const narrow = window.matchMedia('(max-width: 1360px)')
|
|
12
|
+
const mobile = window.matchMedia('(max-width: 600px)')
|
|
13
|
+
let expanded = false
|
|
14
|
+
|
|
15
|
+
function sync() {
|
|
16
|
+
if (dialog.open) dialog.close()
|
|
17
|
+
if (narrow.matches) {
|
|
18
|
+
dialog.append(pane)
|
|
19
|
+
if (expanded) {
|
|
20
|
+
if (mobile.matches) dialog.showModal()
|
|
21
|
+
else dialog.show()
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
dialog.before(pane)
|
|
25
|
+
}
|
|
26
|
+
launcher.setAttribute('aria-expanded', String(narrow.matches && expanded))
|
|
27
|
+
launcher.hidden = narrow.matches && expanded
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function open() {
|
|
31
|
+
if (narrow.matches && !expanded) {
|
|
32
|
+
expanded = true
|
|
33
|
+
sync()
|
|
34
|
+
}
|
|
35
|
+
pane.querySelector('textarea')?.focus()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function close() {
|
|
39
|
+
expanded = false
|
|
40
|
+
sync()
|
|
41
|
+
launcher.focus()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {Event} event */
|
|
45
|
+
function cancel(event) {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
close()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {KeyboardEvent} event */
|
|
51
|
+
function trapFocus(event) {
|
|
52
|
+
if (event.key !== 'Tab' || !mobile.matches || !dialog.open) return
|
|
53
|
+
const controls = [
|
|
54
|
+
...dialog.querySelectorAll('button, select, textarea, input, a[href], [tabindex]'),
|
|
55
|
+
].filter(
|
|
56
|
+
el =>
|
|
57
|
+
el instanceof HTMLElement &&
|
|
58
|
+
el.tabIndex >= 0 &&
|
|
59
|
+
!el.matches(':disabled') &&
|
|
60
|
+
el.getClientRects().length
|
|
61
|
+
)
|
|
62
|
+
const first = /** @type {HTMLElement | undefined} */ (controls[0])
|
|
63
|
+
const last = /** @type {HTMLElement | undefined} */ (controls.at(-1))
|
|
64
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
last?.focus()
|
|
67
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
first?.focus()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
launcher.addEventListener('click', open)
|
|
74
|
+
minimize.addEventListener('click', close)
|
|
75
|
+
dialog.addEventListener('cancel', cancel)
|
|
76
|
+
dialog.addEventListener('keydown', trapFocus)
|
|
77
|
+
narrow.addEventListener('change', sync)
|
|
78
|
+
mobile.addEventListener('change', sync)
|
|
79
|
+
sync()
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
open,
|
|
83
|
+
stop() {
|
|
84
|
+
if (dialog.open) dialog.close()
|
|
85
|
+
launcher.removeEventListener('click', open)
|
|
86
|
+
minimize.removeEventListener('click', close)
|
|
87
|
+
dialog.removeEventListener('cancel', cancel)
|
|
88
|
+
dialog.removeEventListener('keydown', trapFocus)
|
|
89
|
+
narrow.removeEventListener('change', sync)
|
|
90
|
+
mobile.removeEventListener('change', sync)
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
package/static/js/chat.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
/** @typedef {import('./review-session.js').ReviewSession} ReviewSession */
|
|
9
9
|
import { cancelChat, createThread, fetchThreadHistory, fetchThreads, streamChat } from './api.js'
|
|
10
10
|
import { chatContextAttrs, chatContextLabel, sameChatContext, WHOLE_PR } from './chat-context.js'
|
|
11
|
+
import { wireChatPanel } from './chat-panel.js'
|
|
11
12
|
import {
|
|
12
13
|
canScrollBy,
|
|
13
14
|
chatScrollReducer,
|
|
@@ -43,7 +44,8 @@ export function renderChatShell(opts) {
|
|
|
43
44
|
`<button class="handle" type="button" id="chat-handle" role="separator" aria-orientation="vertical" aria-label="Resize AI Chat" aria-valuenow="${width}" aria-valuemin="${CHAT_WIDTH_MIN}" aria-valuemax="${CHAT_WIDTH_MAX}"></button>` +
|
|
44
45
|
'<div class="chat-h"><h2 id="chat-h">AI Chat</h2><label class="sr" for="thread">Thread</label>' +
|
|
45
46
|
'<select id="thread"></select>' +
|
|
46
|
-
'<button class="cmd" type="button" id="new-thread">new thread</button
|
|
47
|
+
'<button class="cmd" type="button" id="new-thread">new thread</button>' +
|
|
48
|
+
'<button class="cmd chat-minimize" type="button" id="chat-minimize" aria-label="Minimize AI Chat">minimize</button></div>' +
|
|
47
49
|
'<p class="ctx-line" id="chat-ctx-line">Context: <span class="ctx-chip" id="chat-ctx">whole PR</span>' +
|
|
48
50
|
' <button class="cmd" type="button" id="chat-clear" hidden>clear</button></p>' +
|
|
49
51
|
'<div class="transcript" id="chat-log" role="log" aria-label="Transcript" aria-live="polite" tabindex="0">' +
|
|
@@ -55,7 +57,9 @@ export function renderChatShell(opts) {
|
|
|
55
57
|
'<div class="tbtns"><button class="cmd fill" type="submit" id="chat-send">send</button>' +
|
|
56
58
|
'<button class="cmd" type="button" id="chat-stop" hidden>stop</button>' +
|
|
57
59
|
'<span class="muted small">enter to send</span></div></form>' +
|
|
58
|
-
'</aside>'
|
|
60
|
+
'</aside>' +
|
|
61
|
+
'<dialog class="chat-dialog" id="chat-dialog" aria-labelledby="chat-h"></dialog>' +
|
|
62
|
+
'<button class="chat-launcher" id="chat-launcher" type="button" aria-controls="chat-dialog" aria-expanded="false">AI Chat</button>'
|
|
59
63
|
)
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -97,12 +101,13 @@ export function writeChatWidth(storage, width) {
|
|
|
97
101
|
storage?.setItem(CHAT_WIDTH_KEY, String(clampWidth(width)))
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
/** The
|
|
104
|
+
/** The questions the quick menu offers, and the one that just focuses the box. */
|
|
101
105
|
export const QUICK_QUESTIONS = [
|
|
102
|
-
'
|
|
103
|
-
'What could break?',
|
|
106
|
+
'Suggestion to solve this?',
|
|
104
107
|
'Why this way, and what were the alternatives?',
|
|
108
|
+
'What could break?',
|
|
105
109
|
'Does this follow how the codebase already does it?',
|
|
110
|
+
'Is this covered by tests?',
|
|
106
111
|
]
|
|
107
112
|
|
|
108
113
|
/**
|
|
@@ -148,7 +153,9 @@ function turnHtml(role, bodyHtml, opts = {}) {
|
|
|
148
153
|
*/
|
|
149
154
|
function turnInnerHtml(role, bodyHtml, opts = {}) {
|
|
150
155
|
const note =
|
|
151
|
-
opts.incomplete === undefined
|
|
156
|
+
opts.incomplete === undefined
|
|
157
|
+
? ''
|
|
158
|
+
: `<p class="muted small">the answer stopped early (${esc(opts.incomplete)})</p>`
|
|
152
159
|
return (
|
|
153
160
|
`<span class="role">${role === 'assistant' ? 'AI Chat' : 'You'}</span>` +
|
|
154
161
|
`<div class="prose">${bodyHtml}</div>${note}`
|
|
@@ -235,7 +242,11 @@ export function wireChat(options) {
|
|
|
235
242
|
const { root, prNumber, session } = options
|
|
236
243
|
const api = withDefaults(options.api)
|
|
237
244
|
const storage =
|
|
238
|
-
options.storage === undefined
|
|
245
|
+
options.storage === undefined
|
|
246
|
+
? typeof localStorage === 'undefined'
|
|
247
|
+
? null
|
|
248
|
+
: localStorage
|
|
249
|
+
: options.storage
|
|
239
250
|
const paths = new Set(session.artifact.files.map(f => f.path))
|
|
240
251
|
const targets = targetsFromFiles(session.artifact.files)
|
|
241
252
|
const reducedMotion =
|
|
@@ -304,7 +315,8 @@ export function wireChat(options) {
|
|
|
304
315
|
applyWidth(readChatWidth(storage))
|
|
305
316
|
|
|
306
317
|
const layerTitle = /** @param {string} id */ id => session.artifact.layers.find(l => l.id === id)?.title
|
|
307
|
-
const pointTitle = /** @param {string} fp */ fp =>
|
|
318
|
+
const pointTitle = /** @param {string} fp */ fp =>
|
|
319
|
+
session.artifact.points.find(p => p.fingerprint === fp)?.title
|
|
308
320
|
|
|
309
321
|
const drawContext = () => {
|
|
310
322
|
chip.textContent = chatContextLabel(context, layerTitle, pointTitle)
|
|
@@ -416,7 +428,8 @@ export function wireChat(options) {
|
|
|
416
428
|
activeThread = data.activeThread
|
|
417
429
|
select.innerHTML = data.threads
|
|
418
430
|
.map(
|
|
419
|
-
t =>
|
|
431
|
+
t =>
|
|
432
|
+
`<option value="${esc(t.name)}"${t.name === data.activeThread ? ' selected' : ''}>${esc(t.title)}</option>`
|
|
420
433
|
)
|
|
421
434
|
.join('')
|
|
422
435
|
if (data.threads.length === 0) {
|
|
@@ -510,7 +523,7 @@ export function wireChat(options) {
|
|
|
510
523
|
const started = Date.now()
|
|
511
524
|
const updateActivity = () => {
|
|
512
525
|
const elapsed = Date.now() - started
|
|
513
|
-
activity.textContent = `Preparing answer${'.'.repeat(Math.floor(elapsed / 400) % 3 + 1)} · ${Math.floor(elapsed / 1000)}s`
|
|
526
|
+
activity.textContent = `Preparing answer${'.'.repeat((Math.floor(elapsed / 400) % 3) + 1)} · ${Math.floor(elapsed / 1000)}s`
|
|
514
527
|
}
|
|
515
528
|
updateActivity()
|
|
516
529
|
const timer = setInterval(updateActivity, 400)
|
|
@@ -544,7 +557,10 @@ export function wireChat(options) {
|
|
|
544
557
|
const previous = tools.get(id)
|
|
545
558
|
const title = String(field('title') ?? 'tool')
|
|
546
559
|
tools.delete(id)
|
|
547
|
-
tools.set(id, {
|
|
560
|
+
tools.set(id, {
|
|
561
|
+
title: title === 'tool' && previous ? previous.title : title,
|
|
562
|
+
status: String(field('status') ?? 'pending'),
|
|
563
|
+
})
|
|
548
564
|
toolDetails.hidden = false
|
|
549
565
|
const latest = tools.get(id)
|
|
550
566
|
toolDetails.innerHTML = `<summary>${esc(latest.title)} · ${esc(latest.status)} (${tools.size} tool calls)</summary><ul>${[...tools.values()].map(t => `<li>${esc(t.title)} · ${esc(t.status)}</li>`).join('')}</ul>`
|
|
@@ -578,9 +594,13 @@ export function wireChat(options) {
|
|
|
578
594
|
const toolSummary = toolDetails.querySelector('summary')
|
|
579
595
|
if (toolSummary) toolSummary.textContent = `${tools.size} tool calls`
|
|
580
596
|
activity.textContent = `Elapsed: ${Math.floor((Date.now() - started) / 1000)}s`
|
|
581
|
-
if (frame !== 0) {
|
|
597
|
+
if (frame !== 0) {
|
|
598
|
+
cancelFrame(frame)
|
|
599
|
+
frame = 0
|
|
600
|
+
}
|
|
582
601
|
setStreaming(false)
|
|
583
|
-
answer.body.innerHTML =
|
|
602
|
+
answer.body.innerHTML =
|
|
603
|
+
text === '' ? '<span class="muted">no answer</span>' : renderAnswer(text, turnKey)
|
|
584
604
|
if (scroll.pinned) {
|
|
585
605
|
scrollToBottom('follow')
|
|
586
606
|
}
|
|
@@ -728,6 +748,7 @@ export function wireChat(options) {
|
|
|
728
748
|
}
|
|
729
749
|
|
|
730
750
|
const stopResize = wireResize(pane, root, storage, applyWidth)
|
|
751
|
+
const panel = wireChatPanel(root, pane)
|
|
731
752
|
|
|
732
753
|
form.addEventListener('submit', onSubmit)
|
|
733
754
|
root.addEventListener('click', onClick)
|
|
@@ -744,12 +765,12 @@ export function wireChat(options) {
|
|
|
744
765
|
},
|
|
745
766
|
setContext,
|
|
746
767
|
focusInput() {
|
|
747
|
-
|
|
768
|
+
panel.open()
|
|
748
769
|
},
|
|
749
770
|
/** @param {ChatContext} next */
|
|
750
771
|
ask(next) {
|
|
751
772
|
setContext(next)
|
|
752
|
-
|
|
773
|
+
panel.open()
|
|
753
774
|
},
|
|
754
775
|
/**
|
|
755
776
|
* Sends one of the quick questions about a target.
|
|
@@ -758,6 +779,7 @@ export function wireChat(options) {
|
|
|
758
779
|
*/
|
|
759
780
|
askQuestion(next, question) {
|
|
760
781
|
setContext(next)
|
|
782
|
+
panel.open()
|
|
761
783
|
box.value = question
|
|
762
784
|
void runCommand(sendButton, send, { pendingLabel: 'sending…' })
|
|
763
785
|
},
|
|
@@ -775,6 +797,7 @@ export function wireChat(options) {
|
|
|
775
797
|
select.removeEventListener('change', onThreadChange)
|
|
776
798
|
box.removeEventListener('keydown', /** @type {EventListener} */ (onKeyDown))
|
|
777
799
|
stopResize()
|
|
800
|
+
panel.stop()
|
|
778
801
|
},
|
|
779
802
|
}
|
|
780
803
|
}
|
package/static/js/composer.js
CHANGED
|
@@ -61,8 +61,8 @@ export function composerHtml(opts) {
|
|
|
61
61
|
*/
|
|
62
62
|
export function composerRowHtml(opts) {
|
|
63
63
|
return (
|
|
64
|
-
`<tr class="composer" data-decoration="composer"
|
|
65
|
-
`<td class="code x">${composerHtml(opts)}</td></tr>`
|
|
64
|
+
`<tr class="composer" data-decoration="composer">` +
|
|
65
|
+
`<td class="code x" colspan="4">${composerHtml(opts)}</td></tr>`
|
|
66
66
|
)
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -232,7 +232,10 @@ export function toggleMarkdownPreview(button, preview) {
|
|
|
232
232
|
const textarea = host?.querySelector('textarea')
|
|
233
233
|
const output = host?.querySelector('.markdown-preview')
|
|
234
234
|
if (!(textarea instanceof HTMLTextAreaElement) || !(output instanceof HTMLElement)) return
|
|
235
|
-
if (preview)
|
|
235
|
+
if (preview)
|
|
236
|
+
output.innerHTML = textarea.value.trim()
|
|
237
|
+
? renderMarkdown(textarea.value, { github: true })
|
|
238
|
+
: '<p class="muted">Nothing to preview.</p>'
|
|
236
239
|
textarea.hidden = preview
|
|
237
240
|
output.hidden = !preview
|
|
238
241
|
host?.querySelector('[data-act="markdown-write"]')?.setAttribute('aria-pressed', String(!preview))
|
package/static/js/diagram.js
CHANGED
|
@@ -554,7 +554,8 @@ export function initDiagrams(root, opts = {}) {
|
|
|
554
554
|
if (activateNodeLink(event)) {
|
|
555
555
|
return
|
|
556
556
|
}
|
|
557
|
-
const chevron =
|
|
557
|
+
const chevron =
|
|
558
|
+
event.target instanceof Element ? event.target.closest('.diagram > .diagram-h > .chev') : null
|
|
558
559
|
const node = chevron?.closest('.diagram')
|
|
559
560
|
if (node instanceof HTMLElement) {
|
|
560
561
|
void toggleDiagram(node, pass)
|
|
@@ -41,7 +41,7 @@ export function insertNoteRow(card, key, a) {
|
|
|
41
41
|
}
|
|
42
42
|
const range = a.startLine === a.endLine ? `line ${a.startLine}` : `lines ${a.startLine}–${a.endLine}`
|
|
43
43
|
const html =
|
|
44
|
-
`<tr class="annot ann${approx ? ' is-approx' : ''}" ${DECORATION}="note"><td class="
|
|
44
|
+
`<tr class="annot ann${approx ? ' is-approx' : ''}" ${DECORATION}="note"><td class="code x" colspan="4">` +
|
|
45
45
|
`<span class="lbl">Annotation · ${esc(range)}${a.side === 'old' ? ' (old)' : ''}</span>` +
|
|
46
46
|
`<div class="prose">${renderMarkdown(a.text)}</div></td></tr>`
|
|
47
47
|
last.insertAdjacentElement('afterend', firstRow(html))
|
|
@@ -102,7 +102,7 @@ export function threadRowHtml(t, opts) {
|
|
|
102
102
|
: `<button class="cmd" type="button" data-act="thread-hide" data-thread="${rootId}">hide</button>`
|
|
103
103
|
return (
|
|
104
104
|
`<tr class="thread${t.resolved ? ' resolved' : ''}${hidden ? ' hidden-thread' : ''}" data-thread="${rootId}">` +
|
|
105
|
-
'<td class="
|
|
105
|
+
'<td class="code x" colspan="4">' +
|
|
106
106
|
`<div class="thread-full"${collapsed ? ' hidden' : ''}>${comments}` +
|
|
107
107
|
`<span class="tbtns"><button class="cmd" type="button" data-act="thread-reply" data-thread="${rootId}" data-needs-post>reply</button>${hide}</span></div>` +
|
|
108
108
|
`<div class="thread-collapsed"${collapsed ? '' : ' hidden'}>${label} · ` +
|
|
@@ -186,7 +186,9 @@ export function applyDecorations(card, key, data) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
for (const t of [...data.threads].reverse()) {
|
|
189
|
-
count(
|
|
189
|
+
count(
|
|
190
|
+
insertThreadRow(card, key, t, { now: data.now, hidden: data.hiddenThreads?.has(t.root.id) ?? false })
|
|
191
|
+
)
|
|
190
192
|
}
|
|
191
193
|
for (const p of [...data.points].reverse()) {
|
|
192
194
|
count(
|
|
@@ -113,7 +113,12 @@ export function parsePatch(patch) {
|
|
|
113
113
|
prevDel = true
|
|
114
114
|
prevAdd = false
|
|
115
115
|
} else {
|
|
116
|
-
cur.entries.push({
|
|
116
|
+
cur.entries.push({
|
|
117
|
+
...base,
|
|
118
|
+
code: line.startsWith(' ') ? line.slice(1) : line,
|
|
119
|
+
oldLine: oL,
|
|
120
|
+
newLine: nL,
|
|
121
|
+
})
|
|
117
122
|
oL++
|
|
118
123
|
nL++
|
|
119
124
|
prevAdd = false
|
package/static/js/dom.js
CHANGED
|
@@ -20,7 +20,7 @@ export function esc(value) {
|
|
|
20
20
|
* in interactions.js reads `data-act` and toggles the card this button belongs to.
|
|
21
21
|
* @param {string} label
|
|
22
22
|
* @param {boolean} [expanded]
|
|
23
|
-
* @param {{ act?: string }} [opts]
|
|
23
|
+
* @param {{ act?: string }} [opts]
|
|
24
24
|
*/
|
|
25
25
|
export function chevronHtml(label, expanded = true, opts = {}) {
|
|
26
26
|
const act = opts.act === undefined ? '' : ` data-act="${esc(opts.act)}"`
|
|
@@ -29,14 +29,12 @@ export function chevronHtml(label, expanded = true, opts = {}) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The <summary> of a collapsible <details> block: a chevron, then the title. The block relies on the
|
|
32
|
-
* native <details> toggle
|
|
33
|
-
* the <details> carries the `open` attribute so the chevron reports the same state.
|
|
32
|
+
* native <details> toggle. The decorative chevron lets clicks activate the summary.
|
|
34
33
|
* @param {string} titleHtml already escaped
|
|
35
|
-
* @param {string} label the
|
|
36
|
-
* @param {{ open?: boolean }} [opts]
|
|
34
|
+
* @param {string} label the summary's accessible name
|
|
37
35
|
*/
|
|
38
|
-
export function detailsSummaryHtml(titleHtml, label
|
|
39
|
-
return `<summary
|
|
36
|
+
export function detailsSummaryHtml(titleHtml, label) {
|
|
37
|
+
return `<summary aria-label="${esc(label)}"><span class="chev" aria-hidden="true">></span>${titleHtml}</summary>`
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
/**
|
package/static/js/empty-state.js
CHANGED
|
@@ -50,7 +50,9 @@ export function validateCanvasFilename(filename) {
|
|
|
50
50
|
if (!filename.toLowerCase().endsWith('.zip')) {
|
|
51
51
|
return 'that is not a zip file'
|
|
52
52
|
}
|
|
53
|
-
return /^(?:pr-[1-9]\d*|ref)-.+-canvas\.zip$/i.test(filename)
|
|
53
|
+
return /^(?:pr-[1-9]\d*|ref)-.+-canvas\.zip$/i.test(filename)
|
|
54
|
+
? null
|
|
55
|
+
: 'that zip is not a review canvas export'
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
package/static/js/header.js
CHANGED
|
@@ -65,7 +65,7 @@ export function renderHeader(bundle, opts) {
|
|
|
65
65
|
const risk = ready ? riskLineHtml(artifact.risk) : ''
|
|
66
66
|
return (
|
|
67
67
|
'<header class="hdr">' +
|
|
68
|
-
`<div class="hdr-bar"><div class="brand"><span class="
|
|
68
|
+
`<div class="hdr-bar"><div class="brand"><span class="brand-wordmark"><img class="brand-icon" src="/static/brand.svg" width="32" height="32" alt="">PR review canvas</span><span class="mono muted">${esc(opts.host)}</span></div>` +
|
|
69
69
|
'<div class="hdr-actions" role="group" aria-label="Canvas actions">' +
|
|
70
70
|
`<button class="cmd" type="button" id="regenerate" title="Generate a new canvas for this PR" aria-haspopup="dialog"${hasCanvas ? '' : ' disabled'}>regenerate</button>` +
|
|
71
71
|
`<button class="cmd" type="button" id="export-zip" title="Download this canvas as a zip to share on GitHub"${hasCanvas ? '' : ' disabled'}>export zip</button>` +
|