iterate-plugin 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -15
- package/lib/client.js +326 -29
- package/lib/parse.js +399 -0
- package/package.json +1 -1
- package/src/config-write.ts +181 -0
- package/src/index.ts +12 -1
- package/src/paths.ts +38 -0
- package/src/skill-prompt.ts +117 -24
- package/src/tools/checkpoint.ts +285 -0
- package/src/tools/config.ts +64 -6
- package/src/tools/decision-log.ts +4 -4
- package/src/tools/fix.ts +576 -0
- package/src/tools/history.ts +162 -0
- package/src/tools/prune.ts +313 -0
- package/src/tools/triage.ts +7 -0
- package/src/types.ts +64 -0
package/lib/client.js
CHANGED
|
@@ -35,6 +35,20 @@ import {
|
|
|
35
35
|
toKnownIntentionalYaml,
|
|
36
36
|
buildApplyInstruction,
|
|
37
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,
|
|
38
52
|
SEVERITY_LABEL,
|
|
39
53
|
SEVERITY_COLOR,
|
|
40
54
|
} from './parse.js'
|
|
@@ -150,6 +164,41 @@ const ITERATE_CSS = `
|
|
|
150
164
|
.iterate-settings-title { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
151
165
|
.iterate-settings-desc { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: 3px; line-height: 1.5; }
|
|
152
166
|
.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); }
|
|
167
|
+
|
|
168
|
+
/* Filter bar + batch toolbar (triage) */
|
|
169
|
+
.iterate-filter { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 8px 14px; border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
170
|
+
.iterate-filter-select { padding: 4px 8px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-size: 11px; }
|
|
171
|
+
.iterate-filter-search { padding: 4px 8px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-size: 11px; min-width: 140px; }
|
|
172
|
+
.iterate-filter-search::placeholder { color: var(--dsw-alias-label-secondary); }
|
|
173
|
+
.iterate-filter-count { margin-left: auto; white-space: nowrap; }
|
|
174
|
+
.iterate-batch { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
175
|
+
.iterate-batch-label { margin-right: 2px; }
|
|
176
|
+
.iterate-batch-btn { 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; }
|
|
177
|
+
.iterate-batch-btn:hover { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
178
|
+
.iterate-finding[data-selected] { outline: 1px solid var(--dsw-alias-brand-primary); outline-offset: -1px; background: color-mix(in srgb, var(--dsw-alias-brand-primary) 6%, transparent); }
|
|
179
|
+
|
|
180
|
+
/* Trend chart (inline SVG) */
|
|
181
|
+
.iterate-trend { display: flex; align-items: flex-end; gap: 3px; height: 26px; padding: 2px 0; }
|
|
182
|
+
.iterate-trend-bar { width: 6px; border-radius: 2px 2px 0 0; background: color-mix(in srgb, var(--dsw-alias-brand-primary) 70%, var(--dsw-alias-bg-layer-2)); }
|
|
183
|
+
.iterate-trend-bar[data-hot] { background: var(--dsw-alias-brand-primary); }
|
|
184
|
+
|
|
185
|
+
/* History panel */
|
|
186
|
+
.iterate-history { margin: 10px 0; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
|
|
187
|
+
.iterate-history-body { padding: 10px 14px; }
|
|
188
|
+
.iterate-history-table { width: 100%; border-collapse: collapse; font-size: 11px; color: var(--dsw-alias-label-primary); }
|
|
189
|
+
.iterate-history-table th { text-align: left; font-weight: 600; color: var(--dsw-alias-label-secondary); padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
190
|
+
.iterate-history-table td { padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
191
|
+
.iterate-history-table tr:last-child td { border-bottom: none; }
|
|
192
|
+
.iterate-history-num { font-weight: 600; }
|
|
193
|
+
.iterate-history-trendline { margin-top: 10px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
194
|
+
.iterate-completion { margin-top: 8px; padding: 6px 10px; border-radius: 8px; font-size: 11px; }
|
|
195
|
+
.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
|
+
.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
|
+
.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
|
+
.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
|
+
.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
|
+
.iterate-batch-check input { margin: 0; cursor: pointer; }
|
|
153
202
|
`
|
|
154
203
|
|
|
155
204
|
// ─── Small helpers ───────────────────────────────────────────────────────────
|
|
@@ -181,6 +230,7 @@ function createStorage() {
|
|
|
181
230
|
get(key) { return window.localStorage.getItem(key) },
|
|
182
231
|
set(key, value) { window.localStorage.setItem(key, value) },
|
|
183
232
|
remove(key) { window.localStorage.removeItem(key) },
|
|
233
|
+
keys() { return Object.keys(window.localStorage) },
|
|
184
234
|
}
|
|
185
235
|
} catch {
|
|
186
236
|
const mem = new Map()
|
|
@@ -188,8 +238,29 @@ function createStorage() {
|
|
|
188
238
|
get(key) { return mem.has(key) ? mem.get(key) : null },
|
|
189
239
|
set(key, value) { mem.set(key, value) },
|
|
190
240
|
remove(key) { mem.delete(key) },
|
|
241
|
+
keys() { return [...mem.keys()] },
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
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
|
+
function removeStorageByPrefix(prefix) {
|
|
251
|
+
if (!storage) return 0
|
|
252
|
+
let removed = 0
|
|
253
|
+
try {
|
|
254
|
+
for (const key of storage.keys()) {
|
|
255
|
+
if (key.startsWith(prefix)) {
|
|
256
|
+
storage.remove(key)
|
|
257
|
+
removed++
|
|
258
|
+
}
|
|
191
259
|
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log('failed to clear storage prefix', prefix, err)
|
|
192
262
|
}
|
|
263
|
+
return removed
|
|
193
264
|
}
|
|
194
265
|
|
|
195
266
|
/** Copy text to the clipboard, returning whether it succeeded. */
|
|
@@ -214,9 +285,14 @@ let themeDisposer = null
|
|
|
214
285
|
let themeEnabled = true
|
|
215
286
|
const roundPulseListeners = []
|
|
216
287
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
*/
|
|
293
|
+
function emitRoundPulse(round, converged) {
|
|
294
|
+
const payload = { round, converged: converged === true }
|
|
295
|
+
for (const fn of roundPulseListeners.slice()) fn(payload)
|
|
220
296
|
}
|
|
221
297
|
|
|
222
298
|
/** Apply the iterate theme skin (idempotent). */
|
|
@@ -262,6 +338,22 @@ function latestReport(session) {
|
|
|
262
338
|
return raw ? normalizeReport(raw) : null
|
|
263
339
|
}
|
|
264
340
|
|
|
341
|
+
/** Inline bar chart of findings-by-round (pure divs, no SVG needed). */
|
|
342
|
+
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', {
|
|
347
|
+
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)
|
|
355
|
+
}
|
|
356
|
+
|
|
265
357
|
/** Dashboard: live convergence strip above the composer. */
|
|
266
358
|
function ConvergenceDashboard(props) {
|
|
267
359
|
const [pulseKey, setPulseKey] = React.useState(0)
|
|
@@ -270,7 +362,8 @@ function ConvergenceDashboard(props) {
|
|
|
270
362
|
React.useEffect(() => {
|
|
271
363
|
if (!report) return
|
|
272
364
|
const cur = getCurrentRound(report)
|
|
273
|
-
|
|
365
|
+
const conv = report.convergence
|
|
366
|
+
emitRoundPulse(cur, conv && conv.converged === true)
|
|
274
367
|
setPulseKey((k) => k + 1)
|
|
275
368
|
}, [report && hashReport(report) + ':' + getCurrentRound(report)])
|
|
276
369
|
|
|
@@ -281,6 +374,7 @@ function ConvergenceDashboard(props) {
|
|
|
281
374
|
const progress = computeConvergenceProgress(report)
|
|
282
375
|
const stats = severityStats(report)
|
|
283
376
|
const dims = groupByDimension(report)
|
|
377
|
+
const trend = computeTrendMetrics(report)
|
|
284
378
|
|
|
285
379
|
const dimBadges = Object.keys(dims).slice(0, 6).map((dim) =>
|
|
286
380
|
React.createElement(
|
|
@@ -290,6 +384,17 @@ function ConvergenceDashboard(props) {
|
|
|
290
384
|
),
|
|
291
385
|
)
|
|
292
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
|
+
|
|
293
398
|
return React.createElement(
|
|
294
399
|
'div',
|
|
295
400
|
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', className: 'iterate-dashboard' },
|
|
@@ -313,6 +418,8 @@ function ConvergenceDashboard(props) {
|
|
|
313
418
|
React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR.medium } }),
|
|
314
419
|
stats.medium,
|
|
315
420
|
),
|
|
421
|
+
fixBadge,
|
|
422
|
+
React.createElement(TrendChart, { points: trend.points }),
|
|
316
423
|
...dimBadges,
|
|
317
424
|
)
|
|
318
425
|
}
|
|
@@ -320,6 +427,7 @@ function ConvergenceDashboard(props) {
|
|
|
320
427
|
/** Stats card (empty-findings case of the turn-tail chain). */
|
|
321
428
|
function StatsCard(props) {
|
|
322
429
|
const report = props.report
|
|
430
|
+
const [showHistory, setShowHistory] = React.useState(false)
|
|
323
431
|
const stats = severityStats(report)
|
|
324
432
|
const total = stats.critical + stats.high + stats.medium + stats.low
|
|
325
433
|
const rows = [
|
|
@@ -334,6 +442,21 @@ function StatsCard(props) {
|
|
|
334
442
|
),
|
|
335
443
|
)
|
|
336
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)),
|
|
455
|
+
),
|
|
456
|
+
)
|
|
457
|
+
const trendLine = `首轮 ${trend.firstRound} → 末轮 ${trend.lastRound} 项,降幅 ${trend.reductionPercent}%${trend.converged ? ',已收敛' : ''}`
|
|
458
|
+
const completion = buildCompletionSummary(report)
|
|
459
|
+
|
|
337
460
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'stats', className: 'iterate-stats' },
|
|
338
461
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
339
462
|
React.createElement('span', {}, 'Iterate · 收敛统计'),
|
|
@@ -342,10 +465,39 @@ function StatsCard(props) {
|
|
|
342
465
|
),
|
|
343
466
|
),
|
|
344
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,
|
|
345
497
|
)
|
|
346
498
|
}
|
|
347
499
|
|
|
348
|
-
/** Triage panel: per-finding y / n / a with
|
|
500
|
+
/** Triage panel: per-finding y / n / a with filters, batch ops, keyboard shortcuts. */
|
|
349
501
|
function TriagePanel(props) {
|
|
350
502
|
const report = props.report
|
|
351
503
|
const findings = report.findings
|
|
@@ -369,17 +521,81 @@ function TriagePanel(props) {
|
|
|
369
521
|
})
|
|
370
522
|
const [payload, setPayload] = React.useState(null)
|
|
371
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. */
|
|
529
|
+
const persistVerdicts = (next) => {
|
|
530
|
+
if (storage) storage.set(storageKey, JSON.stringify(next))
|
|
531
|
+
return next
|
|
532
|
+
}
|
|
372
533
|
|
|
373
534
|
const setVerdict = (index, verdict) => {
|
|
374
|
-
setVerdicts((prev) => {
|
|
375
|
-
const next = { ...prev, [String(index)]: verdict }
|
|
376
|
-
if (storage) storage.set(storageKey, JSON.stringify(next))
|
|
377
|
-
return next
|
|
378
|
-
})
|
|
535
|
+
setVerdicts((prev) => persistVerdicts({ ...prev, [String(index)]: verdict }))
|
|
379
536
|
}
|
|
380
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
|
|
553
|
+
const applyBatch = (verdict) => {
|
|
554
|
+
setVerdicts((prev) => persistVerdicts(batchSetVerdict(prev, batchTarget, verdict)))
|
|
555
|
+
}
|
|
556
|
+
const applyBatchAll = (verdict) => {
|
|
557
|
+
setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, verdict)))
|
|
558
|
+
}
|
|
559
|
+
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) ──
|
|
565
|
+
React.useEffect(() => {
|
|
566
|
+
const doc = typeof document !== 'undefined' ? document : null
|
|
567
|
+
if (!doc) return
|
|
568
|
+
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)
|
|
572
|
+
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
|
|
579
|
+
}
|
|
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
|
|
585
|
+
}
|
|
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)])
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
doc.addEventListener('keydown', onKeyDown)
|
|
593
|
+
return () => doc.removeEventListener('keydown', onKeyDown)
|
|
594
|
+
}, [selected, indicesKey])
|
|
595
|
+
|
|
381
596
|
const ignored = collectIgnoredEntries(verdicts, findings)
|
|
382
597
|
const ignoredCount = ignored.length
|
|
598
|
+
const counts = countVerdicts(verdicts)
|
|
383
599
|
|
|
384
600
|
const doCopyYaml = () => {
|
|
385
601
|
const yaml = toKnownIntentionalYaml(ignored)
|
|
@@ -396,9 +612,11 @@ function TriagePanel(props) {
|
|
|
396
612
|
if (text) copyText(text)
|
|
397
613
|
}
|
|
398
614
|
|
|
399
|
-
const rows =
|
|
615
|
+
const rows = filtered.map((finding, i) => {
|
|
616
|
+
const index = indices[i]
|
|
400
617
|
const severity = finding.severity || 'low'
|
|
401
618
|
const verdict = verdicts[String(index)] || 'keep'
|
|
619
|
+
const isSelected = selected === index
|
|
402
620
|
const btn = (label, value, title) =>
|
|
403
621
|
React.createElement(
|
|
404
622
|
'button',
|
|
@@ -412,7 +630,12 @@ function TriagePanel(props) {
|
|
|
412
630
|
label,
|
|
413
631
|
)
|
|
414
632
|
|
|
415
|
-
return React.createElement('div', {
|
|
633
|
+
return React.createElement('div', {
|
|
634
|
+
key: String(index),
|
|
635
|
+
className: 'iterate-finding',
|
|
636
|
+
'data-selected': isSelected ? '' : undefined,
|
|
637
|
+
onClick: () => setSelected(index),
|
|
638
|
+
},
|
|
416
639
|
React.createElement('div', { className: 'iterate-finding-meta' },
|
|
417
640
|
React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR[severity] || SEVERITY_COLOR.low } }),
|
|
418
641
|
React.createElement('span', {}, String(SEVERITY_LABEL[severity] || 'LOW')),
|
|
@@ -431,12 +654,53 @@ function TriagePanel(props) {
|
|
|
431
654
|
|
|
432
655
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
433
656
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
434
|
-
React.createElement('span', {}, `Iterate · Findings 分诊 (${findings.length})`),
|
|
435
|
-
React.createElement('span', { className: 'iterate-triage-hint' }, 'y=修复 · n=跳过 · a=已知有意'),
|
|
657
|
+
React.createElement('span', {}, `Iterate · Findings 分诊 (${filtered.length}/${findings.length})`),
|
|
658
|
+
React.createElement('span', { className: 'iterate-triage-hint' }, 'y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择'),
|
|
659
|
+
),
|
|
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
|
+
),
|
|
666
|
+
),
|
|
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
|
+
),
|
|
672
|
+
),
|
|
673
|
+
React.createElement('input', {
|
|
674
|
+
className: 'iterate-filter-search',
|
|
675
|
+
type: 'search',
|
|
676
|
+
placeholder: '搜索文件 / 摘要…',
|
|
677
|
+
value: filter.search,
|
|
678
|
+
onChange: (e) => setSearchFilter(e.target.value),
|
|
679
|
+
}),
|
|
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
|
+
),
|
|
685
|
+
),
|
|
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}` : '全选',
|
|
691
|
+
),
|
|
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(修复)' }, '重置'),
|
|
436
700
|
),
|
|
437
701
|
...rows,
|
|
438
702
|
React.createElement('div', { className: 'iterate-triage-foot' },
|
|
439
|
-
React.createElement('span', {},
|
|
703
|
+
React.createElement('span', {}, `y ${counts.keep} · n ${counts.skip} · a ${counts.ignore} · 待写回 known_intentional:${ignoredCount} 条`),
|
|
440
704
|
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
441
705
|
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyYaml }, copied ? '已复制' : '复制 known_intentional'),
|
|
442
706
|
React.createElement('button', { className: 'iterate-btn', onClick: doBuildInstruction }, '生成应用指令'),
|
|
@@ -471,15 +735,18 @@ function TurnTailEntry(props) {
|
|
|
471
735
|
return React.createElement(TriagePanel, { report })
|
|
472
736
|
}
|
|
473
737
|
|
|
474
|
-
/** Progress capsule: briefly surfaces round-completion
|
|
738
|
+
/** Progress capsule: briefly surfaces round-completion (incl. convergence). */
|
|
475
739
|
function ProgressCapsule() {
|
|
476
740
|
const [info, setInfo] = React.useState(null)
|
|
477
741
|
React.useEffect(() => {
|
|
478
742
|
let timer = null
|
|
479
|
-
const listener = (
|
|
480
|
-
|
|
743
|
+
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 })
|
|
481
747
|
if (timer) clearTimeout(timer)
|
|
482
|
-
|
|
748
|
+
// Converged notifications stay a bit longer.
|
|
749
|
+
timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400)
|
|
483
750
|
}
|
|
484
751
|
roundPulseListeners.push(listener)
|
|
485
752
|
return () => {
|
|
@@ -489,13 +756,18 @@ function ProgressCapsule() {
|
|
|
489
756
|
}
|
|
490
757
|
}, [])
|
|
491
758
|
if (!info) return null
|
|
492
|
-
return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-capsule' }, info)
|
|
759
|
+
return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-capsule', 'data-ok': info.ok ? '' : undefined }, info.text)
|
|
493
760
|
}
|
|
494
761
|
|
|
495
762
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
496
763
|
function SettingsPanel() {
|
|
497
764
|
const [enabled, setEnabled] = React.useState(themeEnabled)
|
|
498
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()
|
|
499
771
|
|
|
500
772
|
const toggleTheme = () => {
|
|
501
773
|
const next = !enabled
|
|
@@ -504,17 +776,17 @@ function SettingsPanel() {
|
|
|
504
776
|
}
|
|
505
777
|
|
|
506
778
|
const doCopyGuide = () => {
|
|
507
|
-
const guide =
|
|
508
|
-
'iterate 配置指引\n' +
|
|
509
|
-
'----------------\n' +
|
|
510
|
-
'目标 / 维度 / 最大轮数在项目的 iterate.config.yaml 中配置。\n' +
|
|
511
|
-
'让模型修改配置:请调用 iterate_config(读)与 iterate_triage(写 known_intentional),\n' +
|
|
512
|
-
'并说明你希望调整的字段(goal、dimensions、max_rounds 等)。\n'
|
|
513
779
|
copyText(guide)
|
|
514
780
|
setCopied(true)
|
|
515
781
|
setTimeout(() => setCopied(false), 1600)
|
|
516
782
|
}
|
|
517
783
|
|
|
784
|
+
const doClearTriage = () => {
|
|
785
|
+
const count = removeStorageByPrefix(TRIAGE_STORAGE_PREFIX)
|
|
786
|
+
setClearedCount(count)
|
|
787
|
+
setTimeout(() => setClearedCount(null), 3000)
|
|
788
|
+
}
|
|
789
|
+
|
|
518
790
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'settings', className: 'iterate-settings' },
|
|
519
791
|
React.createElement('div', { className: 'iterate-settings-title' }, 'iterate 设置'),
|
|
520
792
|
React.createElement('div', { className: 'iterate-settings-row' },
|
|
@@ -533,15 +805,40 @@ function SettingsPanel() {
|
|
|
533
805
|
React.createElement('div', { className: 'iterate-settings-title' }, '分诊持久化'),
|
|
534
806
|
React.createElement('div', { className: 'iterate-settings-desc' }, '分诊面板的 y/n/a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。'),
|
|
535
807
|
),
|
|
536
|
-
React.createElement('span', {
|
|
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} 条` : '清除分诊'),
|
|
817
|
+
),
|
|
537
818
|
),
|
|
538
819
|
React.createElement('div', { className: 'iterate-settings-row' },
|
|
539
820
|
React.createElement('div', {},
|
|
540
821
|
React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
|
|
541
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数写在项目的 iterate.config.yaml
|
|
822
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数写在项目的 iterate.config.yaml。复制指引让模型为你调整,或展开查看可编辑字段。'),
|
|
823
|
+
),
|
|
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 ? '收起' : '查看'),
|
|
542
827
|
),
|
|
543
|
-
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyGuide }, copied ? '已复制' : '复制指引'),
|
|
544
828
|
),
|
|
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 工具用于查看和管理。'),
|
|
836
|
+
),
|
|
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,
|
|
545
842
|
)
|
|
546
843
|
}
|
|
547
844
|
|