@shadowclone/skills 0.0.0-stage → 0.0.30

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 (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/package.json +15 -4
  4. package/skills/choose-by-consequence/SKILL.md +66 -0
  5. package/skills/design-deep-modules/SKILL.md +51 -0
  6. package/skills/diagnose-before-editing/SKILL.md +58 -0
  7. package/skills/guided-pr-review/SKILL.md +87 -0
  8. package/skills/guided-pr-review/assets/document.mjs +197 -0
  9. package/skills/guided-pr-review/assets/guide.mjs +196 -0
  10. package/skills/guided-pr-review/assets/notes.mjs +195 -0
  11. package/skills/guided-pr-review/assets/presentation.mjs +180 -0
  12. package/skills/guided-pr-review/assets/review.html +18 -0
  13. package/skills/guided-pr-review/references/report.md +118 -0
  14. package/skills/guided-pr-review/scripts/build.mjs +117 -0
  15. package/skills/guided-pr-review/scripts/check.mjs +193 -0
  16. package/skills/guided-pr-review/scripts/format.mjs +198 -0
  17. package/skills/guided-pr-review/scripts/git.mjs +199 -0
  18. package/skills/guided-pr-review/scripts/render.mjs +195 -0
  19. package/skills/plan-with-review-page/SKILL.md +73 -0
  20. package/skills/plan-with-review-page/assets/review-page.html +160 -0
  21. package/skills/plan-with-review-page/scripts/validate-page.mjs +158 -0
  22. package/skills/research-primary-sources/SKILL.md +51 -0
  23. package/skills/resolve-conflicts-by-intent/SKILL.md +53 -0
  24. package/skills/scope-confirmed-changes/SKILL.md +63 -0
  25. package/skills/shadowclone-review/SKILL.md +76 -0
  26. package/skills/shadowclone-work/SKILL.md +47 -0
  27. package/skills/tests-that-catch-bugs/SKILL.md +73 -0
  28. package/skills/typescript-type-safety/SKILL.md +52 -0
  29. package/skills/verify-and-review/SKILL.md +58 -0
  30. package/skills/verify-review-findings/SKILL.md +59 -0
  31. package/skills/write-plain-english/SKILL.md +69 -0
  32. package/skills/write-plain-english/scripts/check-ste.mjs +200 -0
@@ -0,0 +1,196 @@
1
+ const review = JSON.parse(document.getElementById("review-data").textContent);
2
+ const initialAnnotations = structuredClone(review.annotations);
3
+ let dirty = false;
4
+ let showFindings = false;
5
+ let currentStep = 0;
6
+ let currentPr = review.combined ? "stack" : review.pulls[0].id;
7
+ let mode = "guide";
8
+
9
+ function renderFileList() {
10
+ const search = document.getElementById("file-search").value.toLowerCase();
11
+ const files = review.fileOrder
12
+ .map(getFile)
13
+ .filter((file) => currentPr === "stack" || currentPr === file.pr || review.pulls.length === 1)
14
+ .filter((file) => file.path.toLowerCase().includes(search));
15
+ document.getElementById("file-list").innerHTML = files
16
+ .map((file) => {
17
+ const pull = review.pulls.find((pull) => pull.id === file.pr);
18
+ return fileCard({ file, open: true, prLabel: "#" + pull.number, url: pull.url });
19
+ })
20
+ .join("");
21
+ document
22
+ .querySelectorAll("#file-list .file-card")
23
+ .forEach((card) => loadCard({ card, deferNotes: true }));
24
+ renderNotes();
25
+ }
26
+
27
+ function refreshGuide() {
28
+ document.getElementById("guide").hidden = mode !== "guide";
29
+ document.getElementById("files").hidden = mode !== "files";
30
+ document
31
+ .querySelectorAll("[data-mode]")
32
+ .forEach((button) => button.classList.toggle("selected", button.dataset.mode === mode));
33
+ document
34
+ .querySelectorAll("[data-pr]")
35
+ .forEach((button) => button.classList.toggle("selected", button.dataset.pr === currentPr));
36
+ document
37
+ .querySelectorAll(".route-step")
38
+ .forEach((button) =>
39
+ button.classList.toggle("selected", Number(button.dataset.step) === currentStep),
40
+ );
41
+ document.querySelectorAll(".reading-step").forEach((element, index) => {
42
+ element.hidden = index !== currentStep;
43
+ element.querySelectorAll(".file-card").forEach((card) => {
44
+ const file = getFile(card.dataset.file);
45
+ card.hidden = file.pr !== currentPr;
46
+ if (!card.hidden && !element.hidden && card.open && card.dataset.loaded !== "true")
47
+ loadCard({ card, deferNotes: true });
48
+ });
49
+ });
50
+ const previous = document.getElementById("previous");
51
+ const next = document.getElementById("next");
52
+ previous.disabled = currentStep === 0;
53
+ next.disabled = currentStep === review.groups.length - 1;
54
+ previous.textContent = currentStep ? "← " + review.groups[currentStep - 1].title : "First step";
55
+ next.textContent =
56
+ currentStep < review.groups.length - 1
57
+ ? review.groups[currentStep + 1].title + " →"
58
+ : "Last step";
59
+ document.querySelectorAll("[data-reviewed]").forEach((input) => {
60
+ input.checked = review.annotations.reviewed.includes(input.dataset.reviewed);
61
+ });
62
+ if (mode === "files") renderFileList();
63
+ renderNotes();
64
+ }
65
+
66
+ function selectStep(index) {
67
+ currentStep = Math.max(0, Math.min(review.groups.length - 1, index));
68
+ mode = "guide";
69
+ document.getElementById("selection-comment").hidden = true;
70
+ refreshGuide();
71
+ const step = document.getElementById("step-" + review.groups[currentStep].id);
72
+ step.scrollIntoView({ block: "start" });
73
+ }
74
+
75
+ function showSource(id) {
76
+ const item = review.evidence.find((item) => item.id === id);
77
+ const number = review.pulls.find((pull) => pull.id === item.pr).number;
78
+ document.getElementById("source-title").textContent =
79
+ item.path + ":" + item.start + (item.end === item.start ? "" : " to " + item.end);
80
+ document.getElementById("source-pin").textContent =
81
+ `PR #${number} · ${item.side} · ${item.sha.slice(0, 12)}`;
82
+ const file = getFile("evidence:" + id);
83
+ document.getElementById("source-code").innerHTML = fileMarkup({
84
+ file,
85
+ pulls: review.pulls,
86
+ sha: file.sha,
87
+ });
88
+ setSourceLink({
89
+ element: document.getElementById("source-link"),
90
+ url: item.url,
91
+ });
92
+ document.getElementById("source-dialog").showModal();
93
+ }
94
+
95
+ function pageAction(event) {
96
+ const button = event.target.closest("button");
97
+ if (!button) return;
98
+ if (button.dataset.step) selectStep(Number(button.dataset.step));
99
+ if (button.dataset.pr) {
100
+ currentPr = button.dataset.pr;
101
+ refreshGuide();
102
+ }
103
+ if (button.dataset.mode) {
104
+ mode = button.dataset.mode;
105
+ refreshGuide();
106
+ }
107
+ if (button.dataset.view)
108
+ loadCard({ card: button.closest(".file-card"), view: button.dataset.view });
109
+ if (button.dataset.evidence) showSource(button.dataset.evidence);
110
+ notesAction(event);
111
+ }
112
+
113
+ try {
114
+ const cached = JSON.parse(localStorage.getItem("guided-review-v2:" + review.id) ?? "null");
115
+ if (
116
+ cached &&
117
+ cached.revision > review.annotations.revision &&
118
+ Array.isArray(cached.notes) &&
119
+ cached.notes.every(
120
+ (note) =>
121
+ review.pulls.some((pull) => pull.id === note.pr) &&
122
+ typeof note.body === "string" &&
123
+ typeof note.quote === "string" &&
124
+ Array.isArray(note.ranges),
125
+ )
126
+ ) {
127
+ review.annotations = cached;
128
+ dirty = true;
129
+ document.getElementById("save-status").textContent = "Recovered unsaved changes";
130
+ }
131
+ } catch {
132
+ document.getElementById("save-status").textContent =
133
+ "Browser recovery is unavailable. Use Save review before closing.";
134
+ }
135
+ document.addEventListener("click", pageAction);
136
+ document.addEventListener("pointerdown", startLineSelection);
137
+ document.addEventListener("pointerup", (event) => {
138
+ finishLineSelection(event);
139
+ captureSelection(event);
140
+ });
141
+ document.addEventListener(
142
+ "toggle",
143
+ (event) => {
144
+ if (
145
+ event.target.matches(".file-card") &&
146
+ event.target.open &&
147
+ !event.target.hidden &&
148
+ !event.target.closest(".reading-step")?.hidden &&
149
+ event.target.dataset.loaded !== "true"
150
+ )
151
+ loadCard({ card: event.target });
152
+ },
153
+ true,
154
+ );
155
+ document.addEventListener("change", (event) => {
156
+ if (!event.target.dataset.reviewed) return;
157
+ const id = event.target.dataset.reviewed;
158
+ review.annotations.reviewed = event.target.checked
159
+ ? [...new Set([...review.annotations.reviewed, id])]
160
+ : review.annotations.reviewed.filter((value) => value !== id);
161
+ changed();
162
+ });
163
+ document.getElementById("toggle-findings").onclick = () => showReviewPanel("findings");
164
+ document.getElementById("toggle-notes").onclick = () => showReviewPanel("notes");
165
+ document.getElementById("close-notes").onclick = () => {
166
+ document.getElementById("notes-panel").hidden = true;
167
+ };
168
+ document.getElementById("cancel-note").onclick = () => {
169
+ document.getElementById("composer").hidden = true;
170
+ };
171
+ document.getElementById("selection-comment").onclick = () => openComposer();
172
+ document.getElementById("note-form").onsubmit = saveNote;
173
+ document.getElementById("save-review").onclick = saveReview;
174
+ document.getElementById("close-source").onclick = () => {
175
+ document.getElementById("source-dialog").close();
176
+ };
177
+ document.getElementById("source-dialog").onclose = () => {
178
+ document.body.append(
179
+ document.getElementById("composer"),
180
+ document.getElementById("selection-comment"),
181
+ );
182
+ };
183
+ document.getElementById("previous").onclick = () => selectStep(currentStep - 1);
184
+ document.getElementById("next").onclick = () => selectStep(currentStep + 1);
185
+ document.getElementById("file-search").oninput = renderFileList;
186
+ window.addEventListener("beforeunload", (event) => {
187
+ if (
188
+ dirty ||
189
+ (!document.getElementById("composer").hidden &&
190
+ document.getElementById("note-body").value.trim())
191
+ ) {
192
+ event.preventDefault();
193
+ event.returnValue = "";
194
+ }
195
+ });
196
+ refreshGuide();
@@ -0,0 +1,195 @@
1
+ let selectedContext = null;
2
+ let editingNote = null;
3
+ let dragStart = null;
4
+
5
+ function showReviewPanel(view) {
6
+ const panel = document.getElementById("notes-panel");
7
+ panel.dataset.view = view;
8
+ panel.hidden = false;
9
+ panel.scrollTop = 0;
10
+ showFindings = view === "findings";
11
+ document.getElementById("toggle-findings").classList.toggle("selected", showFindings);
12
+ renderNotes();
13
+ }
14
+
15
+ function changed() {
16
+ review.annotations.revision += 1;
17
+ dirty = true;
18
+ document.getElementById("save-status").textContent = "Unsaved changes";
19
+ try {
20
+ localStorage.setItem("guided-review-v2:" + review.id, JSON.stringify(review.annotations));
21
+ } catch {
22
+ document.getElementById("save-status").textContent =
23
+ "Unsaved changes. This browser cannot keep a recovery copy.";
24
+ }
25
+ }
26
+
27
+ function renderNotes() {
28
+ document.querySelectorAll(".inline-notes").forEach((element) => {
29
+ element.innerHTML = review.annotations.notes
30
+ .filter((note) => !note.dismissed && note.anchor === element.dataset.notesAt)
31
+ .map(noteMarkup)
32
+ .join("");
33
+ });
34
+ document.querySelectorAll(".group-findings").forEach((element) => {
35
+ const group = review.groups.find((group) => group.id === element.dataset.findingsFor);
36
+ const remaining = [];
37
+ for (const finding of review.pulls.flatMap((pull) => pull.findings)) {
38
+ if (!group.files.some((file) => file.pr === finding.pr && file.path === finding.path))
39
+ continue;
40
+ const rows = [...element.closest(".reading-step").querySelectorAll(".code-line")];
41
+ const row = rows.find(
42
+ (row) =>
43
+ row.dataset.file === finding.pr + ":" + finding.path &&
44
+ row.dataset.side === "HEAD" &&
45
+ Number(row.dataset.line) === finding.line,
46
+ );
47
+ if (showFindings && row && row.closest(".file-card")?.hidden !== true)
48
+ row.nextElementSibling.insertAdjacentHTML("beforeend", findingMarkup(finding));
49
+ else remaining.push(finding);
50
+ }
51
+ element.hidden = !showFindings;
52
+ element.innerHTML = remaining.map(findingMarkup).join("");
53
+ });
54
+ document.getElementById("note-count").textContent = review.annotations.notes.filter(
55
+ (note) => !note.dismissed,
56
+ ).length;
57
+ document.getElementById("notes-list").innerHTML = review.annotations.notes.length
58
+ ? review.annotations.notes.map(noteMarkup).join("")
59
+ : '<p class="muted">Select code or an explanation, then choose Comment.</p>';
60
+ document.getElementById("findings-heading").hidden = !showFindings;
61
+ document.getElementById("findings-list").innerHTML = showFindings
62
+ ? review.pulls
63
+ .flatMap((pull) => pull.findings)
64
+ .map(findingMarkup)
65
+ .join("")
66
+ : "";
67
+ }
68
+
69
+ function captureSelection(event) {
70
+ if (!document.getElementById("composer").hidden) return;
71
+ if (event.target.closest("#composer, #notes-panel, .note, .finding, button, textarea, select"))
72
+ return;
73
+ const selection = window.getSelection();
74
+ if (!selection?.rangeCount || !selection.toString().trim()) return;
75
+ const range = selection.getRangeAt(0);
76
+ const start = selection.anchorNode.parentElement;
77
+ const end = selection.focusNode.parentElement;
78
+ const card = start.closest(".file-card, .source-card, #source-dialog");
79
+ const rows =
80
+ card && end.closest(".file-card, .source-card, #source-dialog") === card
81
+ ? [...card.querySelectorAll(".code-line")].filter((row) =>
82
+ range.intersectsNode(row.querySelector("code")),
83
+ )
84
+ : [];
85
+ if (rows.length) selectedContext = codeContext(rows);
86
+ else {
87
+ const passage = start.closest(".passage");
88
+ if (!passage || end.closest(".passage") !== passage) return;
89
+ selectedContext = {
90
+ anchor: passage.dataset.anchor,
91
+ quote: selection.toString(),
92
+ ranges: [],
93
+ prs: passage.dataset.prs?.split(",") ?? review.pulls.map((pull) => pull.id),
94
+ };
95
+ }
96
+ if (!selectedContext) return;
97
+ const button = document.getElementById("selection-comment");
98
+ const dialog = start.closest("dialog");
99
+ (dialog ?? document.body).append(button);
100
+ button.hidden = false;
101
+ const bounds = range.getBoundingClientRect();
102
+ button.style.left = Math.max(12, Math.min(bounds.left, innerWidth - 110)) + "px";
103
+ button.style.top = Math.max(70, bounds.top - 40) + "px";
104
+ }
105
+
106
+ function openComposer(note) {
107
+ if (!document.getElementById("composer").hidden) return;
108
+ editingNote = note?.id ?? null;
109
+ if (note)
110
+ selectedContext = {
111
+ anchor: note.anchor,
112
+ quote: note.quote,
113
+ ranges: note.ranges,
114
+ prs: [note.pr],
115
+ };
116
+ if (!selectedContext) return;
117
+ const composer = document.getElementById("composer");
118
+ (document.getElementById("source-dialog").open
119
+ ? document.getElementById("source-dialog")
120
+ : document.body
121
+ ).append(composer);
122
+ document.getElementById("selection-comment").hidden = true;
123
+ document.getElementById("selected-quote").textContent = selectedContext.quote;
124
+ document.getElementById("target-pr").innerHTML =
125
+ (selectedContext.prs.length > 1 ? '<option value="">Choose a PR</option>' : "") +
126
+ selectedContext.prs
127
+ .map(
128
+ (id) =>
129
+ '<option value="' +
130
+ id +
131
+ '">#' +
132
+ review.pulls.find((pull) => pull.id === id).number +
133
+ "</option>",
134
+ )
135
+ .join("");
136
+ document.getElementById("target-label").hidden = selectedContext.prs.length === 1;
137
+ document.getElementById("note-body").value = note?.body ?? "";
138
+ composer.hidden = false;
139
+ document.getElementById("note-body").focus();
140
+ }
141
+
142
+ function notesAction(event) {
143
+ const button = event.target.closest("button");
144
+ if (!button) return;
145
+ const note = review.annotations.notes.find(
146
+ (note) => note.id === (button.dataset.editNote ?? button.dataset.dismissNote),
147
+ );
148
+ if (button.dataset.editNote && note) openComposer(note);
149
+ if (button.dataset.dismissNote && note) {
150
+ note.dismissed = !note.dismissed;
151
+ changed();
152
+ renderNotes();
153
+ }
154
+ const finding = button.dataset.includeFinding ?? button.dataset.dismissFinding;
155
+ if (finding) {
156
+ const state = review.annotations.decisions[finding];
157
+ review.annotations.decisions[finding] = button.dataset.includeFinding
158
+ ? state === "included"
159
+ ? null
160
+ : "included"
161
+ : state === "dismissed"
162
+ ? null
163
+ : "dismissed";
164
+ changed();
165
+ renderNotes();
166
+ }
167
+ }
168
+
169
+ function startLineSelection(event) {
170
+ if (!document.getElementById("composer").hidden) return;
171
+ if (!event.target.closest(".line-number")) return;
172
+ event.preventDefault();
173
+ dragStart = event.target.closest(".code-line");
174
+ }
175
+
176
+ function finishLineSelection(event) {
177
+ if (!dragStart) return;
178
+ const end = event.target.closest(".code-line");
179
+ if (
180
+ end &&
181
+ dragStart.closest(".file-card") === end.closest(".file-card") &&
182
+ dragStart.dataset.file === end.dataset.file
183
+ ) {
184
+ const parent = dragStart.closest(".code-lines");
185
+ const rows = [...parent.querySelectorAll(".code-line")];
186
+ const indices = [rows.indexOf(dragStart), rows.indexOf(end)].sort(
187
+ (first, second) => first - second,
188
+ );
189
+ selectedContext = codeContext(rows.slice(indices[0], indices[1] + 1));
190
+ const selectedRows = rows.slice(indices[0], indices[1] + 1);
191
+ rows.forEach((row) => row.classList.toggle("chosen", selectedRows.includes(row)));
192
+ openComposer();
193
+ }
194
+ dragStart = null;
195
+ }
@@ -0,0 +1,180 @@
1
+ import { escapeHtml, fileMarkup } from "../scripts/format.mjs";
2
+
3
+ export const reviewStyle = String.raw`:root { color-scheme: dark; --background: #090a10; --panel: #141620; --border: #303246; --text: #e5e4ed; --muted: #9c9bae; --accent: #b6a2ee; }
4
+ * { box-sizing: border-box; }
5
+ [hidden] { display: none !important; }
6
+ body { margin: 0; background: var(--background); color: var(--text); font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
7
+ button, input, textarea, select { font: inherit; color: inherit; }
8
+ button { cursor: pointer; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); padding: 7px 12px; }
9
+ button:hover, button.selected { border-color: var(--accent); color: var(--accent); }
10
+ button:disabled { cursor: default; opacity: .45; }
11
+ a, .source-link { color: var(--accent); }
12
+ input, textarea, select { background: var(--background); border: 1px solid var(--border); border-radius: 5px; padding: 8px; }
13
+ textarea { resize: vertical; }
14
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
15
+ header { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 24px; background: #10121b; border-bottom: 1px solid var(--border); }
16
+ .brand { white-space: nowrap; font-size: 14px; }
17
+ .brand span { color: var(--accent); }
18
+ .header-actions, .pr-tabs, .view-tabs, .file-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
19
+ .header-actions { font-size: 13px; }
20
+ #save-review { background: var(--accent); color: var(--background); }
21
+ #save-status { max-width: 260px; color: var(--muted); font-size: 12px; }
22
+ .document { display: grid; grid-template-columns: 236px minmax(0, 1fr); max-width: 1440px; margin: auto; }
23
+ .route { position: sticky; top: 64px; height: calc(100vh - 64px); overflow: auto; padding: 24px 14px; border-right: 1px solid var(--border); }
24
+ .route h2 { margin: 0 10px 16px; color: var(--muted); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
25
+ .route-step { display: flex; gap: 12px; width: 100%; margin: 4px 0; padding: 12px 10px; text-align: left; border-color: transparent; background: transparent; }
26
+ .route-step.selected { border-left: 2px solid var(--accent); background: var(--panel); }
27
+ .route-step small { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; }
28
+ .route-footer { margin: 28px 10px; color: var(--muted); font-size: 12px; }
29
+ main { min-width: 0; padding: 24px 40px 64px; }
30
+ .overview { margin-bottom: 28px; }
31
+ .overview h2 { margin: 0; font-size: 17px; }
32
+ .overview > p { margin: 8px 0 16px; color: var(--muted); font-size: 14px; }
33
+ .pr-tabs { margin: 16px 0; font-size: 12px; }
34
+ .view-tabs { margin: 12px 0; }
35
+ .view-tabs button { padding: 5px 12px; font-size: 12px; }
36
+ .pr-details { color: var(--muted); font-size: 12px; }
37
+ summary { cursor: pointer; }
38
+ .pr-details p { padding-left: 18px; }
39
+ .step-meta { display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 12px; }
40
+ h1 { margin: 14px 0 18px; font-size: 27px; line-height: 1.25; }
41
+ h3 { margin: 0 0 6px; font-size: 16px; }
42
+ p { margin: 10px 0; }
43
+ .why { padding-left: 14px; border-left: 2px solid var(--accent); }
44
+ .before-after { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 22px 0; }
45
+ .before-after > div { padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); }
46
+ .before-after span { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
47
+ .before-after p { margin-bottom: 0; font-size: 14px; }
48
+ .flow { display: flex; flex-wrap: wrap; gap: 10px; margin: 18px 0; }
49
+ .flow > div { flex: 1; min-width: 130px; padding: 12px; border: 1px solid #554875; border-radius: 7px; font-size: 13px; }
50
+ .flow span { color: var(--accent); }
51
+ .flow-proof, .sources { margin: 8px 0 18px; color: var(--muted); font-size: 12px; }
52
+ .source-link { padding: 0; border: 0; background: none; font-size: 12px; text-decoration: underline; }
53
+ .passage { margin: 20px 0; }
54
+ .step-files { margin: 24px 0; }
55
+ .file-card { margin: 16px 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
56
+ .file-card > summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: var(--panel); font: 12px/1.5 ui-monospace, monospace; overflow-wrap: anywhere; }
57
+ .file-stat { flex-shrink: 0; color: var(--muted); font-size: 11px; }
58
+ .file-stat b { color: #8fc9a5; }
59
+ .file-toolbar { padding: 10px 14px; border-top: 1px solid var(--border); font-size: 12px; }
60
+ .file-toolbar button { padding: 3px 10px; font-size: 12px; }
61
+ .file-toolbar a { margin-left: auto; }
62
+ .file-content { max-height: 540px; overflow: auto; background: #0b1011; }
63
+ .source-card { margin: 16px 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
64
+ .source-card figcaption { padding: 10px 14px; background: var(--panel); color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
65
+ .source-card figcaption a { float: right; margin-left: 12px; }
66
+ .source-card .code-lines { overflow: auto; background: #0b1011; }
67
+ .source-card .code-line { grid-template-columns: 40px 0 0 minmax(max-content, 1fr) auto; }
68
+ .source-card .code-line > span.line-number { visibility: hidden; padding: 0; }
69
+ .code-line { display: grid; grid-template-columns: 34px 34px 18px minmax(max-content, 1fr) auto; min-width: max-content; padding-right: 12px; font: 12px/1.8 ui-monospace, SFMono-Regular, monospace; }
70
+ .code-line.add { background: #10251e; }
71
+ .code-line.remove { background: #2b171c; }
72
+ .code-line.chosen { background: #332a4b; }
73
+ .line-number { padding: 0 6px; text-align: right; color: #76818a; border: 0; border-radius: 0; background: none; user-select: none; font: inherit; }
74
+ .line-number:hover { color: var(--accent); }
75
+ .sign, .owner { user-select: none; color: var(--muted); }
76
+ .owner { padding-left: 18px; font-size: 10px; }
77
+ .code-line code { white-space: pre; color: #c6d8d3; }
78
+ .hunk { padding: 6px 14px; background: #181827; color: var(--muted); font: 11px ui-monospace, monospace; }
79
+ .metadata, .markdown { margin: 0; padding: 16px; }
80
+ .markdown pre, pre { overflow: auto; font-size: 12px; }
81
+ code { font-family: ui-monospace, SFMono-Regular, monospace; }
82
+ .dry-run { margin: 16px 0; padding: 14px 16px; border: 1px solid var(--border); border-radius: 7px; }
83
+ .dry-run summary span { float: right; color: var(--muted); font-size: 11px; }
84
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
85
+ th, td { padding: 10px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border); }
86
+ th { color: var(--muted); font-size: 12px; }
87
+ .step-navigation { display: flex; justify-content: space-between; margin-top: 28px; }
88
+ .step-navigation button { max-width: 48%; text-align: left; }
89
+ .reading-step { scroll-margin-top: 80px; }
90
+ .note, .finding { margin: 12px 16px; padding: 14px; border-left: 3px solid var(--accent); border-radius: 6px; background: #211b31; font-size: 13px; }
91
+ .finding { background: #171d29; border-color: #879fc4; }
92
+ .note-meta { display: flex; gap: 10px; align-items: center; color: var(--muted); font-size: 11px; }
93
+ .note blockquote, .finding blockquote { margin: 8px 0; padding-left: 10px; color: var(--muted); border-left: 1px solid var(--border); white-space: pre-wrap; }
94
+ .note-actions { display: flex; gap: 8px; margin-top: 8px; }
95
+ .note-actions button { padding: 2px 8px; font-size: 11px; }
96
+ .finding.dismissed { opacity: .55; }
97
+ .finding.included { border-color: var(--accent); }
98
+ #notes-panel { position: fixed; z-index: 40; top: 64px; right: 0; bottom: 0; width: min(460px, 100vw); overflow: auto; padding: 24px 16px; background: #10121b; border-left: 1px solid var(--border); box-shadow: -12px 0 40px #0006; }
99
+ #notes-panel h2 { margin: 18px 14px 8px; font-size: 18px; }
100
+ .human-notes > p { margin: 12px 14px; color: var(--muted); font-size: 13px; }
101
+ #notes-panel[data-view="findings"] .human-notes, #notes-panel[data-view="notes"] .agent-findings { display: none; }
102
+ #close-notes { float: right; }
103
+ #composer { position: fixed; z-index: 60; right: 24px; top: 96px; width: min(380px, calc(100vw - 32px)); padding: 18px; border: 1px solid var(--accent); border-radius: 10px; background: var(--panel); box-shadow: 0 12px 60px #0008; }
104
+ .composer-title { display: flex; justify-content: space-between; align-items: center; }
105
+ #composer blockquote { max-height: 100px; overflow: auto; margin: 12px 0; padding-left: 10px; border-left: 2px solid var(--border); color: var(--muted); white-space: pre-wrap; font-size: 12px; }
106
+ #target-label { display: flex; gap: 8px; align-items: center; font-size: 12px; }
107
+ #composer textarea { width: 100%; min-height: 100px; margin: 12px 0; }
108
+ #selection-comment { position: fixed; z-index: 70; background: var(--accent); color: var(--background); padding: 4px 12px; box-shadow: 0 4px 16px #0008; }
109
+ dialog { position: fixed; width: min(820px, 90vw); max-height: 85vh; overflow: auto; padding: 24px; color: var(--text); background: var(--panel); border: 1px solid var(--border); border-radius: 8px; }
110
+ dialog::backdrop { background: #0008; }
111
+ dialog h2 { font-size: 17px; }
112
+ #close-source { float: right; }
113
+ #source-pin { color: var(--muted); font-size: 12px; }
114
+ #source-code { white-space: pre; }
115
+ #file-search { width: 100%; margin: 8px 0; }
116
+ .prototype { color: #e4c486; }
117
+ .muted { color: var(--muted); }
118
+ @media (max-width: 760px) {
119
+ header { padding: 10px 14px; flex-wrap: wrap; }
120
+ .document { display: block; }
121
+ .route { position: static; display: flex; height: auto; padding: 10px; overflow: auto; border-bottom: 1px solid var(--border); }
122
+ .route h2, .route-footer { display: none; }
123
+ .route-step { min-width: 180px; }
124
+ main { padding: 20px 16px 40px; }
125
+ .before-after { grid-template-columns: 1fr; }
126
+ .file-card > summary { align-items: flex-start; flex-direction: column; }
127
+ .header-actions { gap: 6px; }
128
+ #composer { right: 16px; top: 128px; }
129
+ }
130
+ `;
131
+
132
+ export function sourceExcerptMarkup(options) {
133
+ const { item, pulls } = options;
134
+ const pull = pulls.find((pull) => pull.id === item.pr);
135
+ const file = {
136
+ id: "evidence:" + item.id,
137
+ pr: item.pr,
138
+ rows: item.text.split("\n").map((text, index) => ({
139
+ kind: "context",
140
+ side: item.side,
141
+ text,
142
+ oldLine: item.start + index,
143
+ newLine: item.start + index,
144
+ owner: item.pr,
145
+ })),
146
+ };
147
+ return `<figure class="source-card"><figcaption>#${pull.number} · ${escapeHtml(item.path)}:${item.start} to ${item.end} · ${item.side}<a href="${escapeHtml(item.url)}" target="_blank" rel="noreferrer">Open pinned source</a></figcaption>${fileMarkup({ file, pulls, sha: { BASE: item.sha, HEAD: item.sha } })}</figure>`;
148
+ }
149
+
150
+ export function findingMarkup(finding) {
151
+ const state = review.annotations.decisions[finding.id];
152
+ const pull = review.pulls.find((pull) => pull.id === finding.pr);
153
+ const sourceUrl =
154
+ pull.url.replace(
155
+ /\/pull\/\d+$/,
156
+ "/blob/" + pull.headSha + "/" + finding.path.split("/").map(encodeURIComponent).join("/"),
157
+ ) +
158
+ "#L" +
159
+ finding.line;
160
+ const proof = finding.evidence
161
+ .map(
162
+ (item) =>
163
+ "<p><code>" +
164
+ escapeHtml(item.location) +
165
+ "</code></p><blockquote>" +
166
+ escapeHtml(item.quote) +
167
+ "</blockquote>",
168
+ )
169
+ .join("");
170
+ return `<aside class="finding ${escapeHtml(state ?? "")}" data-finding="${escapeHtml(finding.id)}"><div class="note-meta"><strong>Agent</strong><span>#${escapeHtml(pull.number)} · <a href="${escapeHtml(sourceUrl)}" target="_blank" rel="noreferrer">${escapeHtml(finding.path)}:${escapeHtml(finding.line)}</a></span><span>${state === "included" ? "in your review" : state === "dismissed" ? "dismissed" : "proposal"}</span></div><p>${escapeHtml(finding.explanation)}</p><details><summary>Inspect the reasoning</summary><p><strong>Trigger:</strong> ${escapeHtml(finding.failureScenario)}</p>${proof}<p><strong>Countercheck:</strong> ${escapeHtml(finding.refutation)}</p><p><strong>Suggested direction:</strong> ${escapeHtml(finding.suggestion ?? "Inspect the affected contract before choosing a fix.")}</p></details><div class="note-actions"><button data-include-finding="${escapeHtml(finding.id)}" type="button">${state === "included" ? "Remove from review" : "Add to my review"}</button><button data-dismiss-finding="${escapeHtml(finding.id)}" type="button">${state === "dismissed" ? "Restore" : "Dismiss"}</button></div></aside>`;
171
+ }
172
+
173
+ export function setSourceLink(options) {
174
+ options.element.removeAttribute("href");
175
+ options.element.textContent = "Source link unavailable.";
176
+ if (options.url.startsWith("https://")) {
177
+ options.element.href = options.url;
178
+ options.element.textContent = "Open pinned source";
179
+ }
180
+ }
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{TITLE}}</title>
7
+ <style>
8
+ {{STYLE}}
9
+ </style>
10
+ </head>
11
+ <body>
12
+ {{BODY}}
13
+ <script type="application/json" id="review-data">
14
+ {{DATA}}
15
+ </script>
16
+ <script type="module" id="review-runtime"></script>
17
+ </body>
18
+ </html>