semantic-code-review 0.24.2__py3-none-any.whl

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 (100) hide show
  1. semantic_code_review/__init__.py +5 -0
  2. semantic_code_review/augment/__init__.py +3 -0
  3. semantic_code_review/augment/agents.py +95 -0
  4. semantic_code_review/augment/console.py +440 -0
  5. semantic_code_review/augment/extra_review.py +237 -0
  6. semantic_code_review/augment/fold_summary.py +447 -0
  7. semantic_code_review/augment/hunks.py +264 -0
  8. semantic_code_review/augment/mcp_server.py +156 -0
  9. semantic_code_review/augment/overview.py +233 -0
  10. semantic_code_review/augment/pass_.py +171 -0
  11. semantic_code_review/augment/pipeline.py +571 -0
  12. semantic_code_review/augment/progress.py +223 -0
  13. semantic_code_review/augment/prompts.py +93 -0
  14. semantic_code_review/augment/schemas.py +408 -0
  15. semantic_code_review/augment/tools.py +475 -0
  16. semantic_code_review/augment/trace_adapter.py +369 -0
  17. semantic_code_review/backends/__init__.py +95 -0
  18. semantic_code_review/backends/_cli_driver.py +571 -0
  19. semantic_code_review/backends/anthropic_sdk.py +56 -0
  20. semantic_code_review/backends/base.py +104 -0
  21. semantic_code_review/backends/claude_cli.py +421 -0
  22. semantic_code_review/backends/google_sdk.py +73 -0
  23. semantic_code_review/backends/openai_compat.py +42 -0
  24. semantic_code_review/cache/__init__.py +3 -0
  25. semantic_code_review/cache/store.py +87 -0
  26. semantic_code_review/cli/__init__.py +70 -0
  27. semantic_code_review/cli/_shared.py +157 -0
  28. semantic_code_review/cli/augment.py +74 -0
  29. semantic_code_review/cli/config_cmd.py +266 -0
  30. semantic_code_review/cli/fetch.py +33 -0
  31. semantic_code_review/cli/lint.py +27 -0
  32. semantic_code_review/cli/pr.py +98 -0
  33. semantic_code_review/cli/review.py +104 -0
  34. semantic_code_review/cli/runs_cmd.py +17 -0
  35. semantic_code_review/cli/show.py +19 -0
  36. semantic_code_review/cli/strip.py +20 -0
  37. semantic_code_review/config.py +563 -0
  38. semantic_code_review/config_template.py +154 -0
  39. semantic_code_review/fetch/__init__.py +61 -0
  40. semantic_code_review/fetch/anchor.py +244 -0
  41. semantic_code_review/fetch/github.py +229 -0
  42. semantic_code_review/fetch/github_comments.py +404 -0
  43. semantic_code_review/fetch/local.py +443 -0
  44. semantic_code_review/fetch/run_source.py +70 -0
  45. semantic_code_review/format/__init__.py +3 -0
  46. semantic_code_review/format/emit.py +163 -0
  47. semantic_code_review/format/lint.py +74 -0
  48. semantic_code_review/format/parse.py +574 -0
  49. semantic_code_review/format/sidecar.py +19 -0
  50. semantic_code_review/format/strip.py +20 -0
  51. semantic_code_review/git_ops.py +252 -0
  52. semantic_code_review/paths.py +61 -0
  53. semantic_code_review/review/__init__.py +3 -0
  54. semantic_code_review/review/comments.py +176 -0
  55. semantic_code_review/review/github.py +284 -0
  56. semantic_code_review/review/github_graphql.py +433 -0
  57. semantic_code_review/review/pr_flow.py +299 -0
  58. semantic_code_review/review/runner.py +407 -0
  59. semantic_code_review/review/server.py +1043 -0
  60. semantic_code_review/structural/__init__.py +41 -0
  61. semantic_code_review/structural/diff.py +102 -0
  62. semantic_code_review/structural/parse.py +330 -0
  63. semantic_code_review/structural/symbols.py +43 -0
  64. semantic_code_review/viewer/__init__.py +3 -0
  65. semantic_code_review/viewer/assets/annotations.ts +547 -0
  66. semantic_code_review/viewer/assets/boot.ts +273 -0
  67. semantic_code_review/viewer/assets/comment_store.ts +121 -0
  68. semantic_code_review/viewer/assets/comments.ts +624 -0
  69. semantic_code_review/viewer/assets/console.ts +426 -0
  70. semantic_code_review/viewer/assets/console_render.ts +213 -0
  71. semantic_code_review/viewer/assets/console_selection.ts +102 -0
  72. semantic_code_review/viewer/assets/data_store.ts +156 -0
  73. semantic_code_review/viewer/assets/file_rows.ts +49 -0
  74. semantic_code_review/viewer/assets/folds.ts +560 -0
  75. semantic_code_review/viewer/assets/index.html +62 -0
  76. semantic_code_review/viewer/assets/post_modal.ts +383 -0
  77. semantic_code_review/viewer/assets/progress.ts +138 -0
  78. semantic_code_review/viewer/assets/render.ts +937 -0
  79. semantic_code_review/viewer/assets/sidebar.ts +429 -0
  80. semantic_code_review/viewer/assets/sse.ts +78 -0
  81. semantic_code_review/viewer/assets/text_highlight.ts +215 -0
  82. semantic_code_review/viewer/assets/types.d.ts +388 -0
  83. semantic_code_review/viewer/assets/vendor/LICENSE +29 -0
  84. semantic_code_review/viewer/assets/vendor/VENDOR.md +55 -0
  85. semantic_code_review/viewer/assets/vendor/github-dark.min.css +10 -0
  86. semantic_code_review/viewer/assets/vendor/github.min.css +10 -0
  87. semantic_code_review/viewer/assets/vendor/highlight.min.js +1244 -0
  88. semantic_code_review/viewer/assets/vendor/mermaid.LICENSE +21 -0
  89. semantic_code_review/viewer/assets/vendor/mermaid.min.js +3587 -0
  90. semantic_code_review/viewer/assets/vendor/refresh.sh +65 -0
  91. semantic_code_review/viewer/assets/viewer.css +1202 -0
  92. semantic_code_review/viewer/assets/viewer.js +10588 -0
  93. semantic_code_review/viewer/build_json.py +546 -0
  94. semantic_code_review/viewer/hunk_layout.py +471 -0
  95. semantic_code_review-0.24.2.dist-info/METADATA +348 -0
  96. semantic_code_review-0.24.2.dist-info/RECORD +100 -0
  97. semantic_code_review-0.24.2.dist-info/WHEEL +5 -0
  98. semantic_code_review-0.24.2.dist-info/entry_points.txt +2 -0
  99. semantic_code_review-0.24.2.dist-info/licenses/LICENSE +21 -0
  100. semantic_code_review-0.24.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,560 @@
1
+ // Indent-based fold detection + on-demand fold-summary requests.
2
+ //
3
+ // The viewer's fold story is unified per-file (not per-stretch):
4
+ // `attachFileFolds(fileEl, file)` walks every visible row in the
5
+ // file body in DOM order — across hunks and adjacent expanded
6
+ // context blocks — runs an indent-based fold detector over the
7
+ // unified sequence, and attaches one chevron per region. Folds
8
+ // whose body spans a hunk boundary collapse the right rows in
9
+ // every container because each row carries its own DOM refs.
10
+ //
11
+ // First time the reviewer collapses a region whose summary is
12
+ // empty, this module fires `POST /fold-summary` against the live
13
+ // review server. The response writes back into the region object
14
+ // (mutating DATA in place); the server's `fold-summary` SSE event
15
+ // is handled by `applyFoldSummary` in boot.ts.
16
+ //
17
+ import { Annotations, type AnnotationHandle } from "./annotations";
18
+ import { FileRows, type RowWithEls } from "./file_rows";
19
+
20
+ interface DetectedRegion {
21
+ header_idx: number;
22
+ body_start_idx: number;
23
+ body_end_idx: number;
24
+ context: FoldContext;
25
+ right_start: number | null;
26
+ right_end: number | null;
27
+ left_start: number | null;
28
+ left_end: number | null;
29
+ // Identity of the definition the region snapped to; null for an
30
+ // indentation-fallback region.
31
+ qualified_name: string | null;
32
+ kind: string | null;
33
+ }
34
+
35
+ interface AttachedFold {
36
+ marker: SVGElement;
37
+ foldHandle: AnnotationHandle | null;
38
+ }
39
+
40
+ interface FoldRequestAddress {
41
+ context: FoldContext;
42
+ right_start?: number;
43
+ right_end?: number;
44
+ left_start?: number;
45
+ left_end?: number;
46
+ }
47
+
48
+ interface FoldFileState {
49
+ handles: AnnotationHandle[];
50
+ chevrons: SVGElement[];
51
+ }
52
+
53
+ const _FILE_FOLD_STATE: Record<string, FoldFileState> = Object.create(null);
54
+
55
+ function _sessionEndpoint(): string | null {
56
+ // Read at call time, not module init — the meta tag may be
57
+ // injected after this module loads (tests set up the DOM
58
+ // dynamically, and a future bootloader might too). Empty
59
+ // string content means "same origin" (the production case);
60
+ // a missing meta tag (null) means no server is available
61
+ // and the route is wired off.
62
+ if (typeof document === "undefined") return null;
63
+ const m = document.querySelector('meta[name="scr-session-endpoint"]');
64
+ if (!m) return null;
65
+ return m.getAttribute("content") || "";
66
+ }
67
+
68
+ // --- DOM helpers (private, duplicated from viewer.js because the
69
+ // classic-script module boundary doesn't let us import them) ----------
70
+
71
+ const _SVG_NS = "http://www.w3.org/2000/svg";
72
+
73
+ function _chev(folded: boolean, extraClass: string): SVGElement {
74
+ const svg = document.createElementNS(_SVG_NS, "svg") as unknown as SVGElement;
75
+ svg.setAttribute("viewBox", "0 0 12 12");
76
+ svg.setAttribute("aria-hidden", "true");
77
+ svg.classList.add("chevron");
78
+ svg.classList.add(extraClass);
79
+ if (!folded) svg.classList.add("open");
80
+ const path = document.createElementNS(_SVG_NS, "path");
81
+ path.setAttribute("d", "M4.25 2.75 L8 6 L4.25 9.25");
82
+ path.setAttribute("fill", "none");
83
+ path.setAttribute("stroke", "currentColor");
84
+ path.setAttribute("stroke-width", "1.75");
85
+ path.setAttribute("stroke-linecap", "round");
86
+ path.setAttribute("stroke-linejoin", "round");
87
+ svg.appendChild(path);
88
+ return svg;
89
+ }
90
+
91
+ function _isRowContentEmpty(rowEl: HTMLElement | undefined | null): boolean {
92
+ if (!rowEl) return true;
93
+ const content = rowEl.children[1];
94
+ return !content || content.classList.contains("empty");
95
+ }
96
+
97
+ // --- File-level walker --------------------------------------------------
98
+
99
+ function _teardownFileFolds(fileId: string): void {
100
+ const s = _FILE_FOLD_STATE[fileId];
101
+ if (!s) return;
102
+ for (const h of s.handles) {
103
+ try { h.remove(); } catch (_) { /* ignore */ }
104
+ }
105
+ for (const c of s.chevrons) {
106
+ try { c.remove(); } catch (_) { /* ignore */ }
107
+ }
108
+ delete _FILE_FOLD_STATE[fileId];
109
+ }
110
+
111
+ // Walk the file body's .diff / .gap-expansion containers in DOM order,
112
+ // pull each one's row stream out of `FileRows` (recorded by render.ts
113
+ // at construction time), and flatten into one indexable list so folds
114
+ // can straddle hunks and adjacent gap-context.
115
+ function _collectFileRows(fileEl: HTMLElement): RowWithEls[] {
116
+ const body = fileEl.querySelector(".file-body");
117
+ if (!body) return [];
118
+ const out: RowWithEls[] = [];
119
+ for (const child of Array.from(body.children) as HTMLElement[]) {
120
+ const cls = child.classList;
121
+ let source: HTMLElement | null = null;
122
+ if (cls.contains("hunk")) {
123
+ source = child.querySelector(".diff");
124
+ } else if (cls.contains("gap-expansion")) {
125
+ source = child;
126
+ }
127
+ if (!source) continue;
128
+ const entry = FileRows.get(source);
129
+ if (!entry) continue;
130
+ for (let i = 0; i < entry.rows.length; i++) {
131
+ out.push({
132
+ ...entry.rows[i],
133
+ oldEl: entry.oldEls[i], newEl: entry.newEls[i],
134
+ });
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+
140
+ function _findExistingFoldRecord(
141
+ file: FileBlock, det: DetectedRegion,
142
+ ): FoldRegion | null {
143
+ const rs = det.right_start || 0, re_ = det.right_end || 0;
144
+ const ls = det.left_start || 0, le = det.left_end || 0;
145
+ for (const h of file.hunks || []) {
146
+ for (const r of h.fold_regions || []) {
147
+ if (
148
+ (r.context || "right") === det.context
149
+ && (r.right_start || 0) === rs && (r.right_end || 0) === re_
150
+ && (r.left_start || 0) === ls && (r.left_end || 0) === le
151
+ ) {
152
+ return r;
153
+ }
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function _upsertFoldRegion(
160
+ file: FileBlock, det: DetectedRegion, rows: RowWithEls[],
161
+ ): FoldRegion {
162
+ // The local POST handler and the SSE updater both mutate the
163
+ // region object's `summary` field — they need to point at the
164
+ // same reference. Find a matching persistent record if one
165
+ // exists, refresh its detected fields, and return it. Otherwise
166
+ // create a new one and stash it on the file's first hunk so the
167
+ // next render picks it up.
168
+ const hasChanges = _anyChangesInRange(rows, det.header_idx, det.body_end_idx);
169
+ const existing = _findExistingFoldRecord(file, det);
170
+ if (existing) {
171
+ existing.header_idx = det.header_idx;
172
+ existing.body_start_idx = det.body_start_idx;
173
+ existing.body_end_idx = det.body_end_idx;
174
+ existing.has_changes = hasChanges;
175
+ existing.qualified_name = det.qualified_name;
176
+ existing.kind = det.kind;
177
+ return existing;
178
+ }
179
+ const candidate: FoldRegion = {
180
+ header_idx: det.header_idx,
181
+ body_start_idx: det.body_start_idx,
182
+ body_end_idx: det.body_end_idx,
183
+ context: det.context,
184
+ right_start: det.right_start, right_end: det.right_end,
185
+ left_start: det.left_start, left_end: det.left_end,
186
+ has_changes: hasChanges,
187
+ qualified_name: det.qualified_name, kind: det.kind,
188
+ summary: "",
189
+ };
190
+ if (file.hunks && file.hunks.length > 0) {
191
+ const h0 = file.hunks[0];
192
+ if (!h0.fold_regions) h0.fold_regions = [];
193
+ h0.fold_regions.push(candidate);
194
+ }
195
+ return candidate;
196
+ }
197
+
198
+ function _anyChangesInRange(
199
+ rows: RowWithEls[], start: number, end: number,
200
+ ): boolean {
201
+ for (let i = start; i <= end; i++) {
202
+ const k = rows[i].kind;
203
+ if (k === "ins" || k === "del" || k === "pair") return true;
204
+ }
205
+ return false;
206
+ }
207
+
208
+ // --- Indent-based detection --------------------------------------------
209
+
210
+ function _rowIndent(row: RowBlock): number {
211
+ const text = row.kind === "del" ? row.old_text : row.new_text;
212
+ if (!text || !text.trim()) return -1;
213
+ let ind = 0;
214
+ for (const ch of text) {
215
+ if (ch === " ") ind += 1;
216
+ else if (ch === "\t") ind += 4;
217
+ else break;
218
+ }
219
+ return ind;
220
+ }
221
+
222
+ function _indentRawRegions(rows: RowWithEls[]): Array<[number, number]> {
223
+ const indents = rows.map(_rowIndent);
224
+ const nextNonBlank = (i: number): number | null => {
225
+ for (let j = i + 1; j < indents.length; j++) {
226
+ if (indents[j] !== -1) return indents[j];
227
+ }
228
+ return null;
229
+ };
230
+ const raw: Array<[number, number]> = [];
231
+ const stack: Array<[number, number]> = [];
232
+ for (let i = 0; i < indents.length; i++) {
233
+ const ind = indents[i];
234
+ if (ind === -1) continue;
235
+ while (stack.length && stack[stack.length - 1][0] >= ind) {
236
+ const top = stack.pop()!;
237
+ raw.push([top[1], i - 1]);
238
+ }
239
+ const ni = nextNonBlank(i);
240
+ if (ni !== null && ni > ind) stack.push([ind, i]);
241
+ }
242
+ while (stack.length) {
243
+ const top = stack.pop()!;
244
+ raw.push([top[1], indents.length - 1]);
245
+ }
246
+ return raw;
247
+ }
248
+
249
+ // Definition spans enclosing a row, outermost-first: the row maps by
250
+ // line number into one side's tree — new_line into head spans (ctx /
251
+ // pair / ins rows), else old_line into base spans (del-only rows).
252
+ function _rowSymbols(
253
+ row: RowWithEls, headSpans: FoldSymbolSpan[], baseSpans: FoldSymbolSpan[],
254
+ ): FoldSymbolSpan[] {
255
+ let line: number | null;
256
+ let spans: FoldSymbolSpan[];
257
+ if (row.new_line != null) { line = row.new_line; spans = headSpans; }
258
+ else if (row.old_line != null) { line = row.old_line; spans = baseSpans; }
259
+ else return [];
260
+ return spans
261
+ .filter((s) => s.start_line <= line! && line! <= s.end_line)
262
+ .sort((a, b) => a.depth - b.depth);
263
+ }
264
+
265
+ // `(headerIdx, bodyEndIdx, qualifiedName, kind)` snapped to definition
266
+ // spans, plus the set of row indices inside any definition. Every
267
+ // definition with >=1 present row becomes a region from its first to its
268
+ // last present row carrying that definition's identity; nested defs nest
269
+ // because a row carries its whole enclosing chain.
270
+ type RawRegion = [number, number, string | null, string | null];
271
+
272
+ function _symbolRawRegions(
273
+ rows: RowWithEls[], headSpans: FoldSymbolSpan[], baseSpans: FoldSymbolSpan[],
274
+ ): { raw: RawRegion[]; covered: Set<number> } {
275
+ const runs = new Map<string, [number, number]>();
276
+ const kinds = new Map<string, string>();
277
+ const order: string[] = [];
278
+ const covered = new Set<number>();
279
+ for (let i = 0; i < rows.length; i++) {
280
+ for (const s of _rowSymbols(rows[i], headSpans, baseSpans)) {
281
+ covered.add(i);
282
+ const run = runs.get(s.qualified_name);
283
+ if (run === undefined) {
284
+ runs.set(s.qualified_name, [i, i]);
285
+ kinds.set(s.qualified_name, s.kind);
286
+ order.push(s.qualified_name);
287
+ } else {
288
+ run[1] = i;
289
+ }
290
+ }
291
+ }
292
+ const raw: RawRegion[] = order.map((qn) => {
293
+ const run = runs.get(qn)!;
294
+ return [run[0], run[1], qn, kinds.get(qn)!];
295
+ });
296
+ return { raw, covered };
297
+ }
298
+
299
+ function _computeFoldRegions(
300
+ rows: RowWithEls[],
301
+ headSpans: FoldSymbolSpan[] = [],
302
+ baseSpans: FoldSymbolSpan[] = [],
303
+ ): DetectedRegion[] {
304
+ // Uniform shape: [headerIdx, bodyEndIdx, qualifiedName|null, kind|null].
305
+ // Indentation regions carry no symbol.
306
+ let raw: RawRegion[];
307
+ if (headSpans.length || baseSpans.length) {
308
+ const sym = _symbolRawRegions(rows, headSpans, baseSpans);
309
+ // Keep an indentation region only where no row it spans is already
310
+ // covered by a definition — the snapped region owns that stretch.
311
+ raw = sym.raw.concat(
312
+ _indentRawRegions(rows)
313
+ .filter(([h, e]) => {
314
+ for (let j = h; j <= e; j++) if (sym.covered.has(j)) return false;
315
+ return true;
316
+ })
317
+ .map(([h, e]): RawRegion => [h, e, null, null]),
318
+ );
319
+ } else {
320
+ raw = _indentRawRegions(rows).map(([h, e]): RawRegion => [h, e, null, null]);
321
+ }
322
+ raw.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
323
+ const regions: DetectedRegion[] = [];
324
+ for (const [header_idx, body_end, qualified_name, kind] of raw) {
325
+ const body_start = header_idx + 1;
326
+ if (body_start > body_end) continue;
327
+ const right_start = _firstLine(rows, header_idx, body_end, "new_line");
328
+ const right_end = _lastLine(rows, header_idx, body_end, "new_line");
329
+ const left_start = _firstLine(rows, header_idx, body_end, "old_line");
330
+ const left_end = _lastLine(rows, header_idx, body_end, "old_line");
331
+ const hasChanges = _anyChangesInRange(rows, header_idx, body_end);
332
+ let context: FoldContext;
333
+ if (right_start != null && left_start != null && hasChanges) context = "both";
334
+ else if (right_start != null) context = "right";
335
+ else context = "left";
336
+ regions.push({
337
+ header_idx, body_start_idx: body_start, body_end_idx: body_end,
338
+ context, right_start, right_end, left_start, left_end,
339
+ qualified_name, kind,
340
+ });
341
+ }
342
+ return regions;
343
+ }
344
+
345
+ function _firstLine(
346
+ rows: RowWithEls[], start: number, end: number, attr: "new_line" | "old_line",
347
+ ): number | null {
348
+ for (let j = start; j <= end; j++) {
349
+ const v = rows[j][attr];
350
+ if (v != null) return v;
351
+ }
352
+ return null;
353
+ }
354
+
355
+ function _lastLine(
356
+ rows: RowWithEls[], start: number, end: number, attr: "new_line" | "old_line",
357
+ ): number | null {
358
+ for (let j = end; j >= start; j--) {
359
+ const v = rows[j][attr];
360
+ if (v != null) return v;
361
+ }
362
+ return null;
363
+ }
364
+
365
+ // --- Attach + click ----------------------------------------------------
366
+
367
+ function _canRequestFoldSummary(
368
+ fileIdx: number | null, region: FoldRegion,
369
+ ): boolean {
370
+ if (_sessionEndpoint() === null) return false;
371
+ if (fileIdx == null) return false;
372
+ return _foldAddress(region) !== null;
373
+ }
374
+
375
+ function _foldAddress(region: FoldRegion): FoldRequestAddress | null {
376
+ const context = region.context || "right";
377
+ const addr: FoldRequestAddress = { context };
378
+ if (context === "right" || context === "both") {
379
+ if (region.right_start == null || region.right_end == null) return null;
380
+ addr.right_start = region.right_start;
381
+ addr.right_end = region.right_end;
382
+ }
383
+ if (context === "left" || context === "both") {
384
+ if (region.left_start == null || region.left_end == null) return null;
385
+ addr.left_start = region.left_start;
386
+ addr.left_end = region.left_end;
387
+ }
388
+ return addr;
389
+ }
390
+
391
+ // Prefix the collapsed placeholder with the region's symbol identity,
392
+ // e.g. "function Foo.bar — ". Empty for an indentation-fallback region
393
+ // (no symbol), which keeps today's unlabelled placeholder.
394
+ function _foldLabel(region: FoldRegion): string {
395
+ if (!region.qualified_name) return "";
396
+ const kind = region.kind ? `${region.kind} ` : "";
397
+ return `${kind}${region.qualified_name} — `;
398
+ }
399
+
400
+ function _requestFoldSummary(
401
+ fileIdx: number, region: FoldRegion,
402
+ foldHandle: AnnotationHandle,
403
+ ): void {
404
+ if (region._inflight || region.summary) return;
405
+ const addr = _foldAddress(region);
406
+ if (!addr) return;
407
+ region._inflight = true;
408
+ const label = _foldLabel(region);
409
+ _setFoldBoxContent(foldHandle, label + "summarising…", { pending: true });
410
+ const retry = (): void => _requestFoldSummary(fileIdx, region, foldHandle);
411
+ fetch(_sessionEndpoint() + "/fold-summary", {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify({ file_idx: fileIdx, ...addr }),
415
+ })
416
+ .then((r) => r.json().then((j: { summary?: string }) => ({ status: r.status, body: j })))
417
+ .then(({ status, body }) => {
418
+ region._inflight = false;
419
+ if (status === 200 && body.summary) {
420
+ region.summary = body.summary;
421
+ _setFoldBoxContent(foldHandle, label + body.summary, {});
422
+ } else {
423
+ _setFoldBoxContent(
424
+ foldHandle, label + "(summary failed — click to retry)",
425
+ { failed: true }, retry,
426
+ );
427
+ }
428
+ })
429
+ .catch(() => {
430
+ region._inflight = false;
431
+ _setFoldBoxContent(
432
+ foldHandle, label + "(summary failed — click to retry)",
433
+ { failed: true }, retry,
434
+ );
435
+ });
436
+ }
437
+
438
+ function _setFoldBoxContent(
439
+ foldHandle: AnnotationHandle, text: string,
440
+ classes: { pending?: boolean; failed?: boolean },
441
+ onClick?: () => void,
442
+ ): void {
443
+ if (!foldHandle || !foldHandle.element) return;
444
+ const box = foldHandle.element.querySelector(".annot-box") as HTMLElement | null;
445
+ if (!box) return;
446
+ box.textContent = text;
447
+ box.classList.remove("pending", "failed");
448
+ if (classes.pending) box.classList.add("pending");
449
+ if (classes.failed) box.classList.add("failed");
450
+ if (onClick) {
451
+ const clone = box.cloneNode(true) as HTMLElement;
452
+ clone.style.cursor = "pointer";
453
+ clone.addEventListener("click", onClick);
454
+ box.replaceWith(clone);
455
+ }
456
+ foldHandle.resize();
457
+ }
458
+
459
+ function _attachOneFold(
460
+ rows: RowWithEls[], region: FoldRegion, fileIdx: number,
461
+ ): AttachedFold | null {
462
+ const bodyStart = region.body_start_idx;
463
+ const bodyEnd = region.body_end_idx;
464
+ if (bodyStart > bodyEnd) return null;
465
+
466
+ const headerRow = rows[region.header_idx];
467
+ if (!headerRow) return null;
468
+ const headerOld = headerRow.oldEl;
469
+ const headerNew = headerRow.newEl;
470
+ if (!headerOld && !headerNew) return null;
471
+
472
+ const side = _isRowContentEmpty(headerNew) && !_isRowContentEmpty(headerOld)
473
+ ? "old" : "new";
474
+ const anchor = side === "new" ? headerNew : headerOld;
475
+ const shadow = side === "new" ? headerOld : headerNew;
476
+
477
+ const marker = _chev(false, "fold-chev");
478
+ marker.setAttribute("role", "button");
479
+ marker.setAttribute("tabindex", "0");
480
+
481
+ let foldHandle: AnnotationHandle | null = null;
482
+ const canSummarise = _canRequestFoldSummary(fileIdx, region);
483
+ if (region.summary || region.has_changes || canSummarise) {
484
+ // Seed the placeholder with the symbol identity (if any) followed by
485
+ // the summary or its pending/run-augment stand-in.
486
+ const label = _foldLabel(region);
487
+ const pending = !region.summary && canSummarise;
488
+ const bodyText = region.summary
489
+ || (canSummarise
490
+ ? "summarising…"
491
+ : "(changes here; run augment to generate a description)");
492
+ foldHandle = Annotations.attach({
493
+ anchor, shadowAnchor: shadow,
494
+ variant: "fold", content: label + bodyText,
495
+ });
496
+ if (!region.summary) {
497
+ const box = foldHandle.element.querySelector(".annot-box");
498
+ if (box) box.classList.add("missing");
499
+ if (pending && box) box.classList.add("pending");
500
+ }
501
+ foldHandle.element.style.display = "none";
502
+ if (foldHandle.placeholder) foldHandle.placeholder.style.display = "none";
503
+ }
504
+
505
+ marker.addEventListener("click", (e) => {
506
+ e.stopPropagation();
507
+ const nowOpen = marker.classList.toggle("open");
508
+ for (let i = bodyStart; i <= bodyEnd; i++) {
509
+ const r = rows[i];
510
+ if (!r) continue;
511
+ if (r.oldEl) r.oldEl.style.display = nowOpen ? "" : "none";
512
+ if (r.newEl) r.newEl.style.display = nowOpen ? "" : "none";
513
+ }
514
+ if (foldHandle) {
515
+ foldHandle.element.style.display = nowOpen ? "none" : "";
516
+ if (foldHandle.placeholder) {
517
+ foldHandle.placeholder.style.display = nowOpen ? "none" : "";
518
+ }
519
+ if (!nowOpen) foldHandle.resize();
520
+ }
521
+ if (!nowOpen && !region.summary && foldHandle
522
+ && _canRequestFoldSummary(fileIdx, region)) {
523
+ _requestFoldSummary(fileIdx, region, foldHandle);
524
+ }
525
+ Annotations.reflow(anchor);
526
+ });
527
+
528
+ const contentCell = anchor && (anchor.children[1] as HTMLElement | undefined);
529
+ if (contentCell) contentCell.prepend(marker);
530
+ return { marker, foldHandle };
531
+ }
532
+
533
+ function attachFileFolds(fileEl: HTMLElement, file: FileBlock): void {
534
+ _teardownFileFolds(file.id);
535
+ const fileIdx = Number(file.id.replace("F", ""));
536
+ const rows = _collectFileRows(fileEl);
537
+ if (rows.length === 0) return;
538
+ const syms = file.fold_symbols || { head: [], base: [] };
539
+ const detected = _computeFoldRegions(rows, syms.head, syms.base);
540
+ const handles: AnnotationHandle[] = [];
541
+ const chevrons: SVGElement[] = [];
542
+ for (const det of detected) {
543
+ const region = _upsertFoldRegion(file, det, rows);
544
+ const attached = _attachOneFold(rows, region, fileIdx);
545
+ if (!attached) continue;
546
+ if (attached.foldHandle) handles.push(attached.foldHandle);
547
+ if (attached.marker) chevrons.push(attached.marker);
548
+ }
549
+ _FILE_FOLD_STATE[file.id] = { handles, chevrons };
550
+ }
551
+
552
+ // The single runtime surface. boot.ts calls attachFileFolds on
553
+ // initial render, after every gap expand/collapse, and from
554
+ // applyFoldSummary's cross-tab path.
555
+ export const Folds = { attachFileFolds };
556
+
557
+ // Exposed for the cross-language lockstep fixture (tests/js/folds.test.ts):
558
+ // the same (rows, spans) input must yield the same regions as the Python
559
+ // `compute_fold_regions`. Not used by the runtime bundle.
560
+ export { _computeFoldRegions };
@@ -0,0 +1,62 @@
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
+ <meta name="scr-session-endpoint" content="">
7
+ <title>Semantic Code Review</title>
8
+ <link rel="stylesheet" media="(prefers-color-scheme: dark)" href="/static/vendor/github-dark.min.css">
9
+ <link rel="stylesheet" media="(prefers-color-scheme: light)" href="/static/vendor/github.min.css">
10
+ <link rel="stylesheet" href="/static/viewer.css">
11
+ </head>
12
+ <body>
13
+ <header class="pr-bar">
14
+ <div class="pr-title">
15
+ <span class="pr-meta"></span>
16
+ </div>
17
+ <div class="fold-slider" role="tablist" aria-label="Fold level">
18
+ <button data-fold="files" title="Collapse to files (1)">Files</button>
19
+ <button data-fold="hunks" title="Collapse to hunks (2)">Hunks</button>
20
+ <button data-fold="segments" title="Collapse to segments (3)">Segments</button>
21
+ <button data-fold="off" title="Fully expanded (4)">Off</button>
22
+ </div>
23
+ <button id="reset-btn" class="reset-btn" title="Clear per-region overrides">Reset</button>
24
+ <button id="help-btn" class="help-btn" title="Help (?)" aria-label="Help">?</button>
25
+ </header>
26
+
27
+ <div id="scr-progress" class="scr-progress hidden" aria-live="polite">
28
+ <div class="scr-progress-summary">
29
+ <span class="scr-progress-overview" data-state="pending">Overview</span>
30
+ <span class="scr-progress-hunks">Hunks <span class="scr-progress-done">0</span>/<span class="scr-progress-total">0</span></span>
31
+ <span class="scr-progress-detail">
32
+ (<span class="scr-progress-running">0</span> running &middot;
33
+ <span class="scr-progress-queued">0</span> queued &middot;
34
+ <span class="scr-progress-failed">0</span> failed)
35
+ </span>
36
+ </div>
37
+ <div class="scr-progress-grid" role="list" aria-label="Hunk progress"></div>
38
+ </div>
39
+
40
+ <div class="layout">
41
+ <aside id="group-sidebar" class="group-sidebar"></aside>
42
+ <main id="app"></main>
43
+ </div>
44
+
45
+ <footer id="status-bar" class="status-bar">
46
+ <span id="status-counts" class="status-counts"></span>
47
+ </footer>
48
+
49
+ <div id="help-overlay" class="help-overlay hidden" role="dialog" aria-label="Help">
50
+ <div class="help-card">
51
+ <h3>Keyboard shortcuts</h3>
52
+ <p><kbd>1</kbd> fold to files &middot; <kbd>2</kbd> hunks &middot; <kbd>3</kbd> segments &middot; <kbd>4</kbd> expand</p>
53
+ <p><kbd>?</kbd> toggle this help &middot; <kbd>Esc</kbd> close</p>
54
+ <p>Click any region header to toggle its fold state independently.
55
+ "Reset" clears per-region overrides and applies the global fold level.</p>
56
+ </div>
57
+ </div>
58
+
59
+ <script src="/static/vendor/highlight.min.js"></script>
60
+ <script src="/static/viewer.js"></script>
61
+ </body>
62
+ </html>