iterate-plugin 2.7.1 → 2.7.3

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/lib/client.js CHANGED
@@ -1,90 +1,709 @@
1
- /**
2
- * lib/client.js iterate-plugin client entry.
3
- *
4
- * Loaded by the dsh web front-end via `package.json.dsh.client` (the
5
- * `exports["./client"]` subpath). Module contract matches dsh-gui-customization:
6
- * the file exports an `apply(ctx)` function that is called with the client
7
- * Cordis context.
8
- *
9
- * Implements the §14.4 / §16 six-part UI layer:
10
- * 1. Convergence dashboard -> conversation.input.dock
11
- * 2. Findings triage panel -> conversation.chat.turnTail
12
- * 3. Iteration stats card -> conversation.chat.turnTail (same chain, empty-findings case)
13
- * 4. iterate theme skin -> theme.overrideTokens
14
- * 5. Progress event linkage -> ctx.on('slots/changed') + round-pulse + shell.overlay capsule
15
- * 6. Settings page section -> settings.section
16
- *
17
- * Design notes:
18
- * - Build-free: no bundler. All styles are injected via one <style> tag and
19
- * colors use the dsh token variables (`--dsw-*`).
20
- * - Defensive by design: every service lookup is optional and guarded, so a
21
- * missing slot/theme/React degrades gracefully instead of crashing the UI.
22
- */
23
-
24
- import {
25
- findReportInObject,
26
- scanSessionForReport,
27
- normalizeReport,
28
- computeConvergenceProgress,
29
- getCurrentRound,
30
- getTotalRounds,
31
- severityStats,
32
- groupByDimension,
33
- buildTriageState,
34
- hashReport,
35
- toKnownIntentionalYaml,
36
- buildApplyInstruction,
37
- collectIgnoredEntries,
38
- filterFindingsWithIndices,
39
- buildFilterOptions,
40
- countVerdicts,
41
- batchSetVerdict,
42
- setAllVerdicts,
43
- buildRoundHistory,
44
- buildFindingTrend,
45
- computeTrendMetrics,
46
- trendMax,
47
- buildCompletionSummary,
48
- buildConfigEditGuide,
49
- keyToVerdict,
50
- allVerdictKeys,
51
- buildRuntimeStatusGuide,
52
- SEVERITY_LABEL,
53
- SEVERITY_COLOR,
54
- } from './parse.js'
55
-
56
- // ─── Constants ───────────────────────────────────────────────────────────────
57
-
58
- const PLUGIN_TAG = 'iterate-ui'
59
-
60
- /** localStorage key prefix for triage verdicts. */
61
- const TRIAGE_STORAGE_PREFIX = 'iterate.triage.'
62
-
63
- /** localStorage key for the theme skin toggle. */
64
- const THEME_STORAGE_KEY = 'iterate.theme.enabled'
65
-
66
- /** Theme override source name (matches the doc §16.1). */
67
- const THEME_SOURCE = 'iterate'
68
-
69
- /** Theme tokens: 13 dsw tokens, each with light + dark values (warm amber accent). */
70
- const ITERATE_TOKENS = {
71
- '--dsw-alias-bg-base': { light: '#FAF8F5', dark: '#171412' },
72
- '--dsw-alias-bg-layer-1': { light: '#FFFFFF', dark: '#1F1B17' },
73
- '--dsw-alias-bg-layer-2': { light: '#F4F1EA', dark: '#27221C' },
74
- '--dsw-alias-bg-overlay': { light: 'rgba(255,255,255,0.96)', dark: 'rgba(23,20,18,0.96)' },
75
- '--dsw-alias-border-l1': { light: '#E8E2D8', dark: '#332C25' },
76
- '--dsw-alias-border-l2': { light: '#DDD5C9', dark: '#3C342B' },
77
- '--dsw-alias-brand-primary': { light: '#B45309', dark: '#F59E0B' },
78
- '--dsw-alias-label-primary': { light: '#1C1917', dark: '#F5F0EA' },
79
- '--dsw-alias-label-secondary': { light: '#57534E', dark: '#A9A29B' },
80
- '--dsw-alias-state-error-primary': { light: '#DC2626', dark: '#F87171' },
81
- '--dsw-alias-state-success-primary': { light: '#15803D', dark: '#4ADE80' },
82
- '--dsw-alias-state-warn-primary': { light: '#D97706', dark: '#FBBF24' },
83
- '--dsw-specific-sidebar-fill': { light: '#F5F2EC', dark: '#14110E' },
1
+ window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
2
+ var module = { exports: {} };
3
+ var exports = module.exports;
4
+ "use strict";
5
+ var __create = Object.create;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name2 in all)
13
+ __defProp(target, name2, { get: all[name2], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/client/index.ts
34
+ var index_exports = {};
35
+ __export(index_exports, {
36
+ apply: () => apply,
37
+ inject: () => inject,
38
+ name: () => name
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+ var React = __toESM(require("react"), 1);
42
+
43
+ // lib/parse.js
44
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low"];
45
+ var SEVERITY_LABEL = {
46
+ critical: "CRIT",
47
+ high: "HIGH",
48
+ medium: "MED",
49
+ low: "LOW"
50
+ };
51
+ var SEVERITY_COLOR = {
52
+ critical: "#ef4444",
53
+ high: "#f97316",
54
+ medium: "#eab308",
55
+ low: "#6b7280"
56
+ };
57
+ function isReviewReport(obj) {
58
+ if (!obj || typeof obj !== "object") return false;
59
+ const o = (
60
+ /** @type {Record<string, unknown>} */
61
+ obj
62
+ );
63
+ return typeof o.convergence === "object" && o.convergence !== null && Array.isArray(o.findings) && Array.isArray(o.rounds);
64
+ }
65
+ function findReportInObject(obj, seen, maxDepth = 20) {
66
+ if (maxDepth <= 0) return null;
67
+ if (!obj || typeof obj !== "object") return null;
68
+ const s = seen || /* @__PURE__ */ new Set();
69
+ if (s.has(obj)) return null;
70
+ s.add(obj);
71
+ if (isReviewReport(obj)) return (
72
+ /** @type {Record<string, unknown>} */
73
+ obj
74
+ );
75
+ if (Array.isArray(obj)) {
76
+ for (const item of obj) {
77
+ const found = findReportInObject(item, s, maxDepth - 1);
78
+ if (found) return found;
79
+ }
80
+ return null;
81
+ }
82
+ const o = (
83
+ /** @type {Record<string, unknown>} */
84
+ obj
85
+ );
86
+ for (const key of Object.keys(o)) {
87
+ const val = o[key];
88
+ if (val && typeof val === "object") {
89
+ const found = findReportInObject(val, s, maxDepth - 1);
90
+ if (found) return found;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ function scanSessionForReport(session) {
96
+ if (!session || typeof session !== "object") return null;
97
+ const direct = findReportInObject(session);
98
+ if (direct) return direct;
99
+ const s = (
100
+ /** @type {Record<string, unknown>} */
101
+ session
102
+ );
103
+ if (Array.isArray(s.toolCalls)) {
104
+ const calls = (
105
+ /** @type {Array<Record<string, unknown>>} */
106
+ s.toolCalls
107
+ );
108
+ for (let i = calls.length - 1; i >= 0; i--) {
109
+ const call = calls[i];
110
+ if (!call) continue;
111
+ if (call.tool === "iterate_review" || String(call.tool ?? "").endsWith("iterate_review")) {
112
+ const result = call.result;
113
+ if (result && typeof result === "object") {
114
+ const r = (
115
+ /** @type {Record<string, unknown>} */
116
+ result
117
+ );
118
+ if (r.report && typeof r.report === "object") {
119
+ return (
120
+ /** @type {Record<string, unknown>} */
121
+ r.report
122
+ );
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ if (Array.isArray(s.messages)) {
129
+ const msgs = (
130
+ /** @type {Array<Record<string, unknown>>} */
131
+ s.messages
132
+ );
133
+ for (let i = msgs.length - 1; i >= 0; i--) {
134
+ const msg = msgs[i];
135
+ if (!msg || !Array.isArray(msg.tool_calls)) continue;
136
+ const calls = (
137
+ /** @type {Array<Record<string, unknown>>} */
138
+ msg.tool_calls
139
+ );
140
+ for (const call of calls) {
141
+ if (!call) continue;
142
+ const fn = call.function;
143
+ if (fn && typeof fn === "object") {
144
+ const f = (
145
+ /** @type {Record<string, unknown>} */
146
+ fn
147
+ );
148
+ if (String(f.name ?? "").endsWith("iterate_review")) {
149
+ try {
150
+ const args = JSON.parse(String(f.arguments ?? "{}"));
151
+ const found = findReportInObject(args);
152
+ if (found) return found;
153
+ } catch {
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ function isRunSummary(obj) {
163
+ if (!obj || typeof obj !== "object") return false;
164
+ const o = (
165
+ /** @type {Record<string, unknown>} */
166
+ obj
167
+ );
168
+ const final = o.finalReport;
169
+ return !!final && typeof final === "object" && (final.verdict === "approved" || final.verdict === "needs_revision");
170
+ }
171
+ function findRunSummaryInObject(obj, seen, maxDepth = 20) {
172
+ if (maxDepth <= 0) return null;
173
+ if (!obj || typeof obj !== "object") return null;
174
+ const s = seen || /* @__PURE__ */ new Set();
175
+ if (s.has(obj)) return null;
176
+ s.add(obj);
177
+ if (isRunSummary(obj)) return (
178
+ /** @type {Record<string, unknown>} */
179
+ obj
180
+ );
181
+ if (Array.isArray(obj)) {
182
+ for (const item of obj) {
183
+ const found = findRunSummaryInObject(item, s, maxDepth - 1);
184
+ if (found) return found;
185
+ }
186
+ return null;
187
+ }
188
+ const o = (
189
+ /** @type {Record<string, unknown>} */
190
+ obj
191
+ );
192
+ for (const key of Object.keys(o)) {
193
+ const val = o[key];
194
+ if (val && typeof val === "object") {
195
+ const found = findRunSummaryInObject(val, s, maxDepth - 1);
196
+ if (found) return found;
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ function scanSessionForRunSummary(session) {
202
+ if (!session || typeof session !== "object") return null;
203
+ const direct = findRunSummaryInObject(session);
204
+ if (direct) return direct;
205
+ const s = (
206
+ /** @type {Record<string, unknown>} */
207
+ session
208
+ );
209
+ if (Array.isArray(s.toolCalls)) {
210
+ const calls = (
211
+ /** @type {Array<Record<string, unknown>>} */
212
+ s.toolCalls
213
+ );
214
+ for (let i = calls.length - 1; i >= 0; i--) {
215
+ const call = calls[i];
216
+ if (!call) continue;
217
+ if (call.tool === "workflow" || String(call.tool ?? "").endsWith("workflow")) {
218
+ const found = findRunSummaryInObject(call.result, void 0, 24);
219
+ if (found) return found;
220
+ }
221
+ }
222
+ }
223
+ if (Array.isArray(s.messages)) {
224
+ const msgs = (
225
+ /** @type {Array<Record<string, unknown>>} */
226
+ s.messages
227
+ );
228
+ for (let i = msgs.length - 1; i >= 0; i--) {
229
+ const msg = msgs[i];
230
+ if (!msg) continue;
231
+ const found = findRunSummaryInObject(msg.content);
232
+ if (found) return found;
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+ function extractVerdict(runSummary) {
238
+ if (!isRunSummary(runSummary)) return null;
239
+ const o = (
240
+ /** @type {Record<string, unknown>} */
241
+ runSummary
242
+ );
243
+ const final = (
244
+ /** @type {Record<string, unknown>} */
245
+ o.finalReport
246
+ );
247
+ const meta = final.metaReview && typeof final.metaReview === "object" ? (
248
+ /** @type {Record<string, unknown>} */
249
+ final.metaReview
250
+ ) : {};
251
+ const issues = Array.isArray(meta.issues) ? meta.issues : [];
252
+ const roundsVal = o.rounds;
253
+ const totalRounds = typeof roundsVal === "number" ? roundsVal : Array.isArray(roundsVal) ? roundsVal.length : 0;
254
+ return {
255
+ verdict: final.verdict === "needs_revision" ? "needs_revision" : "approved",
256
+ reportIssues: issues.length,
257
+ checksRun: typeof meta.checksRun === "number" ? meta.checksRun : 0,
258
+ converged: o.converged === true,
259
+ totalRounds,
260
+ totalFindings: typeof o.totalFindings === "number" ? o.totalFindings : 0
261
+ };
262
+ }
263
+ function normalizeReport(report) {
264
+ const convergence = (
265
+ /** @type {Record<string, unknown>} */
266
+ report.convergence ?? {}
267
+ );
268
+ const rounds = (
269
+ /** @type {Array<unknown>} */
270
+ report.rounds ?? []
271
+ );
272
+ const findings = (
273
+ /** @type {Array<Record<string, unknown>>} */
274
+ report.findings ?? []
275
+ );
276
+ const totalRounds = typeof convergence.totalRounds === "number" ? convergence.totalRounds : rounds.length;
277
+ const normalizedConvergence = {
278
+ totalRounds,
279
+ findingsByRound: Array.isArray(convergence.findingsByRound) ? convergence.findingsByRound : rounds.map((r) => {
280
+ const rr = (
281
+ /** @type {Record<string, unknown>} */
282
+ r
283
+ );
284
+ return Array.isArray(rr?.findings) ? rr.findings.length : 0;
285
+ }),
286
+ converged: convergence.converged === true,
287
+ stoppedReason: convergence.stoppedReason ?? (rounds.length < totalRounds ? "converged" : "max_rounds_reached")
288
+ };
289
+ let summary = report.summary;
290
+ if (!summary || typeof summary !== "object") {
291
+ summary = computeSummaryFromFindings(findings);
292
+ } else {
293
+ const s = (
294
+ /** @type {Record<string, unknown>} */
295
+ summary
296
+ );
297
+ const computed = computeSummaryFromFindings(findings);
298
+ summary = {
299
+ totalFindings: typeof s.totalFindings === "number" ? s.totalFindings : findings.length,
300
+ critical: typeof s.critical === "number" ? s.critical : computed.critical,
301
+ high: typeof s.high === "number" ? s.high : computed.high,
302
+ medium: typeof s.medium === "number" ? s.medium : computed.medium,
303
+ low: typeof s.low === "number" ? s.low : computed.low,
304
+ byDimension: s.byDimension && typeof s.byDimension === "object" ? s.byDimension : computed.byDimension,
305
+ ...typeof s.fixedCount === "number" ? { fixedCount: s.fixedCount } : {}
306
+ };
307
+ }
308
+ return {
309
+ mode: report.mode ?? "dry-run",
310
+ goal: report.goal ?? "",
311
+ dimensions: Array.isArray(report.dimensions) ? report.dimensions : [],
312
+ maxReviewRounds: report.maxReviewRounds ?? totalRounds,
313
+ rounds,
314
+ findings,
315
+ convergence: normalizedConvergence,
316
+ summary
317
+ };
318
+ }
319
+ function computeSummaryFromFindings(findings) {
320
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
321
+ const byDimension = {};
322
+ for (const f of findings) {
323
+ const sev = String(f.severity ?? "low");
324
+ if (sev in counts) counts[sev]++;
325
+ const dim = String(f.dimension ?? "unknown");
326
+ byDimension[dim] = (byDimension[dim] ?? 0) + 1;
327
+ }
328
+ return {
329
+ totalFindings: findings.length,
330
+ critical: counts.critical,
331
+ high: counts.high,
332
+ medium: counts.medium,
333
+ low: counts.low,
334
+ byDimension
335
+ };
336
+ }
337
+ function computeConvergenceProgress(report) {
338
+ const convergence = (
339
+ /** @type {Record<string, unknown>} */
340
+ report.convergence ?? {}
341
+ );
342
+ const totalRounds = typeof convergence.totalRounds === "number" ? convergence.totalRounds : 1;
343
+ const currentRounds = (
344
+ /** @type {Array<unknown>} */
345
+ (report.rounds ?? []).length
346
+ );
347
+ if (!(totalRounds > 0)) return 0;
348
+ return Math.min(100, Math.round(currentRounds / totalRounds * 100));
349
+ }
350
+ function getCurrentRound(report) {
351
+ return (
352
+ /** @type {Array<unknown>} */
353
+ (report.rounds ?? []).length
354
+ );
355
+ }
356
+ function getTotalRounds(report) {
357
+ const convergence = (
358
+ /** @type {Record<string, unknown>} */
359
+ report.convergence ?? {}
360
+ );
361
+ return typeof convergence.totalRounds === "number" ? convergence.totalRounds : 1;
362
+ }
363
+ function severityStats(report) {
364
+ const findings = (
365
+ /** @type {Array<Record<string, unknown>>} */
366
+ report.findings ?? []
367
+ );
368
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
369
+ for (const f of findings) {
370
+ const sev = String(f.severity ?? "low");
371
+ if (sev in counts) counts[sev]++;
372
+ }
373
+ return counts;
374
+ }
375
+ function groupByDimension(report) {
376
+ const findings = (
377
+ /** @type {Array<Record<string, unknown>>} */
378
+ report.findings ?? []
379
+ );
380
+ const groups = {};
381
+ for (const f of findings) {
382
+ const dim = String(f.dimension ?? "unknown");
383
+ if (!groups[dim]) groups[dim] = [];
384
+ groups[dim].push(f);
385
+ }
386
+ return groups;
387
+ }
388
+ function buildTriageState(report) {
389
+ const findings = (
390
+ /** @type {Array<unknown>} */
391
+ report.findings ?? []
392
+ );
393
+ const state = {};
394
+ for (let i = 0; i < findings.length; i++) {
395
+ state[String(i)] = "keep";
396
+ }
397
+ return state;
398
+ }
399
+ function hashReport(report) {
400
+ const convergence = (
401
+ /** @type {Record<string, unknown>} */
402
+ report.convergence ?? {}
403
+ );
404
+ const totalRounds = String(convergence.totalRounds ?? "");
405
+ const findingsCount = String(
406
+ /** @type {Array<unknown>} */
407
+ (report.findings ?? []).length
408
+ );
409
+ const firstFinding = (
410
+ /** @type {Array<Record<string, unknown>>} */
411
+ (report.findings ?? [])[0]
412
+ );
413
+ const firstSummary = firstFinding ? String(firstFinding.summary ?? "") : "";
414
+ const mode = String(report.mode ?? "");
415
+ return `iterate-triage-${mode}-${totalRounds}-${findingsCount}-${firstSummary.slice(0, 20)}`;
416
+ }
417
+ function toKnownIntentionalYaml(entries) {
418
+ if (!entries || entries.length === 0) return "";
419
+ const lines = ["known_intentional:"];
420
+ for (const e of entries) {
421
+ lines.push(` - file: ${JSON.stringify(e.file)}`);
422
+ if (e.line !== void 0 && e.line > 0) {
423
+ lines.push(` line: ${e.line}`);
424
+ }
425
+ lines.push(` dimension: ${JSON.stringify(e.dimension)}`);
426
+ lines.push(` reason: ${JSON.stringify(e.reason)}`);
427
+ }
428
+ return lines.join("\n");
429
+ }
430
+ function buildApplyInstruction(entries) {
431
+ if (!entries || entries.length === 0) return "";
432
+ const payload = JSON.stringify(
433
+ {
434
+ operation: "apply",
435
+ entries: entries.map((e) => ({
436
+ file: e.file,
437
+ ...e.line !== void 0 ? { line: e.line } : {},
438
+ dimension: e.dimension,
439
+ reason: e.reason
440
+ }))
441
+ },
442
+ null,
443
+ 2
444
+ );
445
+ return `Please call \`iterate_triage\` with the following payload to apply the triage verdicts:
446
+
447
+ \`\`\`json
448
+ ${payload}
449
+ \`\`\``;
450
+ }
451
+ function collectIgnoredEntries(triageState, findings) {
452
+ const entries = [];
453
+ for (const [idx, verdict] of Object.entries(triageState)) {
454
+ if (verdict !== "ignore") continue;
455
+ const finding = findings[Number(idx)];
456
+ if (!finding) continue;
457
+ entries.push({
458
+ file: String(finding.file ?? ""),
459
+ ...typeof finding.line === "number" && finding.line > 0 ? { line: finding.line } : {},
460
+ dimension: String(finding.dimension ?? ""),
461
+ reason: String(finding.summary ?? "")
462
+ });
463
+ }
464
+ return entries;
465
+ }
466
+ function normalizeFindingFilter(filter) {
467
+ const f = filter && typeof filter === "object" ? filter : {};
468
+ const severities = Array.isArray(f.severities) ? f.severities.filter((s) => SEVERITY_ORDER.includes(String(s))) : [];
469
+ const dimensions = Array.isArray(f.dimensions) ? f.dimensions.filter((d) => typeof d === "string" && d.length > 0) : [];
470
+ const search = typeof f.search === "string" ? f.search.trim().toLowerCase() : "";
471
+ return { severities, dimensions, search };
472
+ }
473
+ function findingMatches(finding, filter) {
474
+ const f = normalizeFindingFilter(filter);
475
+ const sev = String(finding.severity ?? "low");
476
+ if (f.severities.length > 0 && !f.severities.includes(sev)) return false;
477
+ const dim = String(finding.dimension ?? "");
478
+ if (f.dimensions.length > 0 && !f.dimensions.includes(dim)) return false;
479
+ if (f.search) {
480
+ const haystack = [
481
+ String(finding.file ?? ""),
482
+ String(finding.summary ?? ""),
483
+ String(finding.dimension ?? ""),
484
+ String(finding.suggested_fix ?? "")
485
+ ].join(" ").toLowerCase();
486
+ if (haystack.indexOf(f.search) < 0) return false;
487
+ }
488
+ return true;
489
+ }
490
+ function filterFindingsWithIndices(findings, filter) {
491
+ const f = normalizeFindingFilter(filter);
492
+ const list = Array.isArray(findings) ? findings : [];
493
+ const filtered = [];
494
+ const indices = [];
495
+ for (let i = 0; i < list.length; i++) {
496
+ if (findingMatches(list[i], f)) {
497
+ filtered.push(list[i]);
498
+ indices.push(i);
499
+ }
500
+ }
501
+ return { filtered, indices };
502
+ }
503
+ function buildFilterOptions(findings) {
504
+ const list = Array.isArray(findings) ? findings : [];
505
+ const severities = SEVERITY_ORDER.map((value) => ({ value, count: 0 }));
506
+ const dimCounts = {};
507
+ for (const f of list) {
508
+ const sev = String(f.severity ?? "low");
509
+ const sv = severities.find((s) => s.value === sev);
510
+ if (sv) sv.count++;
511
+ const dim = String(f.dimension ?? "unknown");
512
+ dimCounts[dim] = (dimCounts[dim] ?? 0) + 1;
513
+ }
514
+ return {
515
+ severities: severities.map((s) => ({ ...s })),
516
+ dimensions: Object.keys(dimCounts).map((value) => ({ value, count: dimCounts[value] }))
517
+ };
518
+ }
519
+ function countVerdicts(triageState) {
520
+ const counts = { keep: 0, skip: 0, ignore: 0 };
521
+ for (const v of Object.values(triageState ?? {})) {
522
+ if (v === "keep" || v === "skip" || v === "ignore") counts[v]++;
523
+ }
524
+ return counts;
525
+ }
526
+ function batchSetVerdict(triageState, indices, verdict) {
527
+ if (verdict !== "keep" && verdict !== "skip" && verdict !== "ignore") return triageState;
528
+ if (!Array.isArray(indices) || indices.length === 0) return triageState;
529
+ const next = { ...triageState };
530
+ for (const idx of indices) {
531
+ if (typeof idx === "number" && Number.isInteger(idx) && idx >= 0) {
532
+ next[String(idx)] = verdict;
533
+ }
534
+ }
535
+ return next;
536
+ }
537
+ function setAllVerdicts(triageState, verdict, indices) {
538
+ const targets = Array.isArray(indices) ? indices : Object.keys(triageState ?? {}).map(Number);
539
+ return batchSetVerdict(triageState, targets, verdict);
540
+ }
541
+ function buildRoundHistory(report) {
542
+ const rounds = Array.isArray(report.rounds) ? report.rounds : [];
543
+ return rounds.map((r) => {
544
+ const rr = (
545
+ /** @type {Record<string, unknown>} */
546
+ r
547
+ );
548
+ const findings = Array.isArray(rr.findings) ? rr.findings : [];
549
+ const sev = severityStats({ findings });
550
+ return {
551
+ round: typeof rr.round === "number" ? rr.round : 0,
552
+ count: findings.length,
553
+ critical: sev.critical,
554
+ high: sev.high,
555
+ medium: sev.medium,
556
+ low: sev.low
557
+ };
558
+ });
559
+ }
560
+ function buildFindingTrend(report) {
561
+ const conv = (
562
+ /** @type {Record<string, unknown>} */
563
+ report.convergence ?? {}
564
+ );
565
+ if (Array.isArray(conv.findingsByRound)) {
566
+ return conv.findingsByRound.map((n, i) => ({ round: i + 1, count: typeof n === "number" ? n : 0 }));
567
+ }
568
+ return buildRoundHistory(report).map((h) => ({ round: h.round, count: h.count }));
569
+ }
570
+ function computeTrendMetrics(report) {
571
+ const conv = (
572
+ /** @type {Record<string, unknown>} */
573
+ report.convergence ?? {}
574
+ );
575
+ const points = buildFindingTrend(report);
576
+ const total = points.reduce((sum, p) => sum + p.count, 0);
577
+ const firstRound = points.length > 0 ? points[0].count : 0;
578
+ const lastRound = points.length > 0 ? points[points.length - 1].count : 0;
579
+ const reductionPercent = firstRound > 0 ? Math.round((firstRound - lastRound) / firstRound * 100) : 0;
580
+ return {
581
+ points,
582
+ total,
583
+ firstRound,
584
+ lastRound,
585
+ reductionPercent,
586
+ converged: conv.converged === true
587
+ };
588
+ }
589
+ function trendMax(points) {
590
+ let max = 1;
591
+ for (const p of Array.isArray(points) ? points : []) {
592
+ if (typeof p.count === "number" && p.count > max) max = p.count;
593
+ }
594
+ return max;
595
+ }
596
+ function buildCompletionSummary(report) {
597
+ const conv = (
598
+ /** @type {Record<string, unknown>} */
599
+ report.convergence ?? {}
600
+ );
601
+ const rounds = getCurrentRound(report);
602
+ const total = getTotalRounds(report);
603
+ const stats = severityStats(report);
604
+ const converged = conv.converged === true;
605
+ const reason = converged ? "\u5DF2\u6536\u655B" : `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`;
606
+ const totalFindings = stats.critical + stats.high + stats.medium + stats.low;
607
+ return `iterate \u8BC4\u5BA1\u5B8C\u6210 \xB7 ${rounds}/${total} \u8F6E \xB7 ${totalFindings} \u9879\u53D1\u73B0 \xB7 ${reason}`;
608
+ }
609
+ var CONFIG_EDIT_FIELDS = [
610
+ { key: "goal", label: "\u76EE\u6807", hint: "\u4E00\u53E5\u8BDD\u63CF\u8FF0\u672C\u6B21\u8FED\u4EE3\u76EE\u6807\uFF08\u5B57\u7B26\u4E32\uFF09" },
611
+ { key: "dimensions", label: "\u5BA1\u67E5\u7EF4\u5EA6", hint: '\u6570\u7EC4\uFF0C\u5982 ["correctness","security"]' },
612
+ { key: "max_rounds", label: "\u6700\u5927\u8F6E\u6570", hint: "\u6B63\u6574\u6570" },
613
+ { key: "review.scope", label: "\u5BA1\u67E5\u8303\u56F4", hint: '"full" \u6216 "changed-only"' },
614
+ { key: "atomic.max_lines", label: "\u539F\u5B50\u4FEE\u590D\u4E0A\u9650\u884C\u6570", hint: "\u6B63\u6574\u6570" },
615
+ { key: "git.push_per_round", label: "\u6BCF\u8F6E\u63A8\u9001", hint: "true / false" }
616
+ ];
617
+ function buildConfigEditGuide() {
618
+ const lines = [
619
+ "iterate \u914D\u7F6E\u7F16\u8F91\u6307\u5F15",
620
+ "---------------------",
621
+ "\u914D\u7F6E\u6587\u4EF6\uFF1A\u9879\u76EE\u6839\u76EE\u5F55 iterate.config.yaml\u3002",
622
+ "",
623
+ "\u53EF\u7F16\u8F91\u5B57\u6BB5\uFF1A",
624
+ ...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}\uFF08${f.label}\uFF09\uFF1A${f.hint}`),
625
+ "",
626
+ "\u8BA9\u6A21\u578B\u5E2E\u4F60\u6539\uFF1A",
627
+ '1. \u8C03\u7528 iterate_config({ operation: "read" }) \u67E5\u770B\u5F53\u524D\u914D\u7F6E\uFF1B',
628
+ "2. \u8BF4\u660E\u60F3\u6539\u7684\u5B57\u6BB5\uFF0C\u4F8B\u5982\u300C\u628A max_rounds \u6539\u6210 5\uFF0Cdimensions \u53EA\u4FDD\u7559 correctness \u548C security\u300D\uFF1B",
629
+ '3. \u6A21\u578B\u4F1A\u8C03\u7528 iterate_config({ operation: "write", updates: {...} }) \u5199\u5165\uFF0C\u5199\u5165\u524D\u81EA\u52A8\u5907\u4EFD\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\u3002'
630
+ ];
631
+ return lines.join("\n");
632
+ }
633
+ var VERDICT_SHORTCUTS = {
634
+ y: "keep",
635
+ Y: "keep",
636
+ n: "skip",
637
+ N: "skip",
638
+ a: "ignore",
639
+ A: "ignore"
640
+ };
641
+ function keyToVerdict(key) {
642
+ return VERDICT_SHORTCUTS[key] ?? null;
643
+ }
644
+ function allVerdictKeys(triageState) {
645
+ const state = triageState && typeof triageState === "object" ? triageState : {};
646
+ return Object.keys(state).map(Number).filter((n) => Number.isInteger(n) && n >= 0).sort((a, b) => a - b);
647
+ }
648
+ var RUNTIME_ARTIFACTS = [
649
+ {
650
+ key: "decision-log.jsonl",
651
+ label: "\u51B3\u7B56\u65E5\u5FD7",
652
+ hint: "\u8FFD\u52A0\u5F0F JSONL\uFF0C\u8BB0\u5F55\u6BCF\u8F6E plan / review / fix / revert / validation \u51B3\u7B56"
653
+ },
654
+ {
655
+ key: "checkpoint.json",
656
+ label: "\u8FED\u4EE3\u65AD\u70B9",
657
+ hint: "\u957F\u8FED\u4EE3\u7684\u8FDB\u5EA6\u5FEB\u7167\uFF0C\u4E2D\u65AD\u540E\u53EF\u6062\u590D\uFF08iterate_checkpoint\uFF09"
658
+ },
659
+ {
660
+ key: "fixes/registry.json",
661
+ label: "\u4FEE\u590D\u6CE8\u518C\u8868",
662
+ hint: "\u6BCF\u4E2A\u539F\u5B50\u4FEE\u590D\u7684 id / diff / \u5907\u4EFD\u8DEF\u5F84\uFF08iterate_fix / iterate_diff\uFF09"
663
+ },
664
+ {
665
+ key: "fixes/*.bak",
666
+ label: "\u4FEE\u590D\u5907\u4EFD",
667
+ hint: "\u6BCF\u6B21\u4FEE\u590D\u524D\u7684\u539F\u6587\u4EF6\u5907\u4EFD\uFF0C\u56DE\u6EDA\u4F9D\u8D56\uFF08iterate_rollback\uFF09"
668
+ }
669
+ ];
670
+ function buildRuntimeStatusGuide() {
671
+ const lines = [
672
+ "iterate \u8FD0\u884C\u65F6\u72B6\u6001\u6982\u89C8",
673
+ "----------------------",
674
+ "\u6240\u6709\u8FD0\u884C\u65F6\u4EA7\u7269\u4F4D\u4E8E\u9879\u76EE\u6839\u76EE\u5F55 .iterate/ \u4E0B\uFF1A",
675
+ "",
676
+ ...RUNTIME_ARTIFACTS.map((a) => `- ${a.key}\uFF08${a.label}\uFF09\uFF1A${a.hint}`),
677
+ "",
678
+ "\u67E5\u770B\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_status\uFF08\u6C47\u603B\uFF09\u6216 iterate_history\uFF08\u660E\u7EC6\uFF09\u3002",
679
+ "\u6E05\u7406\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_prune\uFF08\u9ED8\u8BA4 dry-run\uFF0C\u53EA\u62A5\u544A\u4E0D\u5220\u9664\uFF0C\u663E\u5F0F dryRun:false \u624D\u771F\u6B63\u6E05\u7406\uFF09\u3002"
680
+ ];
681
+ return lines.join("\n");
84
682
  }
85
683
 
86
- /** Injected stylesheet. Every selector is prefixed to avoid clobbering dsh. */
87
- const ITERATE_CSS = `
684
+ // src/client/index.ts
685
+ var name = "iterate-plugin";
686
+ var inject = ["slots", "theme"];
687
+ var PLUGIN_TAG = "iterate-ui";
688
+ var TRIAGE_STORAGE_PREFIX = "iterate.triage.";
689
+ var THEME_STORAGE_KEY = "iterate.theme.enabled";
690
+ var THEME_SOURCE = "iterate";
691
+ var ITERATE_TOKENS = {
692
+ "--dsw-alias-bg-base": { light: "#FAF8F5", dark: "#171412" },
693
+ "--dsw-alias-bg-layer-1": { light: "#FFFFFF", dark: "#1F1B17" },
694
+ "--dsw-alias-bg-layer-2": { light: "#F4F1EA", dark: "#27221C" },
695
+ "--dsw-alias-bg-overlay": { light: "rgba(255,255,255,0.96)", dark: "rgba(23,20,18,0.96)" },
696
+ "--dsw-alias-border-l1": { light: "#E8E2D8", dark: "#332C25" },
697
+ "--dsw-alias-border-l2": { light: "#DDD5C9", dark: "#3C342B" },
698
+ "--dsw-alias-brand-primary": { light: "#B45309", dark: "#F59E0B" },
699
+ "--dsw-alias-label-primary": { light: "#1C1917", dark: "#F5F0EA" },
700
+ "--dsw-alias-label-secondary": { light: "#57534E", dark: "#A9A29B" },
701
+ "--dsw-alias-state-error-primary": { light: "#DC2626", dark: "#F87171" },
702
+ "--dsw-alias-state-success-primary": { light: "#15803D", dark: "#4ADE80" },
703
+ "--dsw-alias-state-warn-primary": { light: "#D97706", dark: "#FBBF24" },
704
+ "--dsw-specific-sidebar-fill": { light: "#F5F2EC", dark: "#14110E" }
705
+ };
706
+ var ITERATE_CSS = `
88
707
  [data-iterate-root] { box-sizing: border-box; font-family: var(--dsw-font-sans, system-ui, sans-serif); }
89
708
  [data-iterate-root] * , [data-iterate-root] *::before, [data-iterate-root] *::after { box-sizing: border-box; }
90
709
 
@@ -159,8 +778,6 @@ const ITERATE_CSS = `
159
778
  @keyframes iterate-fadein { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
160
779
 
161
780
  .iterate-settings { display: flex; flex-direction: column; gap: 12px; padding: 4px 2px 12px; }
162
- .iterate-settings-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--dsw-alias-border-l1); }
163
- .iterate-settings-row:last-child { border-bottom: none; }
164
781
  .iterate-settings-title { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); }
165
782
  .iterate-settings-desc { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: 3px; line-height: 1.5; }
166
783
  .iterate-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); }
@@ -195,738 +812,916 @@ const ITERATE_CSS = `
195
812
  .iterate-completion[data-ok] { border: 1px solid var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); }
196
813
  .iterate-completion[data-warn] { border: 1px solid var(--dsw-alias-state-warn-primary); color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent); }
197
814
  .iterate-capsule[data-ok] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
198
- .iterate-settings-guide { white-space: pre-wrap; padding: 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 11px; max-height: 180px; overflow: auto; }
199
815
  .iterate-chip[data-ok] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); }
200
816
  .iterate-batch-check { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
201
817
  .iterate-batch-check input { margin: 0; cursor: pointer; }
202
- `
203
-
204
- // ─── Small helpers ───────────────────────────────────────────────────────────
205
818
 
206
- /** Namespaced logger (kept minimal; only errors/warnings, no debug spam). */
819
+ /* Meta-review verdict banner (dry-run closing result) */
820
+ .iterate-verdict { margin: 10px 0; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12px; color: var(--dsw-alias-label-primary); }
821
+ .iterate-verdict-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-weight: 600; white-space: nowrap; }
822
+ .iterate-verdict-tag[data-ok] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent); }
823
+ .iterate-verdict-tag[data-warn] { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent); }
824
+ .iterate-verdict-detail { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; color: var(--dsw-alias-label-secondary); }
825
+ .iterate-verdict-item { white-space: nowrap; }
826
+ .iterate-verdict-item b { color: var(--dsw-alias-label-primary); font-weight: 600; }
827
+
828
+ /* Settings page redesign: grouped cards + switch + code blocks. */
829
+ .iterate-settings { display: flex; flex-direction: column; gap: 14px; padding: 6px 2px 16px; }
830
+ .iterate-scard { border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); padding: 14px 16px; }
831
+ .iterate-scard-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
832
+ .iterate-scard-title { font-size: 14px; font-weight: 650; color: var(--dsw-alias-label-primary); letter-spacing: -0.01em; }
833
+ .iterate-scard-desc { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); margin-top: 4px; }
834
+ .iterate-scard-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
835
+
836
+ /* Status pill */
837
+ .iterate-pill { display: inline-flex; align-items: center; gap: 7px; padding: 3px 11px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
838
+ .iterate-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
839
+
840
+ /* Accessibility-switch toggle */
841
+ .iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
842
+ .iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
843
+ .iterate-switch-knob { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--dsw-alias-label-secondary); transition: transform 160ms ease, background-color 160ms ease; }
844
+ .iterate-switch[data-on] { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); }
845
+ .iterate-switch[data-on] .iterate-switch-knob { transform: translateX(18px); background: #FFFFFF; }
846
+
847
+ /* Button variants */
848
+ .iterate-btn[data-ghost] { background: transparent; }
849
+ .iterate-btn[data-danger] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); color: var(--dsw-alias-state-error-primary); background: transparent; }
850
+ .iterate-btn[data-danger]:hover { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
851
+ .iterate-btn[data-confirm] { border-color: var(--dsw-alias-state-error-primary); color: #FFFFFF; background: var(--dsw-alias-state-error-primary); }
852
+
853
+ /* Collapsible guide / status code blocks */
854
+ .iterate-guide { margin-top: 4px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: hidden; background: var(--dsw-alias-bg-layer-2); }
855
+ .iterate-guide-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 12px; background: var(--dsw-alias-bg-layer-2); border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 12px; color: var(--dsw-alias-label-secondary); }
856
+ .iterate-guide-body { padding: 12px 14px; font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 11.5px; line-height: 1.75; white-space: pre-wrap; color: var(--dsw-alias-label-primary); max-height: 260px; overflow: auto; }
857
+ `;
207
858
  function log(...args) {
208
- if (typeof console !== 'undefined' && typeof console.error === 'function') {
209
- console.error(`[${PLUGIN_TAG}]`, ...args)
859
+ if (typeof console !== "undefined" && typeof console.error === "function") {
860
+ console.error(`[${PLUGIN_TAG}]`, ...args);
210
861
  }
211
862
  }
212
-
213
- /** Resolve the React runtime (ctx.React first, then window.React). */
214
- function resolveReact(ctx) {
215
- if (ctx && ctx.React && typeof ctx.React.createElement === 'function') return ctx.React
216
- const globalScope = typeof window !== 'undefined' ? window : typeof globalThis !== 'undefined' ? globalThis : null
217
- if (globalScope && globalScope.React && typeof globalScope.React.createElement === 'function') {
218
- return globalScope.React
219
- }
220
- return null
221
- }
222
-
223
- /** Safe localStorage wrapper (never throws in private mode / SSR). */
224
863
  function createStorage() {
225
864
  try {
226
- const testKey = '__iterate_storage_test__'
227
- window.localStorage.setItem(testKey, '1')
228
- window.localStorage.removeItem(testKey)
865
+ const testKey = "__iterate_storage_test__";
866
+ window.localStorage.setItem(testKey, "1");
867
+ window.localStorage.removeItem(testKey);
229
868
  return {
230
- get(key) { return window.localStorage.getItem(key) },
231
- set(key, value) { window.localStorage.setItem(key, value) },
232
- remove(key) { window.localStorage.removeItem(key) },
233
- keys() { return Object.keys(window.localStorage) },
234
- }
869
+ get(key) {
870
+ return window.localStorage.getItem(key);
871
+ },
872
+ set(key, value) {
873
+ window.localStorage.setItem(key, value);
874
+ },
875
+ remove(key) {
876
+ window.localStorage.removeItem(key);
877
+ },
878
+ keys() {
879
+ return Object.keys(window.localStorage);
880
+ }
881
+ };
235
882
  } catch {
236
- const mem = new Map()
883
+ const mem = /* @__PURE__ */ new Map();
237
884
  return {
238
- get(key) { return mem.has(key) ? mem.get(key) : null },
239
- set(key, value) { mem.set(key, value) },
240
- remove(key) { mem.delete(key) },
241
- keys() { return [...mem.keys()] },
242
- }
885
+ get(key) {
886
+ return mem.has(key) ? mem.get(key) : null;
887
+ },
888
+ set(key, value) {
889
+ mem.set(key, value);
890
+ },
891
+ remove(key) {
892
+ mem.delete(key);
893
+ },
894
+ keys() {
895
+ return [...mem.keys()];
896
+ }
897
+ };
243
898
  }
244
899
  }
245
-
246
- /**
247
- * Remove every stored key with the given prefix (e.g. all triage verdicts).
248
- * Returns how many keys were removed.
249
- */
250
900
  function removeStorageByPrefix(prefix) {
251
- if (!storage) return 0
252
- let removed = 0
901
+ if (!storage) return 0;
902
+ let removed = 0;
253
903
  try {
254
904
  for (const key of storage.keys()) {
255
905
  if (key.startsWith(prefix)) {
256
- storage.remove(key)
257
- removed++
906
+ storage.remove(key);
907
+ removed++;
258
908
  }
259
909
  }
260
910
  } catch (err) {
261
- log('failed to clear storage prefix', prefix, err)
911
+ log("failed to clear storage prefix", prefix, err);
262
912
  }
263
- return removed
913
+ return removed;
264
914
  }
265
-
266
- /** Copy text to the clipboard, returning whether it succeeded. */
267
915
  function copyText(text) {
268
- if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
916
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
269
917
  navigator.clipboard.writeText(text).then(
270
918
  () => true,
271
- () => false,
272
- )
273
- return true
919
+ () => false
920
+ );
921
+ return true;
274
922
  }
275
- return false
923
+ return false;
276
924
  }
277
-
278
- // ─── Module-level runtime state (shared across slots) ────────────────────────
279
-
280
- let React = null
281
- let slotsSvc = null
282
- let themeSvc = null
283
- let storage = null
284
- let themeDisposer = null
285
- let themeEnabled = true
286
- const roundPulseListeners = []
287
-
288
- /**
289
- * Emit a "round changed" pulse to subscribed components.
290
- * The payload is `{ round, converged }` so the overlay capsule can surface
291
- * completion AND convergence in one notification.
292
- */
925
+ var SEVERITY_KEYS = ["critical", "high", "medium", "low"];
926
+ function coerceSeverity(severity) {
927
+ return SEVERITY_KEYS.includes(severity ?? "") ? severity : "low";
928
+ }
929
+ function severityColor(severity) {
930
+ return SEVERITY_COLOR[coerceSeverity(severity)];
931
+ }
932
+ function severityLabel(severity) {
933
+ return SEVERITY_LABEL[coerceSeverity(severity)];
934
+ }
935
+ function readSlots(ctx) {
936
+ if (ctx && ctx.slots && typeof ctx.slots.inject === "function" && typeof ctx.slots.register === "function") {
937
+ return ctx.slots;
938
+ }
939
+ const raw = ctx && typeof ctx.get === "function" ? ctx.get("slots", false) : void 0;
940
+ if (raw && typeof raw.inject === "function" && typeof raw.register === "function") {
941
+ return raw;
942
+ }
943
+ return void 0;
944
+ }
945
+ function readTheme(ctx) {
946
+ if (ctx && ctx.theme && typeof ctx.theme.overrideTokens === "function") {
947
+ return ctx.theme;
948
+ }
949
+ const raw = ctx && typeof ctx.get === "function" ? ctx.get("theme", false) : void 0;
950
+ if (raw && typeof raw.overrideTokens === "function") {
951
+ return raw;
952
+ }
953
+ return void 0;
954
+ }
955
+ var slotsSvc = void 0;
956
+ var themeSvc = void 0;
957
+ var storage = null;
958
+ var themeDisposer = null;
959
+ var themeEnabled = true;
960
+ var roundPulseListeners = [];
293
961
  function emitRoundPulse(round, converged) {
294
- const payload = { round, converged: converged === true }
295
- for (const fn of roundPulseListeners.slice()) fn(payload)
962
+ const payload = { round, converged: converged === true };
963
+ for (const fn of roundPulseListeners.slice()) fn(payload);
296
964
  }
297
-
298
- /** Apply the iterate theme skin (idempotent). */
299
965
  function applyThemeSkin() {
300
- if (!themeSvc || typeof themeSvc.overrideTokens !== 'function') return
301
- // Never stack disposers: drop any previously applied override first.
302
- clearThemeSkin()
303
- themeDisposer = themeSvc.overrideTokens(THEME_SOURCE, ITERATE_TOKENS)
966
+ if (!themeSvc || typeof themeSvc.overrideTokens !== "function") return;
967
+ clearThemeSkin();
968
+ themeDisposer = themeSvc.overrideTokens(THEME_SOURCE, ITERATE_TOKENS);
304
969
  }
305
-
306
- /** Remove the iterate theme skin (idempotent). */
307
970
  function clearThemeSkin() {
308
971
  if (themeDisposer) {
309
- try { themeDisposer() } catch (err) { log('theme disposer failed', err) }
310
- themeDisposer = null
972
+ try {
973
+ themeDisposer();
974
+ } catch (err) {
975
+ log("theme disposer failed", err);
976
+ }
977
+ themeDisposer = null;
311
978
  }
312
979
  }
313
-
314
- /** Persist + apply the theme preference. */
315
980
  function setThemeEnabled(enabled) {
316
- themeEnabled = enabled
317
- if (storage) storage.set(THEME_STORAGE_KEY, enabled ? '1' : '0')
318
- if (enabled) applyThemeSkin()
319
- else clearThemeSkin()
981
+ themeEnabled = enabled;
982
+ if (storage) storage.set(THEME_STORAGE_KEY, enabled ? "1" : "0");
983
+ if (enabled) applyThemeSkin();
984
+ else clearThemeSkin();
320
985
  }
321
-
322
- // ─── Components (React.createElement trees) ──────────────────────────────────
323
-
324
- /** Obtain a session snapshot defensively from the slot props. */
325
986
  function sessionSnapshot(props) {
326
- let session = null
327
- if (props && typeof props.useSession === 'function') {
328
- try { session = props.useSession() } catch (err) { log('useSession failed', err) }
987
+ let session = null;
988
+ const useSession = props && typeof props.useSession === "function" ? props.useSession : null;
989
+ if (useSession) {
990
+ try {
991
+ session = useSession();
992
+ } catch (err) {
993
+ log("useSession failed", err);
994
+ }
329
995
  }
330
- if (!session && props && props.session) session = props.session
331
- return session
996
+ if (!session && props && props.session) session = props.session;
997
+ return session;
332
998
  }
333
-
334
- /** Find the latest report inside a session snapshot (normalized). */
335
999
  function latestReport(session) {
336
- if (!session) return null
337
- const raw = scanSessionForReport(session) || findReportInObject(session, undefined, 24)
338
- return raw ? normalizeReport(raw) : null
1000
+ if (!session) return null;
1001
+ const raw = scanSessionForReport(session) || findReportInObject(session, void 0, 24);
1002
+ return raw ? normalizeReport(raw) : null;
339
1003
  }
340
-
341
- /** Inline bar chart of findings-by-round (pure divs, no SVG needed). */
342
1004
  function TrendChart({ points }) {
343
- if (!points || points.length === 0) return null
344
- const max = trendMax(points)
345
- const bars = points.map((p) =>
346
- React.createElement('div', {
1005
+ if (!points || points.length === 0) return null;
1006
+ const max = trendMax(points);
1007
+ const bars = points.map(
1008
+ (p) => React.createElement("div", {
347
1009
  key: p.round,
348
- className: 'iterate-trend-bar',
349
- 'data-hot': p.count > 0 ? '' : undefined,
350
- title: `Round ${p.round}:${p.count} 项`,
351
- style: { height: `${Math.max(4, Math.round((p.count / max) * 24))}px` },
352
- }),
353
- )
354
- return React.createElement('div', { className: 'iterate-trend', title: '各轮发现数量趋势' }, ...bars)
1010
+ className: "iterate-trend-bar",
1011
+ "data-hot": p.count > 0 ? "" : void 0,
1012
+ title: `Round ${p.round}\uFF1A${p.count} \u9879`,
1013
+ style: { height: `${Math.max(4, Math.round(p.count / max * 24))}px` }
1014
+ })
1015
+ );
1016
+ return React.createElement("div", { className: "iterate-trend", title: "\u5404\u8F6E\u53D1\u73B0\u6570\u91CF\u8D8B\u52BF" }, ...bars);
355
1017
  }
356
-
357
- /** Dashboard: live convergence strip above the composer. */
358
1018
  function ConvergenceDashboard(props) {
359
- const [pulseKey, setPulseKey] = React.useState(0)
360
- const report = latestReport(sessionSnapshot(props))
361
-
1019
+ const [pulseKey, setPulseKey] = React.useState(0);
1020
+ const report = latestReport(sessionSnapshot(props));
362
1021
  React.useEffect(() => {
363
- if (!report) return
364
- const cur = getCurrentRound(report)
365
- const conv = report.convergence
366
- emitRoundPulse(cur, conv && conv.converged === true)
367
- setPulseKey((k) => k + 1)
368
- }, [report && hashReport(report) + ':' + getCurrentRound(report)])
369
-
370
- if (!report) return null
371
-
372
- const round = getCurrentRound(report)
373
- const total = getTotalRounds(report)
374
- const progress = computeConvergenceProgress(report)
375
- const stats = severityStats(report)
376
- const dims = groupByDimension(report)
377
- const trend = computeTrendMetrics(report)
378
-
379
- const dimBadges = Object.keys(dims).slice(0, 6).map((dim) =>
380
- React.createElement(
381
- 'span',
382
- { key: dim, className: 'iterate-dim-badge' },
383
- `${dim} · ${dims[dim].length}`,
384
- ),
385
- )
386
-
387
- // Fix-count badge: show when the report has a fixes summary.
388
- const mode = report.mode
389
- const summary = report.summary
390
- const isNormal = mode === 'normal'
391
- const fixCount = isNormal && summary && typeof summary.fixedCount === 'number' ? summary.fixedCount : null
392
- const fixBadge = fixCount !== null
393
- ? React.createElement('span', { className: 'iterate-metric', key: 'fixes' },
394
- '\u{1F527} ' + String(fixCount) + ' fixes',
395
- )
396
- : null
397
-
1022
+ if (!report) return;
1023
+ const cur = getCurrentRound(report);
1024
+ const conv = report.convergence;
1025
+ emitRoundPulse(cur, conv?.converged === true);
1026
+ setPulseKey((k) => k + 1);
1027
+ }, [report && hashReport(report) + ":" + getCurrentRound(report)]);
1028
+ if (!report) return null;
1029
+ const round = getCurrentRound(report);
1030
+ const total = getTotalRounds(report);
1031
+ const progress = computeConvergenceProgress(report);
1032
+ const stats = severityStats(report);
1033
+ const dims = groupByDimension(report);
1034
+ const trend = computeTrendMetrics(report);
1035
+ const dimBadges = Object.keys(dims).slice(0, 6).map(
1036
+ (dim) => React.createElement(
1037
+ "span",
1038
+ { key: dim, className: "iterate-dim-badge" },
1039
+ `${dim} \xB7 ${dims[dim]?.length ?? 0}`
1040
+ )
1041
+ );
1042
+ const mode = report.mode;
1043
+ const summary = report.summary;
1044
+ const isNormal = mode === "normal";
1045
+ const fixCount = isNormal && summary && typeof summary.fixedCount === "number" ? summary.fixedCount : null;
1046
+ const fixBadge = fixCount !== null ? React.createElement("span", {
1047
+ className: "iterate-metric",
1048
+ key: "fixes",
1049
+ title: "\u672C\u8F6E\u5DF2\u5E94\u7528\u7684\u539F\u5B50\u4FEE\u590D\u6570\uFF08\u6B63\u5E38\u6A21\u5F0F\uFF09"
1050
+ }, `${String(fixCount)} fixes`) : null;
398
1051
  return React.createElement(
399
- 'div',
400
- { 'data-iterate-root': '', 'data-iterate': 'dashboard', className: 'iterate-dashboard' },
1052
+ "div",
1053
+ { "data-iterate-root": "", "data-iterate": "dashboard", className: "iterate-dashboard" },
401
1054
  React.createElement(
402
- 'span',
403
- { className: 'iterate-round-badge', 'data-pulse': pulseKey > 0 ? '' : undefined, key: `round-${round}` },
404
- `Round ${round} / ${total}`,
1055
+ "span",
1056
+ { className: "iterate-round-badge", "data-pulse": pulseKey > 0 ? "" : void 0, key: `round-${round}` },
1057
+ `Round ${round} / ${total}`
405
1058
  ),
406
- React.createElement('div', { className: 'iterate-progress' },
407
- React.createElement('div', { className: 'iterate-progress-fill', style: { width: `${progress}%` } }),
1059
+ React.createElement(
1060
+ "div",
1061
+ { className: "iterate-progress" },
1062
+ React.createElement("div", { className: "iterate-progress-fill", style: { width: `${progress}%` } })
408
1063
  ),
409
- React.createElement('span', { className: 'iterate-metric' },
410
- React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR.critical } }),
411
- stats.critical,
1064
+ React.createElement(
1065
+ "span",
1066
+ { className: "iterate-metric" },
1067
+ React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR.critical } }),
1068
+ stats.critical
412
1069
  ),
413
- React.createElement('span', { className: 'iterate-metric' },
414
- React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR.high } }),
415
- stats.high,
1070
+ React.createElement(
1071
+ "span",
1072
+ { className: "iterate-metric" },
1073
+ React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR.high } }),
1074
+ stats.high
416
1075
  ),
417
- React.createElement('span', { className: 'iterate-metric' },
418
- React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR.medium } }),
419
- stats.medium,
1076
+ React.createElement(
1077
+ "span",
1078
+ { className: "iterate-metric" },
1079
+ React.createElement("span", { className: "iterate-sev-dot", style: { background: SEVERITY_COLOR.medium } }),
1080
+ stats.medium
420
1081
  ),
421
1082
  fixBadge,
422
1083
  React.createElement(TrendChart, { points: trend.points }),
423
- ...dimBadges,
424
- )
1084
+ ...dimBadges
1085
+ );
425
1086
  }
426
-
427
- /** Stats card (empty-findings case of the turn-tail chain). */
428
1087
  function StatsCard(props) {
429
- const report = props.report
430
- const [showHistory, setShowHistory] = React.useState(false)
431
- const stats = severityStats(report)
432
- const total = stats.critical + stats.high + stats.medium + stats.low
1088
+ const report = props.report;
1089
+ const [showHistory, setShowHistory] = React.useState(false);
1090
+ const stats = severityStats(report);
1091
+ const total = stats.critical + stats.high + stats.medium + stats.low;
433
1092
  const rows = [
434
- { label: 'Critical', value: stats.critical, color: SEVERITY_COLOR.critical },
435
- { label: 'High', value: stats.high, color: SEVERITY_COLOR.high },
436
- { label: 'Medium', value: stats.medium, color: SEVERITY_COLOR.medium },
437
- { label: 'Low', value: stats.low, color: SEVERITY_COLOR.low },
438
- ].map((r) =>
439
- React.createElement('div', { key: r.label, className: 'iterate-stat' },
440
- React.createElement('div', { className: 'iterate-stat-num', style: { color: r.color } }, String(r.value)),
441
- React.createElement('div', { className: 'iterate-stat-label' }, r.label),
1093
+ { label: "Critical", value: stats.critical, color: SEVERITY_COLOR.critical },
1094
+ { label: "High", value: stats.high, color: SEVERITY_COLOR.high },
1095
+ { label: "Medium", value: stats.medium, color: SEVERITY_COLOR.medium },
1096
+ { label: "Low", value: stats.low, color: SEVERITY_COLOR.low }
1097
+ ].map(
1098
+ (r) => React.createElement(
1099
+ "div",
1100
+ { key: r.label, className: "iterate-stat" },
1101
+ React.createElement("div", { className: "iterate-stat-num", style: { color: r.color } }, String(r.value)),
1102
+ React.createElement("div", { className: "iterate-stat-label" }, r.label)
1103
+ )
1104
+ );
1105
+ const history = buildRoundHistory(report);
1106
+ const trend = computeTrendMetrics(report);
1107
+ const historyRows = history.map(
1108
+ (h) => React.createElement(
1109
+ "tr",
1110
+ { key: h.round },
1111
+ React.createElement("td", { className: "iterate-history-num" }, `Round ${h.round}`),
1112
+ React.createElement("td", {}, String(h.count)),
1113
+ React.createElement("td", { style: { color: SEVERITY_COLOR.critical } }, String(h.critical)),
1114
+ React.createElement("td", { style: { color: SEVERITY_COLOR.high } }, String(h.high)),
1115
+ React.createElement("td", { style: { color: SEVERITY_COLOR.medium } }, String(h.medium)),
1116
+ React.createElement("td", { style: { color: SEVERITY_COLOR.low } }, String(h.low))
1117
+ )
1118
+ );
1119
+ const trendLine = `\u9996\u8F6E ${trend.firstRound} \u2192 \u672B\u8F6E ${trend.lastRound} \u9879\uFF0C\u964D\u5E45 ${trend.reductionPercent}%${trend.converged ? "\uFF0C\u5DF2\u6536\u655B" : ""}`;
1120
+ const completion = buildCompletionSummary(report);
1121
+ return React.createElement(
1122
+ "div",
1123
+ { "data-iterate-root": "", "data-iterate": "stats", className: "iterate-stats" },
1124
+ React.createElement(
1125
+ "div",
1126
+ { className: "iterate-triage-head" },
1127
+ React.createElement("span", {}, "Iterate \xB7 \u6536\u655B\u7EDF\u8BA1"),
1128
+ React.createElement(
1129
+ "span",
1130
+ { className: "iterate-triage-hint" },
1131
+ `Round ${getCurrentRound(report)}/${getTotalRounds(report)} \xB7 ${total} findings \xB7 ${report.convergence && report.convergence.converged ? "\u5DF2\u6536\u655B" : "\u672A\u6536\u655B"}`
1132
+ )
442
1133
  ),
443
- )
444
-
445
- const history = buildRoundHistory(report)
446
- const trend = computeTrendMetrics(report)
447
- const historyRows = history.map((h) =>
448
- React.createElement('tr', { key: h.round },
449
- React.createElement('td', { className: 'iterate-history-num' }, `Round ${h.round}`),
450
- React.createElement('td', {}, String(h.count)),
451
- React.createElement('td', { style: { color: SEVERITY_COLOR.critical } }, String(h.critical)),
452
- React.createElement('td', { style: { color: SEVERITY_COLOR.high } }, String(h.high)),
453
- React.createElement('td', { style: { color: SEVERITY_COLOR.medium } }, String(h.medium)),
454
- React.createElement('td', { style: { color: SEVERITY_COLOR.low } }, String(h.low)),
1134
+ React.createElement("div", { className: "iterate-stats-grid" }, ...rows),
1135
+ React.createElement("div", { className: "iterate-completion", "data-ok": trend.converged ? "" : void 0, "data-warn": trend.converged ? void 0 : "" }, completion),
1136
+ React.createElement(
1137
+ "div",
1138
+ { className: "iterate-triage-foot", style: { marginTop: 8, borderRadius: 8 } },
1139
+ React.createElement("span", {}, trendLine),
1140
+ React.createElement("button", {
1141
+ className: "iterate-btn",
1142
+ "data-primary": showHistory ? "" : void 0,
1143
+ onClick: () => setShowHistory((v) => !v)
1144
+ }, showHistory ? "\u6536\u8D77\u5386\u53F2" : "\u5386\u53F2 / \u8D8B\u52BF")
455
1145
  ),
456
- )
457
- const trendLine = `首轮 ${trend.firstRound} → 末轮 ${trend.lastRound} 项,降幅 ${trend.reductionPercent}%${trend.converged ? ',已收敛' : ''}`
458
- const completion = buildCompletionSummary(report)
459
-
460
- return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'stats', className: 'iterate-stats' },
461
- React.createElement('div', { className: 'iterate-triage-head' },
462
- React.createElement('span', {}, 'Iterate · 收敛统计'),
463
- React.createElement('span', { className: 'iterate-triage-hint' },
464
- `Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? '已收敛' : '未收敛'}`,
1146
+ showHistory ? React.createElement(
1147
+ "div",
1148
+ { className: "iterate-history-body" },
1149
+ React.createElement(
1150
+ "table",
1151
+ { className: "iterate-history-table" },
1152
+ React.createElement(
1153
+ "thead",
1154
+ {},
1155
+ React.createElement(
1156
+ "tr",
1157
+ {},
1158
+ React.createElement("th", {}, "\u8F6E\u6B21"),
1159
+ React.createElement("th", {}, "\u53D1\u73B0"),
1160
+ React.createElement("th", { style: { color: SEVERITY_COLOR.critical } }, "CRIT"),
1161
+ React.createElement("th", { style: { color: SEVERITY_COLOR.high } }, "HIGH"),
1162
+ React.createElement("th", { style: { color: SEVERITY_COLOR.medium } }, "MED"),
1163
+ React.createElement("th", { style: { color: SEVERITY_COLOR.low } }, "LOW")
1164
+ )
1165
+ ),
1166
+ React.createElement("tbody", {}, ...historyRows)
465
1167
  ),
466
- ),
467
- React.createElement('div', { className: 'iterate-stats-grid' }, ...rows),
468
- React.createElement('div', { className: 'iterate-completion', 'data-ok': trend.converged ? '' : undefined, 'data-warn': trend.converged ? undefined : '' }, completion),
469
- React.createElement('div', { className: 'iterate-triage-foot', style: { marginTop: 8, borderRadius: 8 } },
470
- React.createElement('span', {}, trendLine),
471
- React.createElement('button', {
472
- className: 'iterate-btn',
473
- 'data-primary': showHistory ? '' : undefined,
474
- onClick: () => setShowHistory((v) => !v),
475
- }, showHistory ? '收起历史' : '历史 / 趋势'),
476
- ),
477
- showHistory
478
- ? React.createElement('div', { className: 'iterate-history-body' },
479
- React.createElement('table', { className: 'iterate-history-table' },
480
- React.createElement('thead', {},
481
- React.createElement('tr', {},
482
- React.createElement('th', {}, '轮次'),
483
- React.createElement('th', {}, '发现'),
484
- React.createElement('th', { style: { color: SEVERITY_COLOR.critical } }, 'CRIT'),
485
- React.createElement('th', { style: { color: SEVERITY_COLOR.high } }, 'HIGH'),
486
- React.createElement('th', { style: { color: SEVERITY_COLOR.medium } }, 'MED'),
487
- React.createElement('th', { style: { color: SEVERITY_COLOR.low } }, 'LOW'),
488
- ),
489
- ),
490
- React.createElement('tbody', {}, ...historyRows),
491
- ),
492
- React.createElement('div', { className: 'iterate-history-trendline' },
493
- React.createElement(TrendChart, { points: trend.points }),
494
- ),
495
- )
496
- : null,
497
- )
1168
+ React.createElement(
1169
+ "div",
1170
+ { className: "iterate-history-trendline" },
1171
+ React.createElement(TrendChart, { points: trend.points })
1172
+ )
1173
+ ) : null
1174
+ );
498
1175
  }
499
-
500
- /** Triage panel: per-finding y / n / a with filters, batch ops, keyboard shortcuts. */
501
1176
  function TriagePanel(props) {
502
- const report = props.report
503
- const findings = report.findings
504
- const storageKey = TRIAGE_STORAGE_PREFIX + hashReport(report)
1177
+ const report = props.report;
1178
+ const findings = report.findings || [];
1179
+ const storageKey = TRIAGE_STORAGE_PREFIX + hashReport(report);
505
1180
  const [verdicts, setVerdicts] = React.useState(() => {
506
- const initial = buildTriageState(report)
507
- const saved = storage ? storage.get(storageKey) : null
1181
+ const initial = buildTriageState(report);
1182
+ const saved = storage ? storage.get(storageKey) : null;
508
1183
  if (saved) {
509
1184
  try {
510
- const parsed = JSON.parse(saved)
511
- if (parsed && typeof parsed === 'object') {
1185
+ const parsed = JSON.parse(saved);
1186
+ if (parsed && typeof parsed === "object") {
512
1187
  for (const key of Object.keys(initial)) {
513
- if (parsed[key] === 'keep' || parsed[key] === 'skip' || parsed[key] === 'ignore') {
514
- initial[key] = parsed[key]
1188
+ if (parsed[key] === "keep" || parsed[key] === "skip" || parsed[key] === "ignore") {
1189
+ initial[key] = parsed[key];
515
1190
  }
516
1191
  }
517
1192
  }
518
- } catch { /* corrupt storage, fall back to defaults */ }
1193
+ } catch {
1194
+ }
519
1195
  }
520
- return initial
521
- })
522
- const [payload, setPayload] = React.useState(null)
523
- const [copied, setCopied] = React.useState(false)
524
- const [filter, setFilter] = React.useState({ severities: [], dimensions: [], search: '' })
525
- const [selected, setSelected] = React.useState(null)
526
- const [selectAll, setSelectAll] = React.useState(false)
527
-
528
- /** Persist + return the next verdicts state. */
1196
+ return initial;
1197
+ });
1198
+ const [payload, setPayload] = React.useState(null);
1199
+ const [copied, setCopied] = React.useState(false);
1200
+ const [filter, setFilter] = React.useState({ severities: [], dimensions: [], search: "" });
1201
+ const [selected, setSelected] = React.useState(null);
1202
+ const [selectAll, setSelectAll] = React.useState(false);
529
1203
  const persistVerdicts = (next) => {
530
- if (storage) storage.set(storageKey, JSON.stringify(next))
531
- return next
532
- }
533
-
1204
+ if (storage) storage.set(storageKey, JSON.stringify(next));
1205
+ return next;
1206
+ };
534
1207
  const setVerdict = (index, verdict) => {
535
- setVerdicts((prev) => persistVerdicts({ ...prev, [String(index)]: verdict }))
536
- }
537
-
538
- // ── Filtering (visible findings + their original indices) ───────────────
539
- const { filtered, indices } = filterFindingsWithIndices(findings, filter)
540
- const indicesKey = indices.join(',')
541
- const options = buildFilterOptions(findings)
542
- const filterActive = filter.severities.length > 0 || filter.dimensions.length > 0 || filter.search !== ''
543
-
544
- const setSeverityFilter = (value) => setFilter((f) => ({ ...f, severities: value ? [value] : [] }))
545
- const setDimensionFilter = (value) => setFilter((f) => ({ ...f, dimensions: value ? [value] : [] }))
546
- const setSearchFilter = (value) => setFilter((f) => ({ ...f, search: value }))
547
- const clearFilter = () => setFilter({ severities: [], dimensions: [], search: '' })
548
-
549
- // ── Batch operations (apply to the currently VISIBLE findings, or to ALL
550
- // findings when the select-all toggle is on) ──────────────────────────
551
- const allIndices = allVerdictKeys(verdicts)
552
- const batchTarget = selectAll ? allIndices : indices
1208
+ setVerdicts((prev) => persistVerdicts({ ...prev, [String(index)]: verdict }));
1209
+ };
1210
+ const { filtered, indices } = filterFindingsWithIndices(findings, filter);
1211
+ const indicesKey = indices.join(",");
1212
+ const options = buildFilterOptions(findings);
1213
+ const isFilterActive = filter.severities.length > 0 || filter.dimensions.length > 0 || filter.search !== "";
1214
+ const setSeverityFilter = (value) => setFilter((f) => ({ ...f, severities: value ? [value] : [] }));
1215
+ const setDimensionFilter = (value) => setFilter((f) => ({ ...f, dimensions: value ? [value] : [] }));
1216
+ const setSearchFilter = (value) => setFilter((f) => ({ ...f, search: value }));
1217
+ const clearFilter = () => setFilter({ severities: [], dimensions: [], search: "" });
1218
+ const allIndices = allVerdictKeys(verdicts);
1219
+ const batchTarget = selectAll ? allIndices : indices;
553
1220
  const applyBatch = (verdict) => {
554
- setVerdicts((prev) => persistVerdicts(batchSetVerdict(prev, batchTarget, verdict)))
555
- }
1221
+ setVerdicts((prev) => persistVerdicts(batchSetVerdict(prev, batchTarget, verdict)));
1222
+ };
556
1223
  const applyBatchAll = (verdict) => {
557
- setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, verdict)))
558
- }
1224
+ setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, verdict)));
1225
+ };
559
1226
  const doResetVerdicts = () => {
560
- setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, 'keep')))
561
- setSelectAll(false)
562
- }
563
-
564
- // ── Keyboard shortcuts (y / n / a on the selected finding, ↑/↓ to move) ──
1227
+ setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, "keep")));
1228
+ setSelectAll(false);
1229
+ };
565
1230
  React.useEffect(() => {
566
- const doc = typeof document !== 'undefined' ? document : null
567
- if (!doc) return
1231
+ const doc = typeof document !== "undefined" ? document : null;
1232
+ if (!doc) return;
568
1233
  const onKeyDown = (ev) => {
569
- const t = ev.target
570
- if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
571
- const verdict = keyToVerdict(ev.key)
1234
+ const t = ev.target;
1235
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT")) return;
1236
+ const verdict = keyToVerdict(ev.key);
572
1237
  if (verdict && selected !== null && indices.includes(selected)) {
573
- ev.preventDefault()
574
- setVerdict(selected, verdict)
575
- const pos = indices.indexOf(selected)
576
- const nextIdx = indices[pos + 1]
577
- if (nextIdx !== undefined) setSelected(nextIdx)
578
- return
1238
+ ev.preventDefault();
1239
+ setVerdict(selected, verdict);
1240
+ const pos = indices.indexOf(selected);
1241
+ const nextIdx = indices[pos + 1];
1242
+ if (nextIdx !== void 0) setSelected(nextIdx);
1243
+ return;
579
1244
  }
580
- if (ev.key === 'ArrowDown' && indices.length > 0) {
581
- ev.preventDefault()
582
- const pos = selected === null ? -1 : indices.indexOf(selected)
583
- setSelected(indices[Math.min(pos + 1, indices.length - 1)])
584
- return
1245
+ if (ev.key === "ArrowDown" && indices.length > 0) {
1246
+ ev.preventDefault();
1247
+ const pos = selected === null ? -1 : indices.indexOf(selected);
1248
+ setSelected(indices[Math.min(pos + 1, indices.length - 1)] ?? null);
1249
+ return;
585
1250
  }
586
- if (ev.key === 'ArrowUp' && indices.length > 0) {
587
- ev.preventDefault()
588
- const pos = selected === null ? 0 : indices.indexOf(selected)
589
- setSelected(indices[Math.max(pos - 1, 0)])
1251
+ if (ev.key === "ArrowUp" && indices.length > 0) {
1252
+ ev.preventDefault();
1253
+ const pos = selected === null ? 0 : indices.indexOf(selected);
1254
+ setSelected(indices[Math.max(pos - 1, 0)] ?? null);
590
1255
  }
591
- }
592
- doc.addEventListener('keydown', onKeyDown)
593
- return () => doc.removeEventListener('keydown', onKeyDown)
594
- }, [selected, indicesKey])
595
-
596
- const ignored = collectIgnoredEntries(verdicts, findings)
597
- const ignoredCount = ignored.length
598
- const counts = countVerdicts(verdicts)
599
-
1256
+ };
1257
+ doc.addEventListener("keydown", onKeyDown);
1258
+ return () => doc.removeEventListener("keydown", onKeyDown);
1259
+ }, [selected, indicesKey]);
1260
+ const ignored = collectIgnoredEntries(verdicts, findings);
1261
+ const ignoredCount = ignored.length;
1262
+ const counts = countVerdicts(verdicts);
600
1263
  const doCopyYaml = () => {
601
- const yaml = toKnownIntentionalYaml(ignored)
602
- if (!yaml) return
603
- const ok = copyText(yaml)
604
- setCopied(true)
605
- setTimeout(() => setCopied(false), 1600)
606
- if (!ok) setPayload(yaml)
607
- }
608
-
1264
+ const yaml = toKnownIntentionalYaml(ignored);
1265
+ if (!yaml) return;
1266
+ const ok = copyText(yaml);
1267
+ setCopied(true);
1268
+ setTimeout(() => setCopied(false), 1600);
1269
+ if (!ok) setPayload(yaml);
1270
+ };
609
1271
  const doBuildInstruction = () => {
610
- const text = buildApplyInstruction(ignored)
611
- setPayload(text)
612
- if (text) copyText(text)
613
- }
614
-
1272
+ const text = buildApplyInstruction(ignored);
1273
+ setPayload(text);
1274
+ if (text) copyText(text);
1275
+ };
615
1276
  const rows = filtered.map((finding, i) => {
616
- const index = indices[i]
617
- const severity = finding.severity || 'low'
618
- const verdict = verdicts[String(index)] || 'keep'
619
- const isSelected = selected === index
620
- const btn = (label, value, title) =>
1277
+ const index = indices[i];
1278
+ const severity = finding.severity || "low";
1279
+ const verdict = verdicts[String(index)] || "keep";
1280
+ const isSelected = selected === index;
1281
+ const btn = (label, value, title) => React.createElement(
1282
+ "button",
1283
+ {
1284
+ key: value,
1285
+ className: "iterate-vbtn",
1286
+ "data-active": verdict === value ? value : void 0,
1287
+ title,
1288
+ onClick: () => setVerdict(index, value)
1289
+ },
1290
+ label
1291
+ );
1292
+ return React.createElement(
1293
+ "div",
1294
+ {
1295
+ key: String(index),
1296
+ className: "iterate-finding",
1297
+ "data-selected": isSelected ? "" : void 0,
1298
+ onClick: () => setSelected(index)
1299
+ },
621
1300
  React.createElement(
622
- 'button',
623
- {
624
- key: value,
625
- className: 'iterate-vbtn',
626
- 'data-active': verdict === value ? value : undefined,
627
- title,
628
- onClick: () => setVerdict(index, value),
629
- },
630
- label,
631
- )
632
-
633
- return React.createElement('div', {
634
- key: String(index),
635
- className: 'iterate-finding',
636
- 'data-selected': isSelected ? '' : undefined,
637
- onClick: () => setSelected(index),
638
- },
639
- React.createElement('div', { className: 'iterate-finding-meta' },
640
- React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR[severity] || SEVERITY_COLOR.low } }),
641
- React.createElement('span', {}, String(SEVERITY_LABEL[severity] || 'LOW')),
642
- React.createElement('span', { className: 'iterate-finding-file' }, String(finding.file || '?')),
643
- finding.line ? React.createElement('span', {}, `:${finding.line}`) : null,
644
- React.createElement('span', {}, String(finding.dimension || '')),
645
- ),
646
- React.createElement('div', { className: 'iterate-finding-summary' }, String(finding.summary || '')),
647
- React.createElement('div', { className: 'iterate-finding-actions' },
648
- btn('y 修复', 'keep', '保留该 finding,进入修复'),
649
- btn('n 跳过', 'skip', '跳过该 finding'),
650
- btn('a 已知有意', 'ignore', '标记为已知有意并写入 known_intentional'),
1301
+ "div",
1302
+ { className: "iterate-finding-meta" },
1303
+ React.createElement("span", { className: "iterate-sev-dot", style: { background: severityColor(severity) } }),
1304
+ React.createElement("span", {}, severityLabel(severity)),
1305
+ React.createElement("span", { className: "iterate-finding-file" }, String(finding.file || "?")),
1306
+ finding.line ? React.createElement("span", {}, `:${finding.line}`) : null,
1307
+ React.createElement("span", {}, String(finding.dimension || ""))
651
1308
  ),
652
- )
653
- })
654
-
655
- return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
656
- React.createElement('div', { className: 'iterate-triage-head' },
657
- React.createElement('span', {}, `Iterate · Findings 分诊 (${filtered.length}/${findings.length})`),
658
- React.createElement('span', { className: 'iterate-triage-hint' }, 'y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择'),
1309
+ React.createElement("div", { className: "iterate-finding-summary" }, String(finding.summary || "")),
1310
+ React.createElement(
1311
+ "div",
1312
+ { className: "iterate-finding-actions" },
1313
+ btn("y \u4FEE\u590D", "keep", "\u4FDD\u7559\u8BE5 finding\uFF0C\u8FDB\u5165\u4FEE\u590D"),
1314
+ btn("n \u8DF3\u8FC7", "skip", "\u8DF3\u8FC7\u8BE5 finding"),
1315
+ btn("a \u5DF2\u77E5\u6709\u610F", "ignore", "\u6807\u8BB0\u4E3A\u5DF2\u77E5\u6709\u610F\u5E76\u5199\u5165 known_intentional")
1316
+ )
1317
+ );
1318
+ });
1319
+ return React.createElement(
1320
+ "div",
1321
+ { "data-iterate-root": "", "data-iterate": "triage", className: "iterate-triage" },
1322
+ React.createElement(
1323
+ "div",
1324
+ { className: "iterate-triage-head" },
1325
+ React.createElement("span", {}, `Iterate \xB7 Findings \u5206\u8BCA (${filtered.length}/${findings.length})`),
1326
+ React.createElement("span", { className: "iterate-triage-hint" }, "y=\u4FEE\u590D \xB7 n=\u8DF3\u8FC7 \xB7 a=\u5DF2\u77E5\u6709\u610F \xB7 \u2191/\u2193 \u9009\u62E9")
659
1327
  ),
660
- React.createElement('div', { className: 'iterate-filter' },
661
- React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e) => setSeverityFilter(e.target.value), title: '按严重度筛选' },
662
- React.createElement('option', { value: '' }, '全部严重度'),
663
- ...options.severities.map((s) =>
664
- React.createElement('option', { key: s.value, value: s.value }, `${SEVERITY_LABEL[s.value] || s.value} (${s.count})`),
665
- ),
1328
+ React.createElement(
1329
+ "div",
1330
+ { className: "iterate-filter" },
1331
+ React.createElement(
1332
+ "select",
1333
+ { className: "iterate-filter-select", value: filter.severities[0] || "", onChange: (e) => setSeverityFilter(e.target.value), title: "\u6309\u4E25\u91CD\u5EA6\u7B5B\u9009" },
1334
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u4E25\u91CD\u5EA6"),
1335
+ ...options.severities.map(
1336
+ (s) => React.createElement("option", { key: s.value, value: s.value }, `${severityLabel(s.value)} (${s.count})`)
1337
+ )
666
1338
  ),
667
- React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e) => setDimensionFilter(e.target.value), title: '按维度筛选' },
668
- React.createElement('option', { value: '' }, '全部维度'),
669
- ...options.dimensions.map((d) =>
670
- React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
671
- ),
1339
+ React.createElement(
1340
+ "select",
1341
+ { className: "iterate-filter-select", value: filter.dimensions[0] || "", onChange: (e) => setDimensionFilter(e.target.value), title: "\u6309\u7EF4\u5EA6\u7B5B\u9009" },
1342
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u7EF4\u5EA6"),
1343
+ ...options.dimensions.map(
1344
+ (d) => React.createElement("option", { key: d.value, value: d.value }, `${d.value} (${d.count})`)
1345
+ )
672
1346
  ),
673
- React.createElement('input', {
674
- className: 'iterate-filter-search',
675
- type: 'search',
676
- placeholder: '搜索文件 / 摘要…',
1347
+ React.createElement("input", {
1348
+ className: "iterate-filter-search",
1349
+ type: "search",
1350
+ placeholder: "\u641C\u7D22\u6587\u4EF6 / \u6458\u8981\u2026",
677
1351
  value: filter.search,
678
- onChange: (e) => setSearchFilter(e.target.value),
1352
+ onChange: (e) => setSearchFilter(e.target.value)
679
1353
  }),
680
- React.createElement('span', { className: 'iterate-filter-count' },
681
- filterActive
682
- ? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, `清除筛选(显示 ${filtered.length})`)
683
- : `共 ${findings.length} 项`,
684
- ),
1354
+ React.createElement(
1355
+ "span",
1356
+ { className: "iterate-filter-count" },
1357
+ isFilterActive ? React.createElement("button", { className: "iterate-batch-btn", onClick: clearFilter }, `\u6E05\u9664\u7B5B\u9009\uFF08\u663E\u793A ${filtered.length}\uFF09`) : `\u5171 ${findings.length} \u9879`
1358
+ )
685
1359
  ),
686
- React.createElement('div', { className: 'iterate-batch' },
687
- React.createElement('span', { className: 'iterate-batch-label' }, '批量:'),
688
- React.createElement('label', { className: 'iterate-batch-check', title: '勾选后批量按钮作用于全部 findings,否则仅当前可见' },
689
- React.createElement('input', { type: 'checkbox', checked: selectAll, onChange: (e) => setSelectAll(e.target.checked) }),
690
- selectAll ? `全部 ${allIndices.length}` : '全选',
1360
+ React.createElement(
1361
+ "div",
1362
+ { className: "iterate-batch" },
1363
+ React.createElement("span", { className: "iterate-batch-label" }, "\u6279\u91CF\uFF1A"),
1364
+ React.createElement(
1365
+ "label",
1366
+ { className: "iterate-batch-check", title: "\u52FE\u9009\u540E\u6279\u91CF\u6309\u94AE\u4F5C\u7528\u4E8E\u5168\u90E8 findings\uFF0C\u5426\u5219\u4EC5\u5F53\u524D\u53EF\u89C1" },
1367
+ React.createElement("input", { type: "checkbox", checked: selectAll, onChange: (e) => setSelectAll(e.target.checked) }),
1368
+ selectAll ? `\u5168\u90E8 ${allIndices.length}` : "\u5168\u9009"
691
1369
  ),
692
- React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('keep') }, '全部 y'),
693
- React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('skip') }, '全部 n'),
694
- React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('ignore') }, '全部 a'),
695
- React.createElement('span', { className: 'iterate-batch-label', style: { marginLeft: 8 } }, '全部:'),
696
- React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatchAll('keep') }, 'y'),
697
- React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatchAll('skip') }, 'n'),
698
- React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatchAll('ignore') }, 'a'),
699
- React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: '把所有判定恢复为默认 y(修复)' }, '重置'),
1370
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("keep") }, "\u5168\u90E8 y"),
1371
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("skip") }, "\u5168\u90E8 n"),
1372
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatch("ignore") }, "\u5168\u90E8 a"),
1373
+ React.createElement("span", { className: "iterate-batch-label", style: { marginLeft: 8 } }, "\u5168\u90E8\uFF1A"),
1374
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatchAll("keep") }, "y"),
1375
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatchAll("skip") }, "n"),
1376
+ React.createElement("button", { className: "iterate-batch-btn", onClick: () => applyBatchAll("ignore") }, "a"),
1377
+ React.createElement("button", { className: "iterate-batch-btn", onClick: doResetVerdicts, title: "\u628A\u6240\u6709\u5224\u5B9A\u6062\u590D\u4E3A\u9ED8\u8BA4 y\uFF08\u4FEE\u590D\uFF09" }, "\u91CD\u7F6E")
700
1378
  ),
701
1379
  ...rows,
702
- React.createElement('div', { className: 'iterate-triage-foot' },
703
- React.createElement('span', {}, `y ${counts.keep} · n ${counts.skip} · a ${counts.ignore} · 待写回 known_intentional:${ignoredCount} 条`),
704
- React.createElement('span', { style: { display: 'flex', gap: 6 } },
705
- React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyYaml }, copied ? '已复制' : '复制 known_intentional'),
706
- React.createElement('button', { className: 'iterate-btn', onClick: doBuildInstruction }, '生成应用指令'),
707
- ),
1380
+ React.createElement(
1381
+ "div",
1382
+ { className: "iterate-triage-foot" },
1383
+ React.createElement("span", {}, `y ${counts.keep} \xB7 n ${counts.skip} \xB7 a ${counts.ignore} \xB7 \u5F85\u5199\u56DE known_intentional\uFF1A${ignoredCount} \u6761`),
1384
+ React.createElement(
1385
+ "span",
1386
+ { style: { display: "flex", gap: 6 } },
1387
+ React.createElement("button", { className: "iterate-btn", "data-primary": "", "data-copied": copied ? "" : void 0, onClick: doCopyYaml }, copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236 known_intentional"),
1388
+ React.createElement("button", { className: "iterate-btn", onClick: doBuildInstruction }, "\u751F\u6210\u5E94\u7528\u6307\u4EE4")
1389
+ )
708
1390
  ),
709
- payload
710
- ? React.createElement('div', { className: 'iterate-payload' }, payload)
711
- : null,
712
- )
1391
+ payload ? React.createElement("div", { className: "iterate-payload" }, payload) : null
1392
+ );
1393
+ }
1394
+ function VerdictBanner(props) {
1395
+ const verdict = props.verdict;
1396
+ if (!verdict) return null;
1397
+ const ok = verdict.verdict === "approved";
1398
+ const item = (num, unit) => React.createElement(
1399
+ "span",
1400
+ { className: "iterate-verdict-item" },
1401
+ React.createElement("b", {}, String(num)),
1402
+ ` ${unit}`
1403
+ );
1404
+ const phrase = (text) => React.createElement("span", { className: "iterate-verdict-item" }, text);
1405
+ return React.createElement(
1406
+ "div",
1407
+ { "data-iterate-root": "", "data-iterate": "verdict", className: "iterate-verdict" },
1408
+ React.createElement(
1409
+ "span",
1410
+ { className: "iterate-verdict-tag", "data-ok": ok ? "" : void 0, "data-warn": ok ? void 0 : "" },
1411
+ ok ? "\u62A5\u544A\u5DF2\u6279\u51C6" : "\u62A5\u544A\u9700\u4FEE\u8BA2"
1412
+ ),
1413
+ React.createElement(
1414
+ "span",
1415
+ { className: "iterate-verdict-detail" },
1416
+ item(verdict.totalFindings, "\u9879\u53D1\u73B0"),
1417
+ item(verdict.totalRounds, "\u8F6E"),
1418
+ item(verdict.checksRun, "\u9879\u5BA1\u67E5"),
1419
+ ok ? phrase("\u62A5\u544A\u901A\u8FC7\u5168\u90E8\u4E00\u81F4\u6027\u68C0\u67E5") : item(verdict.reportIssues, "\u5904\u62A5\u544A\u7F3A\u9677"),
1420
+ phrase(verdict.converged ? "\u5DF2\u6536\u655B" : "\u672A\u6536\u655B")
1421
+ )
1422
+ );
713
1423
  }
714
-
715
- /** Turn-tail chain entry: triage when findings exist, stats card otherwise. */
716
1424
  function TurnTailEntry(props) {
717
- const candidates = []
718
- if (props && props.turn) candidates.push(props.turn)
719
- if (props && props.matched) candidates.push(props.matched)
720
- if (props && props.data) candidates.push(props.data)
721
- candidates.push(props)
722
-
723
- let report = null
1425
+ const candidates = [];
1426
+ if (props && props.turn) candidates.push(props.turn);
1427
+ if (props && props.matched) candidates.push(props.matched);
1428
+ if (props && props.data) candidates.push(props.data);
1429
+ candidates.push(props);
1430
+ let verdict = null;
1431
+ for (const c of candidates) {
1432
+ const run = scanSessionForRunSummary(c);
1433
+ if (run) {
1434
+ verdict = extractVerdict(run);
1435
+ break;
1436
+ }
1437
+ }
1438
+ let report = null;
724
1439
  for (const c of candidates) {
725
- const raw = findReportInObject(c, undefined, 24) || scanSessionForReport(c)
726
- if (raw) { report = normalizeReport(raw); break }
1440
+ const raw = findReportInObject(c, void 0, 24) || scanSessionForReport(c);
1441
+ if (raw) {
1442
+ report = normalizeReport(raw);
1443
+ break;
1444
+ }
727
1445
  }
728
- if (!report) return null
729
- // Render through React.createElement so each panel is a real component with
730
- // its own hook identity (calling them as functions would violate the Rules
731
- // of Hooks and crash when the findings/empty branch flips between renders).
732
- if (!report.findings || report.findings.length === 0) {
733
- return React.createElement(StatsCard, { report })
1446
+ const blocks = [];
1447
+ if (verdict) blocks.push(React.createElement(VerdictBanner, { key: "verdict", verdict }));
1448
+ if (report) {
1449
+ const panel = !report.findings || report.findings.length === 0 ? React.createElement(StatsCard, { report }) : React.createElement(TriagePanel, { report });
1450
+ blocks.push(React.createElement("div", { key: "report" }, panel));
734
1451
  }
735
- return React.createElement(TriagePanel, { report })
1452
+ if (blocks.length === 0) return null;
1453
+ return React.createElement("div", { "data-iterate-root": "", className: "iterate-turn-tail-root" }, ...blocks);
736
1454
  }
737
-
738
- /** Progress capsule: briefly surfaces round-completion (incl. convergence). */
739
1455
  function ProgressCapsule() {
740
- const [info, setInfo] = React.useState(null)
1456
+ const [info, setInfo] = React.useState(null);
741
1457
  React.useEffect(() => {
742
- let timer = null
1458
+ let timer = null;
743
1459
  const listener = (payload) => {
744
- const converged = payload && payload.converged === true
745
- const round = payload && typeof payload.round === 'number' ? payload.round : '?'
746
- setInfo({ text: converged ? `Round ${round} 完成 · 已收敛` : `Round ${round} 完成`, ok: converged })
747
- if (timer) clearTimeout(timer)
748
- // Converged notifications stay a bit longer.
749
- timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400)
750
- }
751
- roundPulseListeners.push(listener)
1460
+ const converged = payload && payload.converged === true;
1461
+ const round = payload && typeof payload.round === "number" ? payload.round : "?";
1462
+ setInfo({ text: converged ? `Round ${round} \u5B8C\u6210 \xB7 \u5DF2\u6536\u655B` : `Round ${round} \u5B8C\u6210`, ok: converged });
1463
+ if (timer) clearTimeout(timer);
1464
+ timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400);
1465
+ };
1466
+ roundPulseListeners.push(listener);
752
1467
  return () => {
753
- const i = roundPulseListeners.indexOf(listener)
754
- if (i >= 0) roundPulseListeners.splice(i, 1)
755
- if (timer) clearTimeout(timer)
756
- }
757
- }, [])
758
- if (!info) return null
759
- return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-capsule', 'data-ok': info.ok ? '' : undefined }, info.text)
1468
+ const i = roundPulseListeners.indexOf(listener);
1469
+ if (i >= 0) roundPulseListeners.splice(i, 1);
1470
+ if (timer) clearTimeout(timer);
1471
+ };
1472
+ }, []);
1473
+ if (!info) return null;
1474
+ return React.createElement("div", { "data-iterate-root": "", className: "iterate-capsule", "data-ok": info.ok ? "" : void 0 }, info.text);
760
1475
  }
761
-
762
- /** Settings page section (root scope; reads localStorage + latest known data). */
763
- function SettingsPanel() {
764
- const [enabled, setEnabled] = React.useState(themeEnabled)
765
- const [copied, setCopied] = React.useState(false)
766
- const [showGuide, setShowGuide] = React.useState(false)
767
- const [clearedCount, setClearedCount] = React.useState(null)
768
- const [showStatus, setShowStatus] = React.useState(false)
769
- const guide = buildConfigEditGuide()
770
- const statusGuide = buildRuntimeStatusGuide()
771
-
1476
+ function SettingsPanel(_props) {
1477
+ const [enabled, setEnabled] = React.useState(themeEnabled);
1478
+ const [guideCopied, setGuideCopied] = React.useState(false);
1479
+ const [statusCopied, setStatusCopied] = React.useState(false);
1480
+ const [showGuide, setShowGuide] = React.useState(false);
1481
+ const [showStatus, setShowStatus] = React.useState(false);
1482
+ const [confirming, setConfirming] = React.useState(false);
1483
+ const [clearedInfo, setClearedInfo] = React.useState(null);
1484
+ const guide = buildConfigEditGuide();
1485
+ const statusGuide = buildRuntimeStatusGuide();
772
1486
  const toggleTheme = () => {
773
- const next = !enabled
774
- setEnabled(next)
775
- setThemeEnabled(next)
776
- }
777
-
778
- const doCopyGuide = () => {
779
- copyText(guide)
780
- setCopied(true)
781
- setTimeout(() => setCopied(false), 1600)
782
- }
783
-
784
- const doClearTriage = () => {
785
- const count = removeStorageByPrefix(TRIAGE_STORAGE_PREFIX)
786
- setClearedCount(count)
787
- setTimeout(() => setClearedCount(null), 3000)
788
- }
789
-
790
- return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'settings', className: 'iterate-settings' },
791
- React.createElement('div', { className: 'iterate-settings-title' }, 'iterate 设置'),
792
- React.createElement('div', { className: 'iterate-settings-row' },
793
- React.createElement('div', {},
794
- React.createElement('div', { className: 'iterate-settings-title' }, 'iterate 主题'),
795
- React.createElement('div', { className: 'iterate-settings-desc' }, '启用暖琥珀配色的 iterate 专属皮肤(覆盖 dsh 默认主题令牌)。'),
1487
+ const next = !enabled;
1488
+ setEnabled(next);
1489
+ setThemeEnabled(next);
1490
+ };
1491
+ const flashCopied = (slot) => {
1492
+ const setter = slot === "guide" ? setGuideCopied : setStatusCopied;
1493
+ setter(true);
1494
+ setTimeout(() => setter(false), 1600);
1495
+ };
1496
+ const doCopy = (text, slot) => {
1497
+ copyText(text);
1498
+ flashCopied(slot);
1499
+ };
1500
+ const requestClear = () => {
1501
+ if (confirming) {
1502
+ const count = removeStorageByPrefix(TRIAGE_STORAGE_PREFIX);
1503
+ setClearedInfo(count);
1504
+ setConfirming(false);
1505
+ setTimeout(() => setClearedInfo(null), 3e3);
1506
+ return;
1507
+ }
1508
+ setConfirming(true);
1509
+ setTimeout(() => setConfirming(false), 3e3);
1510
+ };
1511
+ const clearButton = clearedInfo !== null ? React.createElement("button", { className: "iterate-btn", "data-copied": "", disabled: true }, `\u5DF2\u6E05\u9664 ${clearedInfo} \u6761`) : React.createElement("button", {
1512
+ className: "iterate-btn",
1513
+ "data-danger": confirming ? void 0 : "",
1514
+ "data-confirm": confirming ? "" : void 0,
1515
+ onClick: requestClear,
1516
+ title: confirming ? "\u518D\u6B21\u70B9\u51FB\u4EE5\u786E\u8BA4\u6E05\u9664\u5168\u90E8\u5224\u5B9A" : "\u6E05\u9664\u6240\u6709\u5206\u8BCA\u5224\u5B9A\u8BB0\u5F55\uFF08\u9700\u8981\u4E8C\u6B21\u786E\u8BA4\uFF09"
1517
+ }, confirming ? "\u786E\u8BA4\u6E05\u9664\uFF1F" : "\u6E05\u9664\u5206\u8BCA");
1518
+ const themeCard = React.createElement(
1519
+ "div",
1520
+ { key: "theme", className: "iterate-scard" },
1521
+ React.createElement(
1522
+ "div",
1523
+ { className: "iterate-scard-head" },
1524
+ React.createElement(
1525
+ "div",
1526
+ {},
1527
+ React.createElement("div", { className: "iterate-settings-title" }, "\u8FED\u4EE3\u4E3B\u9898"),
1528
+ React.createElement("div", { className: "iterate-settings-desc" }, "\u542F\u7528\u6696\u7425\u73C0\u914D\u8272\u7684 iterate \u4E13\u5C5E\u76AE\u80A4\uFF0C\u8986\u76D6 dsh \u9ED8\u8BA4\u4E3B\u9898\u4EE4\u724C\u3002")
796
1529
  ),
797
- React.createElement('button', {
798
- className: 'iterate-btn',
799
- 'data-primary': enabled ? '' : undefined,
800
- onClick: toggleTheme,
801
- }, enabled ? '已启用' : '已关闭'),
802
- ),
803
- React.createElement('div', { className: 'iterate-settings-row' },
804
- React.createElement('div', {},
805
- React.createElement('div', { className: 'iterate-settings-title' }, '分诊持久化'),
806
- React.createElement('div', { className: 'iterate-settings-desc' }, '分诊面板的 y/n/a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。'),
1530
+ React.createElement(
1531
+ "div",
1532
+ { className: "iterate-scard-actions" },
1533
+ React.createElement("span", { className: "iterate-chip", "data-ok": enabled ? "" : void 0 }, enabled ? "\u5DF2\u542F\u7528" : "\u5DF2\u5173\u95ED"),
1534
+ React.createElement("button", {
1535
+ role: "switch",
1536
+ "aria-checked": enabled,
1537
+ "aria-label": "\u5F00\u5173 iterate \u4E3B\u9898",
1538
+ className: "iterate-switch",
1539
+ "data-on": enabled ? "" : void 0,
1540
+ onClick: toggleTheme
1541
+ }, React.createElement("span", { className: "iterate-switch-knob" }))
1542
+ )
1543
+ )
1544
+ );
1545
+ const dataCard = React.createElement(
1546
+ "div",
1547
+ { key: "data", className: "iterate-scard" },
1548
+ React.createElement(
1549
+ "div",
1550
+ { className: "iterate-scard-head" },
1551
+ React.createElement(
1552
+ "div",
1553
+ {},
1554
+ React.createElement("div", { className: "iterate-settings-title" }, "\u5206\u8BCA\u6301\u4E45\u5316"),
1555
+ React.createElement("div", { className: "iterate-settings-desc" }, "y / n / a \u5224\u5B9A\u4FDD\u5B58\u5728\u672C\u5730\u6D4F\u89C8\u5668\uFF08localStorage\uFF09\uFF0C\u5237\u65B0\u4F1A\u8BDD\u540E\u4ECD\u4FDD\u7559\u3002")
807
1556
  ),
808
- React.createElement('span', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
809
- React.createElement('span', { className: 'iterate-chip' }, '本地保存'),
810
- React.createElement('button', {
811
- className: 'iterate-btn',
812
- onClick: doClearTriage,
813
- title: '清除所有分诊判定记录',
814
- 'data-primary': clearedCount !== null ? '' : undefined,
815
- 'data-copied': clearedCount !== null ? '' : undefined,
816
- }, clearedCount !== null ? `已清除 ${clearedCount} 条` : '清除分诊'),
1557
+ React.createElement(
1558
+ "div",
1559
+ { className: "iterate-scard-actions" },
1560
+ React.createElement("span", { className: "iterate-chip" }, "\u672C\u5730\u4FDD\u5B58"),
1561
+ clearButton
1562
+ )
1563
+ )
1564
+ );
1565
+ const guideCard = React.createElement(
1566
+ "div",
1567
+ { key: "guide", className: "iterate-scard" },
1568
+ React.createElement(
1569
+ "div",
1570
+ { className: "iterate-scard-head" },
1571
+ React.createElement(
1572
+ "div",
1573
+ {},
1574
+ React.createElement("div", { className: "iterate-settings-title" }, "\u914D\u7F6E\u7BA1\u7406"),
1575
+ React.createElement("div", { className: "iterate-settings-desc" }, "\u76EE\u6807 / \u7EF4\u5EA6 / \u6700\u5927\u8F6E\u6570\u5199\u5728\u9879\u76EE\u7684 iterate.config.yaml\uFF0C\u590D\u5236\u6307\u5F15\u53EF\u8BA9\u6A21\u578B\u6309\u9700\u8C03\u6574\u3002")
817
1576
  ),
1577
+ React.createElement(
1578
+ "div",
1579
+ { className: "iterate-scard-actions" },
1580
+ React.createElement("button", { className: "iterate-btn", "data-primary": "", "data-copied": guideCopied ? "" : void 0, onClick: () => doCopy(guide, "guide") }, guideCopied ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u6307\u5F15"),
1581
+ React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => setShowGuide((v) => !v) }, showGuide ? "\u6536\u8D77" : "\u5C55\u5F00")
1582
+ )
818
1583
  ),
819
- React.createElement('div', { className: 'iterate-settings-row' },
820
- React.createElement('div', {},
821
- React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
822
- React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数写在项目的 iterate.config.yaml。复制指引让模型为你调整,或展开查看可编辑字段。'),
1584
+ showGuide ? React.createElement(
1585
+ "div",
1586
+ { className: "iterate-guide" },
1587
+ React.createElement(
1588
+ "div",
1589
+ { className: "iterate-guide-bar" },
1590
+ React.createElement("span", {}, "iterate.config.yaml \u53EF\u7F16\u8F91\u5B57\u6BB5"),
1591
+ React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => doCopy(guide, "guide") }, "\u590D\u5236")
823
1592
  ),
824
- React.createElement('span', { style: { display: 'flex', gap: 6 } },
825
- React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyGuide }, copied ? '已复制' : '复制指引'),
826
- React.createElement('button', { className: 'iterate-btn', onClick: () => setShowGuide((v) => !v) }, showGuide ? '收起' : '查看'),
1593
+ React.createElement("div", { className: "iterate-guide-body" }, guide)
1594
+ ) : null
1595
+ );
1596
+ const statusCard = React.createElement(
1597
+ "div",
1598
+ { key: "status", className: "iterate-scard" },
1599
+ React.createElement(
1600
+ "div",
1601
+ { className: "iterate-scard-head" },
1602
+ React.createElement(
1603
+ "div",
1604
+ {},
1605
+ React.createElement("div", { className: "iterate-settings-title" }, "\u72B6\u6001\u6982\u89C8"),
1606
+ React.createElement("div", { className: "iterate-settings-desc" }, "\u67E5\u770B\u8FD0\u884C\u65F6\u4EA7\u7269\u5E03\u5C40\u4E0E\u6E05\u7406\u6307\u5F15\u3002iterate_status / iterate_history / iterate_prune \u5DE5\u5177\u7528\u4E8E\u67E5\u770B\u548C\u7BA1\u7406\u3002")
827
1607
  ),
1608
+ React.createElement(
1609
+ "div",
1610
+ { className: "iterate-scard-actions" },
1611
+ React.createElement("button", { className: "iterate-btn", "data-primary": "", "data-copied": statusCopied ? "" : void 0, onClick: () => doCopy(statusGuide, "status") }, statusCopied ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u6307\u5F15"),
1612
+ React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => setShowStatus((v) => !v) }, showStatus ? "\u6536\u8D77" : "\u5C55\u5F00")
1613
+ )
828
1614
  ),
829
- showGuide
830
- ? React.createElement('div', { className: 'iterate-settings-guide' }, guide)
831
- : null,
832
- React.createElement('div', { className: 'iterate-settings-row' },
833
- React.createElement('div', {},
834
- React.createElement('div', { className: 'iterate-settings-title' }, '状态概览'),
835
- React.createElement('div', { className: 'iterate-settings-desc' }, '运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。'),
1615
+ showStatus ? React.createElement(
1616
+ "div",
1617
+ { className: "iterate-guide" },
1618
+ React.createElement(
1619
+ "div",
1620
+ { className: "iterate-guide-bar" },
1621
+ React.createElement("span", {}, "\u8FD0\u884C\u65F6\u5E03\u5C40\u4E0E\u6E05\u7406"),
1622
+ React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => doCopy(statusGuide, "status") }, "\u590D\u5236")
836
1623
  ),
837
- React.createElement('button', { className: 'iterate-btn', onClick: () => setShowStatus((v) => !v) }, showStatus ? '收起' : '查看'),
838
- ),
839
- showStatus
840
- ? React.createElement('div', { className: 'iterate-settings-guide' }, statusGuide)
841
- : null,
842
- )
1624
+ React.createElement("div", { className: "iterate-guide-body" }, statusGuide)
1625
+ ) : null
1626
+ );
1627
+ const banner = React.createElement(
1628
+ "div",
1629
+ { key: "banner", className: "iterate-scard" },
1630
+ React.createElement(
1631
+ "div",
1632
+ { className: "iterate-scard-head" },
1633
+ React.createElement(
1634
+ "div",
1635
+ {},
1636
+ React.createElement("div", { className: "iterate-settings-title" }, "iterate"),
1637
+ React.createElement("div", { className: "iterate-settings-desc" }, "\u4E3A\u6BCF\u6B21\u4EE3\u7801\u8BC4\u5BA1\u751F\u6210 Review \u62A5\u544A\u4E0E\u4EA4\u4E92\u5F0F\u5206\u8BCA\u89C6\u56FE\uFF0C\u4E13\u6CE8 AI \u81EA\u6211\u5BA1\u67E5\u4E0E\u4FEE\u6B63\u3002")
1638
+ ),
1639
+ React.createElement(
1640
+ "span",
1641
+ { className: "iterate-pill" },
1642
+ React.createElement("span", { className: "iterate-pill-dot" }),
1643
+ "\u5C31\u7EEA"
1644
+ )
1645
+ )
1646
+ );
1647
+ return React.createElement(
1648
+ "div",
1649
+ { "data-iterate-root": "", "data-iterate": "settings", className: "iterate-settings" },
1650
+ React.createElement("div", { className: "iterate-settings-title", style: { fontSize: 15, fontWeight: 700 } }, "iterate \u8BBE\u7F6E"),
1651
+ banner,
1652
+ themeCard,
1653
+ dataCard,
1654
+ guideCard,
1655
+ statusCard
1656
+ );
843
1657
  }
844
-
845
- // ─── Registration ────────────────────────────────────────────────────────────
846
-
847
- /**
848
- * Chain selector for conversation.chat.turnTail.
849
- * Returns a marker when the turn contains an iterate review report, null otherwise.
850
- */
851
1658
  function selectTurnTail(owner) {
852
- if (!owner) return null
853
- const raw = findReportInObject(owner.turn, undefined, 24) || scanSessionForReport(owner.turn)
854
- return raw ? { matched: true } : null
1659
+ if (!owner) return null;
1660
+ if (findReportInObject(owner.turn, void 0, 24) || scanSessionForReport(owner.turn)) return { matched: true };
1661
+ if (scanSessionForRunSummary(owner.turn)) return { matched: true };
1662
+ return null;
855
1663
  }
856
-
857
- /**
858
- * Plugin client entry. Called by dsh web with the client Cordis context.
859
- */
860
- export function apply(ctx) {
861
- // 1. Resolve optional services (degrade, never throw).
862
- const get = ctx && typeof ctx.get === 'function' ? ctx.get.bind(ctx) : null
863
- slotsSvc = get ? get('slots') : undefined
864
- themeSvc = get ? get('theme') : undefined
865
- React = resolveReact(ctx)
866
- storage = createStorage()
867
-
868
- // 2. Theme preference bootstrap.
869
- const savedTheme = storage.get(THEME_STORAGE_KEY)
870
- themeEnabled = savedTheme === null ? true : savedTheme === '1'
871
- if (themeEnabled) applyThemeSkin()
872
-
873
- // 3. Inject styles (independent of slots / React availability).
874
- if (typeof document !== 'undefined' && document.createElement && document.head) {
875
- const style = document.createElement('style')
876
- style.dataset.plugin = PLUGIN_TAG
877
- style.dataset.pluginCss = 'iterate-main'
878
- style.textContent = ITERATE_CSS
879
- document.head.appendChild(style)
880
- if (typeof ctx.effect === 'function') {
881
- ctx.effect(() => { try { style.remove() } catch { /* noop */ } })
1664
+ function apply(ctx) {
1665
+ slotsSvc = readSlots(ctx);
1666
+ themeSvc = readTheme(ctx);
1667
+ storage = createStorage();
1668
+ const savedTheme = storage.get(THEME_STORAGE_KEY);
1669
+ themeEnabled = savedTheme === null ? true : savedTheme === "1";
1670
+ if (themeEnabled) applyThemeSkin();
1671
+ if (typeof document !== "undefined" && document.createElement && document.head) {
1672
+ const style = document.createElement("style");
1673
+ style.dataset.plugin = PLUGIN_TAG;
1674
+ style.dataset.pluginCss = "iterate-main";
1675
+ style.textContent = ITERATE_CSS;
1676
+ document.head.appendChild(style);
1677
+ if (typeof ctx.effect === "function") {
1678
+ ctx.effect(() => {
1679
+ try {
1680
+ style.remove();
1681
+ } catch {
1682
+ }
1683
+ });
882
1684
  }
883
1685
  }
884
-
885
- // 4. Slot UI requires React + slots service.
886
- if (React === null || slotsSvc === undefined) {
887
- log('React or slots service unavailable — slot UI disabled')
888
- return
1686
+ if (slotsSvc === void 0) {
1687
+ log("slots service unavailable \u2014 slot UI disabled");
1688
+ return;
889
1689
  }
890
-
891
- // 5. Register the six-part UI.
892
- if (typeof slotsSvc.inject === 'function') {
893
- // 1 + 5: convergence dashboard + round pulse (session scope).
894
- slotsSvc.inject('conversation.input.dock', () =>
895
- slotsSvc.register(
896
- { name: 'conversation.input.dock', id: 'iterate-dashboard', order: 90 },
897
- (props) => React.createElement(ConvergenceDashboard, props),
898
- ),
899
- )
900
-
901
- // 2 + 3: triage panel / stats card (turn-tail chain, session scope).
902
- slotsSvc.inject('conversation.chat.turnTail', () =>
903
- slotsSvc.register(
904
- { name: 'conversation.chat.turnTail', id: 'iterate-turn-tail', select: selectTurnTail },
905
- (props) => React.createElement(TurnTailEntry, props),
906
- ),
907
- )
908
-
909
- // 5: progress capsule (frame overlay, root scope).
910
- slotsSvc.inject('shell.overlay', () =>
911
- slotsSvc.register(
912
- { name: 'shell.overlay', id: 'iterate-progress', order: 0 },
913
- (props) => React.createElement(ProgressCapsule, props),
914
- ),
915
- )
916
-
917
- // 6: settings page section (root scope).
918
- slotsSvc.inject('settings.section', () =>
919
- slotsSvc.register(
920
- { name: 'settings.section', id: 'iterate-settings', order: 30, label: () => 'iterate' },
921
- (props) => React.createElement(SettingsPanel, props),
922
- ),
923
- )
1690
+ if (typeof slotsSvc.inject === "function") {
1691
+ slotsSvc.inject(
1692
+ "conversation.input.dock",
1693
+ () => slotsSvc?.register(
1694
+ { name: "conversation.input.dock", id: "iterate-dashboard", order: 90 },
1695
+ (props) => React.createElement(ConvergenceDashboard, props)
1696
+ )
1697
+ );
1698
+ slotsSvc.inject(
1699
+ "conversation.chat.turnTail",
1700
+ () => slotsSvc?.register(
1701
+ { name: "conversation.chat.turnTail", id: "iterate-turn-tail", select: selectTurnTail },
1702
+ (props) => React.createElement(TurnTailEntry, props)
1703
+ )
1704
+ );
1705
+ slotsSvc.inject(
1706
+ "shell.overlay",
1707
+ () => slotsSvc?.register(
1708
+ { name: "shell.overlay", id: "iterate-progress", order: 0 },
1709
+ (props) => React.createElement(ProgressCapsule, props)
1710
+ )
1711
+ );
1712
+ slotsSvc.inject(
1713
+ "settings.section",
1714
+ () => slotsSvc?.register(
1715
+ { name: "settings.section", id: "iterate-settings", order: 30, label: () => "iterate" },
1716
+ (props) => React.createElement(SettingsPanel, props)
1717
+ )
1718
+ );
924
1719
  }
925
-
926
- // 6: keep theme toggle state consistent across theme/change events.
927
- if (typeof ctx.on === 'function') {
928
- ctx.on('theme/change', () => {
929
- if (themeEnabled) applyThemeSkin()
930
- })
1720
+ if (typeof ctx.on === "function") {
1721
+ ctx.on("theme/change", () => {
1722
+ if (themeEnabled) applyThemeSkin();
1723
+ });
931
1724
  }
932
- }
1725
+ }
1726
+ return module.exports; } });
1727
+