@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.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/skills/magpie/README.md +16 -1
  4. package/skills/magpie/bin/magpie.ts +60 -2
  5. package/skills/magpie/fixtures/example-pr/diff.patch +86 -0
  6. package/skills/magpie/fixtures/example-pr/findings.final.json +137 -0
  7. package/skills/magpie/fixtures/example-pr/post-status.json +5 -0
  8. package/skills/magpie/fixtures/example-pr/pr.json +21 -0
  9. package/skills/magpie/fixtures/fake-gh.sh +10 -1
  10. package/skills/magpie/package.json +1 -1
  11. package/skills/magpie/scripts/__tests__/cli.test.ts +119 -0
  12. package/skills/magpie/scripts/__tests__/diff-utils.test.ts +106 -0
  13. package/skills/magpie/scripts/__tests__/file-tree.test.ts +97 -0
  14. package/skills/magpie/scripts/__tests__/gh.test.ts +8 -0
  15. package/skills/magpie/scripts/__tests__/helper.test.ts +63 -0
  16. package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +10 -0
  17. package/skills/magpie/scripts/__tests__/post-cmd.test.ts +90 -1
  18. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +210 -0
  19. package/skills/magpie/scripts/__tests__/refresh.test.ts +2 -2
  20. package/skills/magpie/scripts/__tests__/render-action-bar.test.ts +66 -0
  21. package/skills/magpie/scripts/__tests__/render-annotation.test.ts +67 -0
  22. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +46 -0
  23. package/skills/magpie/scripts/__tests__/render-diff.test.ts +84 -0
  24. package/skills/magpie/scripts/__tests__/render-file-tree.test.ts +54 -0
  25. package/skills/magpie/scripts/__tests__/render-findings.test.ts +104 -182
  26. package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +67 -0
  27. package/skills/magpie/scripts/__tests__/render-progress.test.ts +86 -0
  28. package/skills/magpie/scripts/__tests__/server.test.ts +54 -0
  29. package/skills/magpie/scripts/__tests__/types.test.ts +48 -3
  30. package/skills/magpie/scripts/diff-utils.ts +129 -0
  31. package/skills/magpie/scripts/file-tree.ts +74 -0
  32. package/skills/magpie/scripts/gh.ts +1 -0
  33. package/skills/magpie/scripts/helper.js +402 -192
  34. package/skills/magpie/scripts/post-cmd.ts +202 -0
  35. package/skills/magpie/scripts/preview-cmd.ts +300 -0
  36. package/skills/magpie/scripts/refresh.ts +16 -1
  37. package/skills/magpie/scripts/render-action-bar.ts +51 -0
  38. package/skills/magpie/scripts/render-annotation.ts +96 -0
  39. package/skills/magpie/scripts/render-cmd.ts +24 -1
  40. package/skills/magpie/scripts/render-diff.ts +125 -0
  41. package/skills/magpie/scripts/render-file-tree.ts +86 -0
  42. package/skills/magpie/scripts/render-findings.ts +165 -196
  43. package/skills/magpie/scripts/render-issues-list.ts +78 -0
  44. package/skills/magpie/scripts/render-progress.ts +74 -32
  45. package/skills/magpie/scripts/server.ts +43 -0
  46. package/skills/magpie/scripts/types.ts +13 -0
  47. package/skills/magpie/skill.json +4 -2
  48. package/skills/magpie/templates/styles.css +1288 -629
@@ -99,7 +99,30 @@ export async function runRender(runDir: string, page: 'progress' | 'findings'):
99
99
  parseFinding(raw),
100
100
  )
101
101
  const postStatus = await readJson<PostStatusMap>(join(runDir, 'post-status.json'), {})
102
+ const prJson = await readJson<Record<string, unknown>>(join(runDir, 'pr.json'), {})
103
+ const prNumber = Number(prJson.number ?? 0)
104
+ const pr =
105
+ prNumber > 0
106
+ ? {
107
+ number: prNumber,
108
+ branch: String(prJson.headRefName ?? '?'),
109
+ headSha: String(prJson.headRefOid ?? '?'),
110
+ }
111
+ : undefined
112
+ const filesArray = Array.isArray(prJson.files) ? (prJson.files as unknown[]) : []
113
+ const files = filesArray.map((f) => {
114
+ const entry = f as Record<string, unknown>
115
+ return {
116
+ path: String(entry.path ?? ''),
117
+ additions: Number(entry.additions ?? 0),
118
+ deletions: Number(entry.deletions ?? 0),
119
+ }
120
+ })
121
+ const diff = await readFile(join(runDir, 'diff.patch'), 'utf8').catch(() => '')
102
122
  const outPath = await nextVersionedPath(screenDir, 'findings')
103
- await renderFindingsToDisk({ findings, postStatus, runId: basename(runDir) }, outPath)
123
+ await renderFindingsToDisk(
124
+ { findings, postStatus, runId: basename(runDir), pr, files, diff },
125
+ outPath,
126
+ )
104
127
  return 0
105
128
  }
@@ -0,0 +1,125 @@
1
+ import { buildPairedLines, type DiffHunk, type DiffLine, type PairedLine } from './diff-utils.ts'
2
+ import { renderAnnotation } from './render-annotation.ts'
3
+ import type { PostStatusEntry, PostStatusMap, ReviewFinding } from './types.ts'
4
+
5
+ function esc(s: string): string {
6
+ return s.replace(/[&<>"']/g, (c) => {
7
+ const m: Record<string, string> = {
8
+ '&': '&amp;',
9
+ '<': '&lt;',
10
+ '>': '&gt;',
11
+ '"': '&quot;',
12
+ "'": '&#39;',
13
+ }
14
+ return m[c] ?? c
15
+ })
16
+ }
17
+
18
+ export type RenderDiffInput = {
19
+ hunks: DiffHunk[]
20
+ findings: ReviewFinding[]
21
+ postStatus: PostStatusMap
22
+ selectedIds: Set<string>
23
+ }
24
+
25
+ function isPosted(status: PostStatusEntry | undefined): boolean {
26
+ return status === 'posted'
27
+ }
28
+
29
+ function failedFrom(status: PostStatusEntry | undefined): { message: string } | undefined {
30
+ if (status && typeof status === 'object' && status.status === 'failed') {
31
+ return { message: status.message }
32
+ }
33
+ return undefined
34
+ }
35
+
36
+ function lineRow(line: DiffLine): string {
37
+ const cls = line.type === 'added' ? 'added' : line.type === 'removed' ? 'removed' : 'context'
38
+ const lineNo = line.type === 'removed' ? line.oldLineNo : line.newLineNo
39
+ const sign = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '
40
+ return `<div class="diff-row ${cls}"><span class="gutter">${lineNo ?? ''}</span><span class="sign">${sign}</span><span class="content">${esc(line.content)}</span></div>`
41
+ }
42
+
43
+ function findingsByLine(findings: ReviewFinding[]): Map<number, ReviewFinding[]> {
44
+ const m = new Map<number, ReviewFinding[]>()
45
+ for (const f of findings) {
46
+ if (f.line == null) continue
47
+ const arr = m.get(f.line) ?? []
48
+ arr.push(f)
49
+ m.set(f.line, arr)
50
+ }
51
+ return m
52
+ }
53
+
54
+ export function renderUnifiedDiff(input: RenderDiffInput): string {
55
+ const { hunks, findings, postStatus, selectedIds } = input
56
+ if (hunks.length === 0) return `<div class="diff-empty">No changes</div>`
57
+ const byLine = findingsByLine(findings)
58
+ const parts: string[] = []
59
+ hunks.forEach((h, hi) => {
60
+ if (hi > 0) parts.push(`<div class="diff-hunk-sep">⋯</div>`)
61
+ for (const line of h.lines) {
62
+ parts.push(lineRow(line))
63
+ const targetLine = line.newLineNo ?? null
64
+ if (targetLine != null) {
65
+ const fs = byLine.get(targetLine)
66
+ if (fs) {
67
+ for (const f of fs) {
68
+ parts.push(
69
+ renderAnnotation(f, {
70
+ checked: selectedIds.has(f.id),
71
+ posted: isPosted(postStatus[f.id]),
72
+ failed: failedFrom(postStatus[f.id]),
73
+ asCard: false,
74
+ }),
75
+ )
76
+ }
77
+ }
78
+ }
79
+ }
80
+ })
81
+ return parts.join('\n')
82
+ }
83
+
84
+ function pairedRow(pair: PairedLine): string {
85
+ const cellL =
86
+ pair.left == null
87
+ ? `<div class="diff-cell empty"></div>`
88
+ : `<div class="diff-cell ${pair.left.type}"><span class="gutter">${pair.left.oldLineNo ?? pair.left.newLineNo ?? ''}</span><span class="sign">${pair.left.type === 'removed' ? '-' : ' '}</span><span class="content">${esc(pair.left.content)}</span></div>`
89
+ const cellR =
90
+ pair.right == null
91
+ ? `<div class="diff-cell empty"></div>`
92
+ : `<div class="diff-cell ${pair.right.type}"><span class="gutter">${pair.right.newLineNo ?? pair.right.oldLineNo ?? ''}</span><span class="sign">${pair.right.type === 'added' ? '+' : ' '}</span><span class="content">${esc(pair.right.content)}</span></div>`
93
+ return `<div class="diff-row split">${cellL}${cellR}</div>`
94
+ }
95
+
96
+ export function renderSplitDiff(input: RenderDiffInput): string {
97
+ const { hunks, findings, postStatus, selectedIds } = input
98
+ if (hunks.length === 0) return `<div class="diff-empty">No changes</div>`
99
+ const byLine = findingsByLine(findings)
100
+ const parts: string[] = []
101
+ hunks.forEach((h, hi) => {
102
+ if (hi > 0) parts.push(`<div class="diff-hunk-sep">⋯</div>`)
103
+ const pairs = buildPairedLines([h])
104
+ for (const pair of pairs) {
105
+ parts.push(pairedRow(pair))
106
+ const target = pair.right?.newLineNo ?? null
107
+ if (target != null) {
108
+ const fs = byLine.get(target)
109
+ if (fs) {
110
+ for (const f of fs) {
111
+ parts.push(
112
+ renderAnnotation(f, {
113
+ checked: selectedIds.has(f.id),
114
+ posted: isPosted(postStatus[f.id]),
115
+ failed: failedFrom(postStatus[f.id]),
116
+ asCard: false,
117
+ }),
118
+ )
119
+ }
120
+ }
121
+ }
122
+ }
123
+ })
124
+ return parts.join('\n')
125
+ }
@@ -0,0 +1,86 @@
1
+ import {
2
+ buildTree,
3
+ collapseTree,
4
+ countDirFindings,
5
+ type DirNode,
6
+ findingCountsBySeverity,
7
+ } from './file-tree.ts'
8
+ import type { PrFileEntry, ReviewFinding } from './types.ts'
9
+
10
+ function esc(s: string): string {
11
+ return s.replace(/[&<>"']/g, (c) => {
12
+ const m: Record<string, string> = {
13
+ '&': '&amp;',
14
+ '<': '&lt;',
15
+ '>': '&gt;',
16
+ '"': '&quot;',
17
+ "'": '&#39;',
18
+ }
19
+ return m[c] ?? c
20
+ })
21
+ }
22
+
23
+ function pipsHtml(findings: ReviewFinding[], path: string): string {
24
+ const counts = findingCountsBySeverity(findings, path)
25
+ if (counts.length === 0) return ''
26
+ return counts
27
+ .map(
28
+ (c) =>
29
+ `<span class="sev-pip ${c.severity}" aria-label="${c.count} ${c.severity}">${c.count}</span>`,
30
+ )
31
+ .join('')
32
+ }
33
+
34
+ function deltaHtml(f: PrFileEntry): string {
35
+ return `<span class="delta" aria-label="${f.additions} additions, ${f.deletions} deletions"><span class="add">+${f.additions}</span> <span class="del">-${f.deletions}</span></span>`
36
+ }
37
+
38
+ function renderFiles(files: PrFileEntry[], findings: ReviewFinding[], depth: number): string {
39
+ return [...files]
40
+ .sort((a, b) => a.path.localeCompare(b.path))
41
+ .map((file) => {
42
+ const fileName = file.path.split('/').pop() ?? file.path
43
+ return `<button type="button" class="tree-file" data-action="select-file" data-file="${esc(file.path)}" style="padding-left:${depth * 12 + 12}px">
44
+ <span class="name">${esc(fileName)}</span>
45
+ ${pipsHtml(findings, file.path)}
46
+ ${deltaHtml(file)}
47
+ </button>`
48
+ })
49
+ .join('')
50
+ }
51
+
52
+ function renderDirs(dirs: Map<string, DirNode>, findings: ReviewFinding[], depth: number): string {
53
+ return [...dirs.values()]
54
+ .sort((a, b) => a.name.localeCompare(b.name))
55
+ .map((dir) => {
56
+ const totalCount = countDirFindings(findings, dir)
57
+ const countBadge = totalCount > 0 ? `<span class="dir-count">${totalCount}</span>` : ''
58
+ return `<details class="tree-dir" open data-dir="${esc(dir.fullPath)}">
59
+ <summary style="padding-left:${depth * 12 + 6}px"><span class="chev"></span><span class="name">${esc(dir.name)}</span>${countBadge}</summary>
60
+ ${renderDirs(dir.dirs, findings, depth + 1)}
61
+ ${renderFiles(dir.files, findings, depth + 1)}
62
+ </details>`
63
+ })
64
+ .join('')
65
+ }
66
+
67
+ export type RenderFileTreeInput = {
68
+ files: PrFileEntry[]
69
+ findings: ReviewFinding[]
70
+ selectedFile?: string | null
71
+ }
72
+
73
+ export function renderFileTree(input: RenderFileTreeInput): string {
74
+ const tree = collapseTree(buildTree(input.files))
75
+ const generalCount = input.findings.filter((f) => !f.file).length
76
+ const overviewBadge = generalCount > 0 ? `<span class="dir-count">${generalCount}</span>` : ''
77
+ return `<aside class="file-rail" data-role="file-tree">
78
+ <button type="button" class="tree-overview" data-action="select-file" data-file="">
79
+ <span class="name">Overview</span>${overviewBadge}
80
+ </button>
81
+ <div class="tree-body">
82
+ ${renderDirs(tree.dirs, input.findings, 0)}
83
+ ${renderFiles(tree.files, input.findings, 0)}
84
+ </div>
85
+ </aside>`
86
+ }