@iceinvein/agent-skills 0.1.24 → 0.1.26
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 +1 -0
- package/package.json +1 -1
- package/skills/magpie/README.md +16 -1
- package/skills/magpie/bin/magpie.ts +60 -2
- package/skills/magpie/fixtures/example-pr/diff.patch +86 -0
- package/skills/magpie/fixtures/example-pr/findings.final.json +137 -0
- package/skills/magpie/fixtures/example-pr/post-status.json +5 -0
- package/skills/magpie/fixtures/example-pr/pr.json +21 -0
- package/skills/magpie/fixtures/fake-gh.sh +10 -1
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/scripts/__tests__/cli.test.ts +119 -0
- package/skills/magpie/scripts/__tests__/diff-utils.test.ts +106 -0
- package/skills/magpie/scripts/__tests__/file-tree.test.ts +97 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +8 -0
- package/skills/magpie/scripts/__tests__/helper.test.ts +63 -0
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +10 -0
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +90 -1
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +210 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +2 -2
- package/skills/magpie/scripts/__tests__/render-action-bar.test.ts +66 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +46 -0
- package/skills/magpie/scripts/__tests__/render-diff.test.ts +84 -0
- package/skills/magpie/scripts/__tests__/render-file-tree.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +104 -182
- package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +86 -0
- package/skills/magpie/scripts/__tests__/server.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/types.test.ts +48 -3
- package/skills/magpie/scripts/diff-utils.ts +129 -0
- package/skills/magpie/scripts/file-tree.ts +74 -0
- package/skills/magpie/scripts/gh.ts +1 -0
- package/skills/magpie/scripts/helper.js +402 -192
- package/skills/magpie/scripts/post-cmd.ts +202 -0
- package/skills/magpie/scripts/preview-cmd.ts +300 -0
- package/skills/magpie/scripts/refresh.ts +16 -1
- package/skills/magpie/scripts/render-action-bar.ts +51 -0
- package/skills/magpie/scripts/render-annotation.ts +96 -0
- package/skills/magpie/scripts/render-cmd.ts +24 -1
- package/skills/magpie/scripts/render-diff.ts +125 -0
- package/skills/magpie/scripts/render-file-tree.ts +86 -0
- package/skills/magpie/scripts/render-findings.ts +165 -196
- package/skills/magpie/scripts/render-issues-list.ts +78 -0
- package/skills/magpie/scripts/render-progress.ts +74 -32
- package/skills/magpie/scripts/server.ts +43 -0
- package/skills/magpie/scripts/types.ts +13 -0
- package/skills/magpie/skill.json +4 -2
- package/skills/magpie/templates/styles.css +1288 -629
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
;(() => {
|
|
2
2
|
let isLive = false
|
|
3
|
-
let focusedIndex = -1
|
|
4
3
|
|
|
5
|
-
const
|
|
4
|
+
const fileFindingIdx = new Map()
|
|
6
5
|
|
|
7
6
|
function isLikelyArchived() {
|
|
8
7
|
return location.protocol === 'file:' || location.protocol === 'about:'
|
|
@@ -38,12 +37,9 @@
|
|
|
38
37
|
return Array.from(document.querySelectorAll('input[type="checkbox"][data-finding-id]'))
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function visibleFindings() {
|
|
46
|
-
return allFindings().filter((el) => !el.classList.contains('is-hidden'))
|
|
40
|
+
function cssEscape(s) {
|
|
41
|
+
// Minimal CSS attr-selector escape: backslash-escape backslashes and quotes.
|
|
42
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
function post(payload) {
|
|
@@ -59,7 +55,7 @@
|
|
|
59
55
|
let pendingPostIds = []
|
|
60
56
|
|
|
61
57
|
function showStatus(message, tone, sticky) {
|
|
62
|
-
const el = document.querySelector('[data-role="
|
|
58
|
+
const el = document.querySelector('[data-role="post-status"]')
|
|
63
59
|
if (!(el instanceof HTMLElement)) return
|
|
64
60
|
el.textContent = message
|
|
65
61
|
el.hidden = false
|
|
@@ -106,34 +102,33 @@
|
|
|
106
102
|
return bar instanceof HTMLElement && !bar.hidden
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
function badgeHtml(result) {
|
|
110
|
-
if (result.status === 'posted') return '<span class="badge posted">posted</span>'
|
|
111
|
-
if (result.status === 'already-posted')
|
|
112
|
-
return '<span class="badge posted">already posted</span>'
|
|
113
|
-
if (result.status === 'unknown-id')
|
|
114
|
-
return '<span class="badge failed">failed: unknown id</span>'
|
|
115
|
-
const msg = result.message || 'gh failed'
|
|
116
|
-
const span = document.createElement('span')
|
|
117
|
-
span.className = 'badge failed'
|
|
118
|
-
span.textContent = `failed: ${msg}`
|
|
119
|
-
return span.outerHTML
|
|
120
|
-
}
|
|
121
|
-
|
|
122
105
|
function applyResult(result) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!head) return
|
|
127
|
-
// Remove any pre-existing badge in this head.
|
|
128
|
-
const existing = head.querySelector('.badge')
|
|
129
|
-
if (existing) existing.remove()
|
|
130
|
-
head.insertAdjacentHTML('beforeend', badgeHtml(result))
|
|
106
|
+
if (!result?.id) return
|
|
107
|
+
const containers = findAnnotation(result.id)
|
|
108
|
+
if (containers.length === 0) return
|
|
131
109
|
if (result.status === 'posted' || result.status === 'already-posted') {
|
|
132
|
-
const
|
|
133
|
-
|
|
110
|
+
for (const el of containers) {
|
|
111
|
+
el.setAttribute('data-posted', 'true')
|
|
112
|
+
const chip = el.querySelector('.status-chip')
|
|
113
|
+
if (chip) {
|
|
114
|
+
chip.className = 'status-chip posted'
|
|
115
|
+
chip.textContent = 'POSTED'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const cb of findCheckboxes(result.id)) {
|
|
134
119
|
cb.checked = true
|
|
135
120
|
cb.disabled = true
|
|
136
121
|
}
|
|
122
|
+
} else if (result.status === 'failed' || result.status === 'unknown-id') {
|
|
123
|
+
const msg = result.message || (result.status === 'unknown-id' ? 'unknown id' : 'gh failed')
|
|
124
|
+
for (const el of containers) {
|
|
125
|
+
el.setAttribute('data-failed', 'true')
|
|
126
|
+
const chip = el.querySelector('.status-chip')
|
|
127
|
+
if (chip) {
|
|
128
|
+
chip.className = 'status-chip failed'
|
|
129
|
+
chip.textContent = `FAILED: ${msg}`
|
|
130
|
+
}
|
|
131
|
+
}
|
|
137
132
|
}
|
|
138
133
|
}
|
|
139
134
|
|
|
@@ -197,12 +192,24 @@
|
|
|
197
192
|
// active selection. Exclude them so "clear" actually clears, the counter
|
|
198
193
|
// reflects only what's actionable, and localStorage doesn't pin them.
|
|
199
194
|
const activelyChecked = allCheckboxes().filter((el) => el.checked && !el.disabled)
|
|
200
|
-
const n = activelyChecked.length
|
|
201
|
-
const node = document.querySelector('[data-role="selected-count"] .num')
|
|
202
|
-
if (node) node.textContent = String(n)
|
|
203
|
-
const btn = document.querySelector('[data-action="post"]')
|
|
204
|
-
if (btn instanceof HTMLButtonElement) btn.disabled = n === 0
|
|
205
195
|
writePersistedSelection(activelyChecked.map((el) => el.dataset.findingId))
|
|
196
|
+
updateSelectedCount()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// New Pylon-style selection count
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function updateSelectedCount() {
|
|
204
|
+
// Count unique finding ids that have any checked checkbox.
|
|
205
|
+
const ids = new Set()
|
|
206
|
+
for (const cb of document.querySelectorAll('input[type="checkbox"][data-finding-id]')) {
|
|
207
|
+
if (cb.checked && !cb.disabled) ids.add(cb.getAttribute('data-finding-id'))
|
|
208
|
+
}
|
|
209
|
+
const node = document.querySelector('[data-role="selected-count"]')
|
|
210
|
+
if (node) node.textContent = String(ids.size)
|
|
211
|
+
const postSel = document.querySelector('[data-action="post-selected"]')
|
|
212
|
+
if (postSel) postSel.disabled = ids.size === 0
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
function restoreSelection() {
|
|
@@ -214,144 +221,306 @@
|
|
|
214
221
|
}
|
|
215
222
|
}
|
|
216
223
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const domOk = domainSet.size === 0 || domainSet.has(dom)
|
|
228
|
-
const searchOk = !search || text.includes(search)
|
|
229
|
-
const ok = sevOk && domOk && searchOk
|
|
230
|
-
card.classList.toggle('is-hidden', !ok)
|
|
231
|
-
if (ok) visible++
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Tab switching
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function handleSetView(btn) {
|
|
229
|
+
const view = btn.getAttribute('data-view')
|
|
230
|
+
if (!view) return
|
|
231
|
+
document.body.dataset.view = view
|
|
232
|
+
for (const s of document.querySelectorAll('[data-action="set-view"]')) {
|
|
233
|
+
s.setAttribute('aria-pressed', s.getAttribute('data-view') === view ? 'true' : 'false')
|
|
232
234
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// File selection
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
function handleSelectFile(btn) {
|
|
242
|
+
const path = btn.getAttribute('data-file') ?? ''
|
|
243
|
+
document.body.dataset.selectedFile = path
|
|
244
|
+
for (const p of document.querySelectorAll('[data-file-pane]')) {
|
|
245
|
+
p.hidden = p.getAttribute('data-file-pane') !== path
|
|
240
246
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
const hasFilter = sevSet.size > 0 || domainSet.size > 0 || !!search
|
|
244
|
-
clearBtn.hidden = !hasFilter
|
|
247
|
+
for (const b of document.querySelectorAll('[data-action="select-file"]')) {
|
|
248
|
+
b.classList.toggle('active', b.getAttribute('data-file') === path)
|
|
245
249
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
// Reset finding nav for new file.
|
|
251
|
+
fileFindingIdx.set(path, -1)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Diff-mode toggle
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
function handleSetDiffMode(btn) {
|
|
259
|
+
const mode = btn.getAttribute('data-mode')
|
|
260
|
+
if (!mode) return
|
|
261
|
+
document.body.dataset.diffMode = mode
|
|
262
|
+
for (const b of document.querySelectorAll('[data-action="set-diff-mode"]')) {
|
|
263
|
+
b.setAttribute('aria-pressed', b.getAttribute('data-mode') === mode ? 'true' : 'false')
|
|
251
264
|
}
|
|
265
|
+
// Toggle visibility of .diff-unified and .diff-split inside each file pane.
|
|
266
|
+
for (const el of document.querySelectorAll('.diff-unified')) {
|
|
267
|
+
el.hidden = mode !== 'unified'
|
|
268
|
+
}
|
|
269
|
+
for (const el of document.querySelectorAll('.diff-split')) {
|
|
270
|
+
el.hidden = mode !== 'split'
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Finding navigation (per file)
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function annotsInActiveFile() {
|
|
279
|
+
const path = document.body.dataset.selectedFile ?? ''
|
|
280
|
+
const pane = document.querySelector(`[data-file-pane="${cssEscape(path)}"]`)
|
|
281
|
+
if (!pane) return []
|
|
282
|
+
// Use the visible diff-mode container so split mode finds its own annotations.
|
|
283
|
+
const mode = document.body.dataset.diffMode ?? 'unified'
|
|
284
|
+
const container = pane.querySelector(`[data-diff-mode="${mode}"]`) || pane
|
|
285
|
+
return Array.from(container.querySelectorAll('.annot'))
|
|
252
286
|
}
|
|
253
287
|
|
|
254
|
-
function
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
288
|
+
function navigateFinding(direction) {
|
|
289
|
+
const annots = annotsInActiveFile()
|
|
290
|
+
if (annots.length === 0) return
|
|
291
|
+
const path = document.body.dataset.selectedFile ?? ''
|
|
292
|
+
const cur = fileFindingIdx.get(path) ?? -1
|
|
293
|
+
let next
|
|
294
|
+
if (cur === -1) {
|
|
295
|
+
next = direction > 0 ? 0 : annots.length - 1
|
|
262
296
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
297
|
+
next = cur + direction
|
|
298
|
+
if (next < 0) next = annots.length - 1
|
|
299
|
+
if (next >= annots.length) next = 0
|
|
300
|
+
}
|
|
301
|
+
fileFindingIdx.set(path, next)
|
|
302
|
+
for (const a of annots) a.classList.remove('is-focused')
|
|
303
|
+
const target = annots[next]
|
|
304
|
+
if (target) {
|
|
305
|
+
target.classList.add('is-focused')
|
|
306
|
+
target.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
265
307
|
}
|
|
266
|
-
applyFilters()
|
|
267
308
|
}
|
|
268
309
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (search instanceof HTMLInputElement) search.value = ''
|
|
278
|
-
applyFilters()
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Suggestions toggle
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
function handleToggleSuggestions(btn) {
|
|
315
|
+
const cur = document.body.dataset.showSuggestions === 'true'
|
|
316
|
+
document.body.dataset.showSuggestions = cur ? 'false' : 'true'
|
|
317
|
+
btn.setAttribute('aria-pressed', cur ? 'false' : 'true')
|
|
279
318
|
}
|
|
280
319
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Severity filter (hide-sev-*)
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
function handleFilterSev(btn) {
|
|
325
|
+
const sev = btn.getAttribute('data-sev')
|
|
326
|
+
if (!sev) return
|
|
327
|
+
const cls = `hide-sev-${sev}`
|
|
328
|
+
const wasHidden = document.body.classList.contains(cls)
|
|
329
|
+
document.body.classList.toggle(cls, !wasHidden)
|
|
330
|
+
btn.setAttribute('aria-pressed', wasHidden ? 'true' : 'false')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Checkbox helpers
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
// findAnnotation returns all non-input elements for a given finding id.
|
|
338
|
+
function findAnnotation(id) {
|
|
339
|
+
return Array.from(document.querySelectorAll(`[data-finding-id="${cssEscape(id)}"]:not(input)`))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function findCheckboxes(id) {
|
|
343
|
+
return Array.from(
|
|
344
|
+
document.querySelectorAll(`input[type="checkbox"][data-finding-id="${cssEscape(id)}"]`),
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function setChecked(id, checked) {
|
|
349
|
+
for (const cb of findCheckboxes(id)) {
|
|
284
350
|
if (cb.disabled) continue
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
351
|
+
cb.checked = checked
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Bulk selection
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
function handleSelectSev(btn) {
|
|
360
|
+
const sev = btn.getAttribute('data-sev')
|
|
361
|
+
if (!sev) return
|
|
362
|
+
const ids = new Set()
|
|
363
|
+
for (const el of document.querySelectorAll(
|
|
364
|
+
`[data-finding-id][data-severity="${sev}"][data-suggestion="false"]`,
|
|
365
|
+
)) {
|
|
366
|
+
if (el.tagName.toLowerCase() === 'input') continue
|
|
367
|
+
if (el.getAttribute('data-posted') === 'true') continue
|
|
368
|
+
ids.add(el.getAttribute('data-finding-id'))
|
|
369
|
+
}
|
|
370
|
+
for (const id of ids) setChecked(id, true)
|
|
371
|
+
updateSelectedCount()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function handleSelectRecommended() {
|
|
375
|
+
const ids = new Set()
|
|
376
|
+
for (const el of document.querySelectorAll('[data-finding-id][data-suggestion="false"]')) {
|
|
377
|
+
if (el.tagName.toLowerCase() === 'input') continue
|
|
378
|
+
if (el.getAttribute('data-posted') === 'true') continue
|
|
379
|
+
ids.add(el.getAttribute('data-finding-id'))
|
|
380
|
+
}
|
|
381
|
+
for (const id of ids) setChecked(id, true)
|
|
382
|
+
updateSelectedCount()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Toggle focused finding's checkbox
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
function toggleFocusedFinding() {
|
|
390
|
+
const path = document.body.dataset.selectedFile ?? ''
|
|
391
|
+
const idx = fileFindingIdx.get(path) ?? -1
|
|
392
|
+
if (idx === -1) return
|
|
393
|
+
const annots = annotsInActiveFile()
|
|
394
|
+
const annot = annots[idx]
|
|
395
|
+
if (!annot) return
|
|
396
|
+
const id = annot.getAttribute('data-finding-id')
|
|
397
|
+
if (!id) return
|
|
398
|
+
const cbs = findCheckboxes(id)
|
|
399
|
+
if (cbs.length === 0) return
|
|
400
|
+
const cb = cbs[0]
|
|
401
|
+
if (cb.disabled) return
|
|
402
|
+
const next = !cb.checked
|
|
403
|
+
setChecked(id, next)
|
|
404
|
+
post({
|
|
405
|
+
type: next ? 'select' : 'deselect',
|
|
406
|
+
findingId: id,
|
|
407
|
+
timestamp: Date.now(),
|
|
408
|
+
})
|
|
409
|
+
updateSelectedCount()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Pylon-style bulk post via /api/post-review
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
async function postToReview(findingIds) {
|
|
417
|
+
if (!isLive) {
|
|
418
|
+
showStatus(
|
|
419
|
+
'Cannot post from an archived view. Run `magpie serve <run-dir>` first.',
|
|
420
|
+
'warn',
|
|
421
|
+
true,
|
|
422
|
+
)
|
|
423
|
+
return null
|
|
424
|
+
}
|
|
425
|
+
showStatus(`Posting ${findingIds.length} as a single review...`, 'info', true)
|
|
426
|
+
try {
|
|
427
|
+
const r = await fetch('/api/post-review', {
|
|
428
|
+
method: 'POST',
|
|
429
|
+
headers: { 'content-type': 'application/json' },
|
|
430
|
+
body: JSON.stringify({ findingIds }),
|
|
431
|
+
})
|
|
432
|
+
const data = await r.json()
|
|
433
|
+
if (!r.ok) {
|
|
434
|
+
showStatus(`Post failed: ${data?.error || `HTTP ${r.status}`}`, 'warn', true)
|
|
435
|
+
return null
|
|
295
436
|
}
|
|
437
|
+
return data
|
|
438
|
+
} catch (err) {
|
|
439
|
+
showStatus(`Post failed: ${err?.message || err}`, 'warn', true)
|
|
440
|
+
return null
|
|
296
441
|
}
|
|
297
|
-
if (toggled) recountSelected()
|
|
298
442
|
}
|
|
299
443
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
for (const
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
444
|
+
function applyReviewResult(result) {
|
|
445
|
+
if (!result) return
|
|
446
|
+
for (const c of result.comments || []) {
|
|
447
|
+
if (c.status === 'posted') {
|
|
448
|
+
for (const el of findAnnotation(c.id)) {
|
|
449
|
+
el.setAttribute('data-posted', 'true')
|
|
450
|
+
const chip = el.querySelector('.status-chip')
|
|
451
|
+
if (chip) {
|
|
452
|
+
chip.className = 'status-chip posted'
|
|
453
|
+
chip.textContent = 'POSTED'
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const cb of findCheckboxes(c.id)) {
|
|
457
|
+
cb.checked = false
|
|
458
|
+
cb.disabled = true
|
|
459
|
+
}
|
|
460
|
+
} else if (c.status === 'failed') {
|
|
461
|
+
for (const el of findAnnotation(c.id)) {
|
|
462
|
+
el.setAttribute('data-failed', 'true')
|
|
463
|
+
const chip = el.querySelector('.status-chip')
|
|
464
|
+
if (chip) {
|
|
465
|
+
chip.className = 'status-chip failed'
|
|
466
|
+
chip.textContent = 'FAILED'
|
|
467
|
+
}
|
|
468
|
+
}
|
|
312
469
|
}
|
|
313
470
|
}
|
|
314
|
-
|
|
471
|
+
updateSelectedCount()
|
|
472
|
+
const okCount = (result.comments || []).filter((c) => c.status === 'posted').length
|
|
473
|
+
const failCount = (result.comments || []).filter((c) => c.status === 'failed').length
|
|
474
|
+
const reviewBit = result.reviewId ? ` (review ${result.reviewId})` : ''
|
|
475
|
+
if (failCount === 0) {
|
|
476
|
+
showStatus(`Posted ${okCount}${reviewBit}.`, 'ok')
|
|
477
|
+
} else {
|
|
478
|
+
showStatus(`Posted ${okCount}, ${failCount} failed${reviewBit}.`, 'warn', true)
|
|
479
|
+
}
|
|
315
480
|
}
|
|
316
481
|
|
|
317
|
-
function
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
482
|
+
async function handlePostSelected() {
|
|
483
|
+
const ids = []
|
|
484
|
+
const seen = new Set()
|
|
485
|
+
for (const cb of document.querySelectorAll('input[type="checkbox"][data-finding-id]')) {
|
|
486
|
+
if (!cb.checked || cb.disabled) continue
|
|
487
|
+
const id = cb.getAttribute('data-finding-id')
|
|
488
|
+
if (id && !seen.has(id)) {
|
|
489
|
+
seen.add(id)
|
|
490
|
+
ids.push(id)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (ids.length === 0) {
|
|
494
|
+
showStatus('Nothing selected yet.', 'info')
|
|
322
495
|
return
|
|
323
496
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
target.classList.add('is-focused')
|
|
327
|
-
target.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
497
|
+
const result = await postToReview(ids)
|
|
498
|
+
applyReviewResult(result)
|
|
328
499
|
}
|
|
329
500
|
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
501
|
+
async function handlePostRecommended() {
|
|
502
|
+
const ids = []
|
|
503
|
+
const seen = new Set()
|
|
504
|
+
for (const el of document.querySelectorAll('[data-finding-id][data-suggestion="false"]')) {
|
|
505
|
+
if (el.tagName.toLowerCase() === 'input') continue
|
|
506
|
+
if (el.getAttribute('data-posted') === 'true') continue
|
|
507
|
+
const id = el.getAttribute('data-finding-id')
|
|
508
|
+
if (id && !seen.has(id)) {
|
|
509
|
+
seen.add(id)
|
|
510
|
+
ids.push(id)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (ids.length === 0) {
|
|
514
|
+
showStatus('Nothing to post.', 'info')
|
|
335
515
|
return
|
|
336
516
|
}
|
|
337
|
-
const
|
|
338
|
-
|
|
517
|
+
const result = await postToReview(ids)
|
|
518
|
+
applyReviewResult(result)
|
|
339
519
|
}
|
|
340
520
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!card) return
|
|
345
|
-
const cb = card.querySelector('input[type="checkbox"][data-finding-id]')
|
|
346
|
-
if (!(cb instanceof HTMLInputElement) || cb.disabled) return
|
|
347
|
-
cb.checked = !cb.checked
|
|
348
|
-
post({
|
|
349
|
-
type: cb.checked ? 'select' : 'deselect',
|
|
350
|
-
findingId: cb.dataset.findingId,
|
|
351
|
-
timestamp: Date.now(),
|
|
352
|
-
})
|
|
353
|
-
recountSelected()
|
|
354
|
-
}
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Event binding
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
355
524
|
|
|
356
525
|
function bind() {
|
|
357
526
|
document.addEventListener('change', (event) => {
|
|
@@ -359,10 +528,17 @@
|
|
|
359
528
|
if (!(target instanceof HTMLInputElement)) return
|
|
360
529
|
if (target.type !== 'checkbox') return
|
|
361
530
|
if (!target.dataset.findingId) return
|
|
362
|
-
|
|
531
|
+
const id = target.dataset.findingId
|
|
532
|
+
// Sync the paired copy (inline .annot and .issue-card share the same id).
|
|
533
|
+
for (const cb of findCheckboxes(id)) {
|
|
534
|
+
if (cb === target) continue
|
|
535
|
+
if (cb.disabled) continue
|
|
536
|
+
cb.checked = target.checked
|
|
537
|
+
}
|
|
538
|
+
updateSelectedCount()
|
|
363
539
|
post({
|
|
364
540
|
type: target.checked ? 'select' : 'deselect',
|
|
365
|
-
findingId:
|
|
541
|
+
findingId: id,
|
|
366
542
|
timestamp: Date.now(),
|
|
367
543
|
})
|
|
368
544
|
})
|
|
@@ -371,8 +547,7 @@
|
|
|
371
547
|
const target = event.target
|
|
372
548
|
if (!(target instanceof HTMLElement)) return
|
|
373
549
|
|
|
374
|
-
// Click outside the confirm popup
|
|
375
|
-
// dismisses the popup, matching GitHub's small-popup behavior.
|
|
550
|
+
// Click outside the confirm popup dismisses it.
|
|
376
551
|
if (isConfirmOpen()) {
|
|
377
552
|
const insidePopup = target.closest('[data-role="confirm-bar"]')
|
|
378
553
|
const onTrigger = target.closest('[data-action="post"]')
|
|
@@ -381,15 +556,63 @@
|
|
|
381
556
|
}
|
|
382
557
|
}
|
|
383
558
|
|
|
384
|
-
const chip = target.closest('.filter-chip')
|
|
385
|
-
if (chip instanceof HTMLElement) {
|
|
386
|
-
toggleFilterChip(chip)
|
|
387
|
-
return
|
|
388
|
-
}
|
|
389
|
-
|
|
390
559
|
const action = target.closest('[data-action]')
|
|
391
560
|
if (!(action instanceof HTMLElement)) return
|
|
392
561
|
switch (action.dataset.action) {
|
|
562
|
+
// -----------------------------------------------------------------------
|
|
563
|
+
// New Pylon-style actions
|
|
564
|
+
// -----------------------------------------------------------------------
|
|
565
|
+
case 'set-view':
|
|
566
|
+
handleSetView(action)
|
|
567
|
+
break
|
|
568
|
+
case 'select-file':
|
|
569
|
+
handleSelectFile(action)
|
|
570
|
+
break
|
|
571
|
+
case 'set-diff-mode':
|
|
572
|
+
handleSetDiffMode(action)
|
|
573
|
+
break
|
|
574
|
+
case 'prev-finding':
|
|
575
|
+
navigateFinding(-1)
|
|
576
|
+
break
|
|
577
|
+
case 'next-finding':
|
|
578
|
+
navigateFinding(1)
|
|
579
|
+
break
|
|
580
|
+
case 'toggle-suggestions':
|
|
581
|
+
handleToggleSuggestions(action)
|
|
582
|
+
break
|
|
583
|
+
case 'filter-sev':
|
|
584
|
+
handleFilterSev(action)
|
|
585
|
+
break
|
|
586
|
+
case 'select-sev':
|
|
587
|
+
handleSelectSev(action)
|
|
588
|
+
break
|
|
589
|
+
case 'select-recommended':
|
|
590
|
+
handleSelectRecommended()
|
|
591
|
+
break
|
|
592
|
+
case 'post-selected':
|
|
593
|
+
void handlePostSelected()
|
|
594
|
+
break
|
|
595
|
+
case 'post-recommended':
|
|
596
|
+
void handlePostRecommended()
|
|
597
|
+
break
|
|
598
|
+
case 'post-one': {
|
|
599
|
+
// Legacy per-finding send icon (hover button).
|
|
600
|
+
const id = action.getAttribute('data-finding-id')
|
|
601
|
+
if (id && isLive) {
|
|
602
|
+
openConfirm([id])
|
|
603
|
+
} else if (!isLive) {
|
|
604
|
+
showStatus(
|
|
605
|
+
`Cannot post from an archived view. Run \`magpie serve ${runId()}\` to make this live.`,
|
|
606
|
+
'warn',
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
break
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// -----------------------------------------------------------------------
|
|
613
|
+
// Legacy actions (kept so action-key strings remain present for tests;
|
|
614
|
+
// the new markup does not emit these data-action values).
|
|
615
|
+
// -----------------------------------------------------------------------
|
|
393
616
|
case 'post': {
|
|
394
617
|
const checked = allCheckboxes()
|
|
395
618
|
.filter((el) => el.checked)
|
|
@@ -419,32 +642,29 @@
|
|
|
419
642
|
break
|
|
420
643
|
}
|
|
421
644
|
case 'select-visible':
|
|
422
|
-
|
|
645
|
+
// Legacy; no-op in new layout.
|
|
423
646
|
break
|
|
424
647
|
case 'select-priority':
|
|
425
|
-
|
|
426
|
-
(card) =>
|
|
427
|
-
!card.classList.contains('is-hidden') &&
|
|
428
|
-
(card.dataset.severity === 'blocker' || card.dataset.severity === 'high'),
|
|
429
|
-
)
|
|
648
|
+
// Legacy; no-op in new layout.
|
|
430
649
|
break
|
|
431
|
-
case 'select-none':
|
|
432
|
-
|
|
650
|
+
case 'select-none': {
|
|
651
|
+
// Legacy: deselect all.
|
|
652
|
+
for (const cb of allCheckboxes()) {
|
|
653
|
+
if (cb.disabled) continue
|
|
654
|
+
if (cb.checked) {
|
|
655
|
+
cb.checked = false
|
|
656
|
+
post({ type: 'deselect', findingId: cb.dataset.findingId, timestamp: Date.now() })
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
updateSelectedCount()
|
|
433
660
|
break
|
|
661
|
+
}
|
|
434
662
|
case 'clear-filters':
|
|
435
|
-
|
|
663
|
+
// Legacy; no-op in new layout (filter-sev handles individual toggles).
|
|
436
664
|
break
|
|
437
665
|
}
|
|
438
666
|
})
|
|
439
667
|
|
|
440
|
-
const search = document.querySelector('[data-role="search"]')
|
|
441
|
-
if (search instanceof HTMLInputElement) {
|
|
442
|
-
search.addEventListener('input', () => {
|
|
443
|
-
filters.search = search.value
|
|
444
|
-
applyFilters()
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
|
|
448
668
|
document.addEventListener('keydown', (event) => {
|
|
449
669
|
const t = event.target
|
|
450
670
|
const isTyping = t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement
|
|
@@ -455,38 +675,28 @@
|
|
|
455
675
|
switch (event.key) {
|
|
456
676
|
case 'j':
|
|
457
677
|
event.preventDefault()
|
|
458
|
-
|
|
678
|
+
navigateFinding(1)
|
|
459
679
|
break
|
|
460
680
|
case 'k':
|
|
461
681
|
event.preventDefault()
|
|
462
|
-
|
|
682
|
+
navigateFinding(-1)
|
|
463
683
|
break
|
|
464
684
|
case 'x':
|
|
465
685
|
case ' ':
|
|
466
686
|
event.preventDefault()
|
|
467
|
-
|
|
687
|
+
toggleFocusedFinding()
|
|
468
688
|
break
|
|
469
|
-
case 'a':
|
|
470
|
-
event.preventDefault()
|
|
471
|
-
selectMany((card) => !card.classList.contains('is-hidden'))
|
|
472
|
-
break
|
|
473
|
-
case 'n':
|
|
474
|
-
event.preventDefault()
|
|
475
|
-
selectNone()
|
|
476
|
-
break
|
|
477
|
-
case '/': {
|
|
478
|
-
event.preventDefault()
|
|
479
|
-
const s = document.querySelector('[data-role="search"]')
|
|
480
|
-
if (s instanceof HTMLInputElement) s.focus()
|
|
481
|
-
break
|
|
482
|
-
}
|
|
483
689
|
case 'Escape':
|
|
484
690
|
event.preventDefault()
|
|
485
691
|
if (isConfirmOpen()) {
|
|
486
692
|
closeConfirm()
|
|
487
693
|
} else {
|
|
488
|
-
|
|
489
|
-
|
|
694
|
+
// Clear focused annotation.
|
|
695
|
+
for (const el of document.querySelectorAll('.annot.is-focused')) {
|
|
696
|
+
el.classList.remove('is-focused')
|
|
697
|
+
}
|
|
698
|
+
const path = document.body.dataset.selectedFile ?? ''
|
|
699
|
+
fileFindingIdx.set(path, -1)
|
|
490
700
|
}
|
|
491
701
|
break
|
|
492
702
|
}
|