iterate-plugin 2.4.0 → 2.5.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/lib/client.js +245 -28
- package/lib/parse.js +331 -0
- package/package.json +1 -1
- package/src/config-write.ts +181 -0
- package/src/index.ts +8 -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 +565 -0
- package/src/types.ts +64 -0
package/lib/client.js
CHANGED
|
@@ -35,6 +35,18 @@ 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,
|
|
38
50
|
SEVERITY_LABEL,
|
|
39
51
|
SEVERITY_COLOR,
|
|
40
52
|
} from './parse.js'
|
|
@@ -150,6 +162,38 @@ const ITERATE_CSS = `
|
|
|
150
162
|
.iterate-settings-title { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
151
163
|
.iterate-settings-desc { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: 3px; line-height: 1.5; }
|
|
152
164
|
.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); }
|
|
165
|
+
|
|
166
|
+
/* Filter bar + batch toolbar (triage) */
|
|
167
|
+
.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); }
|
|
168
|
+
.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; }
|
|
169
|
+
.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; }
|
|
170
|
+
.iterate-filter-search::placeholder { color: var(--dsw-alias-label-secondary); }
|
|
171
|
+
.iterate-filter-count { margin-left: auto; white-space: nowrap; }
|
|
172
|
+
.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); }
|
|
173
|
+
.iterate-batch-label { margin-right: 2px; }
|
|
174
|
+
.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; }
|
|
175
|
+
.iterate-batch-btn:hover { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
176
|
+
.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); }
|
|
177
|
+
|
|
178
|
+
/* Trend chart (inline SVG) */
|
|
179
|
+
.iterate-trend { display: flex; align-items: flex-end; gap: 3px; height: 26px; padding: 2px 0; }
|
|
180
|
+
.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)); }
|
|
181
|
+
.iterate-trend-bar[data-hot] { background: var(--dsw-alias-brand-primary); }
|
|
182
|
+
|
|
183
|
+
/* History panel */
|
|
184
|
+
.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; }
|
|
185
|
+
.iterate-history-body { padding: 10px 14px; }
|
|
186
|
+
.iterate-history-table { width: 100%; border-collapse: collapse; font-size: 11px; color: var(--dsw-alias-label-primary); }
|
|
187
|
+
.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); }
|
|
188
|
+
.iterate-history-table td { padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
189
|
+
.iterate-history-table tr:last-child td { border-bottom: none; }
|
|
190
|
+
.iterate-history-num { font-weight: 600; }
|
|
191
|
+
.iterate-history-trendline { margin-top: 10px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
192
|
+
.iterate-completion { margin-top: 8px; padding: 6px 10px; border-radius: 8px; font-size: 11px; }
|
|
193
|
+
.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); }
|
|
194
|
+
.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); }
|
|
195
|
+
.iterate-capsule[data-ok] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
|
|
196
|
+
.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; }
|
|
153
197
|
`
|
|
154
198
|
|
|
155
199
|
// ─── Small helpers ───────────────────────────────────────────────────────────
|
|
@@ -214,9 +258,14 @@ let themeDisposer = null
|
|
|
214
258
|
let themeEnabled = true
|
|
215
259
|
const roundPulseListeners = []
|
|
216
260
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Emit a "round changed" pulse to subscribed components.
|
|
263
|
+
* The payload is `{ round, converged }` so the overlay capsule can surface
|
|
264
|
+
* completion AND convergence in one notification.
|
|
265
|
+
*/
|
|
266
|
+
function emitRoundPulse(round, converged) {
|
|
267
|
+
const payload = { round, converged: converged === true }
|
|
268
|
+
for (const fn of roundPulseListeners.slice()) fn(payload)
|
|
220
269
|
}
|
|
221
270
|
|
|
222
271
|
/** Apply the iterate theme skin (idempotent). */
|
|
@@ -262,6 +311,22 @@ function latestReport(session) {
|
|
|
262
311
|
return raw ? normalizeReport(raw) : null
|
|
263
312
|
}
|
|
264
313
|
|
|
314
|
+
/** Inline bar chart of findings-by-round (pure divs, no SVG needed). */
|
|
315
|
+
function TrendChart({ points }) {
|
|
316
|
+
if (!points || points.length === 0) return null
|
|
317
|
+
const max = trendMax(points)
|
|
318
|
+
const bars = points.map((p) =>
|
|
319
|
+
React.createElement('div', {
|
|
320
|
+
key: p.round,
|
|
321
|
+
className: 'iterate-trend-bar',
|
|
322
|
+
'data-hot': p.count > 0 ? '' : undefined,
|
|
323
|
+
title: `Round ${p.round}:${p.count} 项`,
|
|
324
|
+
style: { height: `${Math.max(4, Math.round((p.count / max) * 24))}px` },
|
|
325
|
+
}),
|
|
326
|
+
)
|
|
327
|
+
return React.createElement('div', { className: 'iterate-trend', title: '各轮发现数量趋势' }, ...bars)
|
|
328
|
+
}
|
|
329
|
+
|
|
265
330
|
/** Dashboard: live convergence strip above the composer. */
|
|
266
331
|
function ConvergenceDashboard(props) {
|
|
267
332
|
const [pulseKey, setPulseKey] = React.useState(0)
|
|
@@ -270,7 +335,8 @@ function ConvergenceDashboard(props) {
|
|
|
270
335
|
React.useEffect(() => {
|
|
271
336
|
if (!report) return
|
|
272
337
|
const cur = getCurrentRound(report)
|
|
273
|
-
|
|
338
|
+
const conv = report.convergence
|
|
339
|
+
emitRoundPulse(cur, conv && conv.converged === true)
|
|
274
340
|
setPulseKey((k) => k + 1)
|
|
275
341
|
}, [report && hashReport(report) + ':' + getCurrentRound(report)])
|
|
276
342
|
|
|
@@ -281,6 +347,7 @@ function ConvergenceDashboard(props) {
|
|
|
281
347
|
const progress = computeConvergenceProgress(report)
|
|
282
348
|
const stats = severityStats(report)
|
|
283
349
|
const dims = groupByDimension(report)
|
|
350
|
+
const trend = computeTrendMetrics(report)
|
|
284
351
|
|
|
285
352
|
const dimBadges = Object.keys(dims).slice(0, 6).map((dim) =>
|
|
286
353
|
React.createElement(
|
|
@@ -313,6 +380,7 @@ function ConvergenceDashboard(props) {
|
|
|
313
380
|
React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR.medium } }),
|
|
314
381
|
stats.medium,
|
|
315
382
|
),
|
|
383
|
+
React.createElement(TrendChart, { points: trend.points }),
|
|
316
384
|
...dimBadges,
|
|
317
385
|
)
|
|
318
386
|
}
|
|
@@ -320,6 +388,7 @@ function ConvergenceDashboard(props) {
|
|
|
320
388
|
/** Stats card (empty-findings case of the turn-tail chain). */
|
|
321
389
|
function StatsCard(props) {
|
|
322
390
|
const report = props.report
|
|
391
|
+
const [showHistory, setShowHistory] = React.useState(false)
|
|
323
392
|
const stats = severityStats(report)
|
|
324
393
|
const total = stats.critical + stats.high + stats.medium + stats.low
|
|
325
394
|
const rows = [
|
|
@@ -334,6 +403,21 @@ function StatsCard(props) {
|
|
|
334
403
|
),
|
|
335
404
|
)
|
|
336
405
|
|
|
406
|
+
const history = buildRoundHistory(report)
|
|
407
|
+
const trend = computeTrendMetrics(report)
|
|
408
|
+
const historyRows = history.map((h) =>
|
|
409
|
+
React.createElement('tr', { key: h.round },
|
|
410
|
+
React.createElement('td', { className: 'iterate-history-num' }, `Round ${h.round}`),
|
|
411
|
+
React.createElement('td', {}, String(h.count)),
|
|
412
|
+
React.createElement('td', { style: { color: SEVERITY_COLOR.critical } }, String(h.critical)),
|
|
413
|
+
React.createElement('td', { style: { color: SEVERITY_COLOR.high } }, String(h.high)),
|
|
414
|
+
React.createElement('td', { style: { color: SEVERITY_COLOR.medium } }, String(h.medium)),
|
|
415
|
+
React.createElement('td', { style: { color: SEVERITY_COLOR.low } }, String(h.low)),
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
const trendLine = `首轮 ${trend.firstRound} → 末轮 ${trend.lastRound} 项,降幅 ${trend.reductionPercent}%${trend.converged ? ',已收敛' : ''}`
|
|
419
|
+
const completion = buildCompletionSummary(report)
|
|
420
|
+
|
|
337
421
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'stats', className: 'iterate-stats' },
|
|
338
422
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
339
423
|
React.createElement('span', {}, 'Iterate · 收敛统计'),
|
|
@@ -342,10 +426,39 @@ function StatsCard(props) {
|
|
|
342
426
|
),
|
|
343
427
|
),
|
|
344
428
|
React.createElement('div', { className: 'iterate-stats-grid' }, ...rows),
|
|
429
|
+
React.createElement('div', { className: 'iterate-completion', 'data-ok': trend.converged ? '' : undefined, 'data-warn': trend.converged ? undefined : '' }, completion),
|
|
430
|
+
React.createElement('div', { className: 'iterate-triage-foot', style: { marginTop: 8, borderRadius: 8 } },
|
|
431
|
+
React.createElement('span', {}, trendLine),
|
|
432
|
+
React.createElement('button', {
|
|
433
|
+
className: 'iterate-btn',
|
|
434
|
+
'data-primary': showHistory ? '' : undefined,
|
|
435
|
+
onClick: () => setShowHistory((v) => !v),
|
|
436
|
+
}, showHistory ? '收起历史' : '历史 / 趋势'),
|
|
437
|
+
),
|
|
438
|
+
showHistory
|
|
439
|
+
? React.createElement('div', { className: 'iterate-history-body' },
|
|
440
|
+
React.createElement('table', { className: 'iterate-history-table' },
|
|
441
|
+
React.createElement('thead', {},
|
|
442
|
+
React.createElement('tr', {},
|
|
443
|
+
React.createElement('th', {}, '轮次'),
|
|
444
|
+
React.createElement('th', {}, '发现'),
|
|
445
|
+
React.createElement('th', { style: { color: SEVERITY_COLOR.critical } }, 'CRIT'),
|
|
446
|
+
React.createElement('th', { style: { color: SEVERITY_COLOR.high } }, 'HIGH'),
|
|
447
|
+
React.createElement('th', { style: { color: SEVERITY_COLOR.medium } }, 'MED'),
|
|
448
|
+
React.createElement('th', { style: { color: SEVERITY_COLOR.low } }, 'LOW'),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
React.createElement('tbody', {}, ...historyRows),
|
|
452
|
+
),
|
|
453
|
+
React.createElement('div', { className: 'iterate-history-trendline' },
|
|
454
|
+
React.createElement(TrendChart, { points: trend.points }),
|
|
455
|
+
),
|
|
456
|
+
)
|
|
457
|
+
: null,
|
|
345
458
|
)
|
|
346
459
|
}
|
|
347
460
|
|
|
348
|
-
/** Triage panel: per-finding y / n / a with
|
|
461
|
+
/** Triage panel: per-finding y / n / a with filters, batch ops, keyboard shortcuts. */
|
|
349
462
|
function TriagePanel(props) {
|
|
350
463
|
const report = props.report
|
|
351
464
|
const findings = report.findings
|
|
@@ -369,17 +482,73 @@ function TriagePanel(props) {
|
|
|
369
482
|
})
|
|
370
483
|
const [payload, setPayload] = React.useState(null)
|
|
371
484
|
const [copied, setCopied] = React.useState(false)
|
|
485
|
+
const [filter, setFilter] = React.useState({ severities: [], dimensions: [], search: '' })
|
|
486
|
+
const [selected, setSelected] = React.useState(null)
|
|
487
|
+
|
|
488
|
+
/** Persist + return the next verdicts state. */
|
|
489
|
+
const persistVerdicts = (next) => {
|
|
490
|
+
if (storage) storage.set(storageKey, JSON.stringify(next))
|
|
491
|
+
return next
|
|
492
|
+
}
|
|
372
493
|
|
|
373
494
|
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
|
-
})
|
|
495
|
+
setVerdicts((prev) => persistVerdicts({ ...prev, [String(index)]: verdict }))
|
|
379
496
|
}
|
|
380
497
|
|
|
498
|
+
// ── Filtering (visible findings + their original indices) ───────────────
|
|
499
|
+
const { filtered, indices } = filterFindingsWithIndices(findings, filter)
|
|
500
|
+
const indicesKey = indices.join(',')
|
|
501
|
+
const options = buildFilterOptions(findings)
|
|
502
|
+
const filterActive = filter.severities.length > 0 || filter.dimensions.length > 0 || filter.search !== ''
|
|
503
|
+
|
|
504
|
+
const setSeverityFilter = (value) => setFilter((f) => ({ ...f, severities: value ? [value] : [] }))
|
|
505
|
+
const setDimensionFilter = (value) => setFilter((f) => ({ ...f, dimensions: value ? [value] : [] }))
|
|
506
|
+
const setSearchFilter = (value) => setFilter((f) => ({ ...f, search: value }))
|
|
507
|
+
const clearFilter = () => setFilter({ severities: [], dimensions: [], search: '' })
|
|
508
|
+
|
|
509
|
+
// ── Batch operations (apply to the currently VISIBLE findings) ───────────
|
|
510
|
+
const applyBatch = (verdict) => {
|
|
511
|
+
setVerdicts((prev) => persistVerdicts(batchSetVerdict(prev, indices, verdict)))
|
|
512
|
+
}
|
|
513
|
+
const applyBatchAll = (verdict) => {
|
|
514
|
+
setVerdicts((prev) => persistVerdicts(setAllVerdicts(prev, verdict)))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── Keyboard shortcuts (y / n / a on the selected finding, ↑/↓ to move) ──
|
|
518
|
+
React.useEffect(() => {
|
|
519
|
+
const doc = typeof document !== 'undefined' ? document : null
|
|
520
|
+
if (!doc) return
|
|
521
|
+
const onKeyDown = (ev) => {
|
|
522
|
+
const t = ev.target
|
|
523
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
|
|
524
|
+
const verdict = keyToVerdict(ev.key)
|
|
525
|
+
if (verdict && selected !== null && indices.includes(selected)) {
|
|
526
|
+
ev.preventDefault()
|
|
527
|
+
setVerdict(selected, verdict)
|
|
528
|
+
const pos = indices.indexOf(selected)
|
|
529
|
+
const nextIdx = indices[pos + 1]
|
|
530
|
+
if (nextIdx !== undefined) setSelected(nextIdx)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
if (ev.key === 'ArrowDown' && indices.length > 0) {
|
|
534
|
+
ev.preventDefault()
|
|
535
|
+
const pos = selected === null ? -1 : indices.indexOf(selected)
|
|
536
|
+
setSelected(indices[Math.min(pos + 1, indices.length - 1)])
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
if (ev.key === 'ArrowUp' && indices.length > 0) {
|
|
540
|
+
ev.preventDefault()
|
|
541
|
+
const pos = selected === null ? 0 : indices.indexOf(selected)
|
|
542
|
+
setSelected(indices[Math.max(pos - 1, 0)])
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
doc.addEventListener('keydown', onKeyDown)
|
|
546
|
+
return () => doc.removeEventListener('keydown', onKeyDown)
|
|
547
|
+
}, [selected, indicesKey])
|
|
548
|
+
|
|
381
549
|
const ignored = collectIgnoredEntries(verdicts, findings)
|
|
382
550
|
const ignoredCount = ignored.length
|
|
551
|
+
const counts = countVerdicts(verdicts)
|
|
383
552
|
|
|
384
553
|
const doCopyYaml = () => {
|
|
385
554
|
const yaml = toKnownIntentionalYaml(ignored)
|
|
@@ -396,9 +565,11 @@ function TriagePanel(props) {
|
|
|
396
565
|
if (text) copyText(text)
|
|
397
566
|
}
|
|
398
567
|
|
|
399
|
-
const rows =
|
|
568
|
+
const rows = filtered.map((finding, i) => {
|
|
569
|
+
const index = indices[i]
|
|
400
570
|
const severity = finding.severity || 'low'
|
|
401
571
|
const verdict = verdicts[String(index)] || 'keep'
|
|
572
|
+
const isSelected = selected === index
|
|
402
573
|
const btn = (label, value, title) =>
|
|
403
574
|
React.createElement(
|
|
404
575
|
'button',
|
|
@@ -412,7 +583,12 @@ function TriagePanel(props) {
|
|
|
412
583
|
label,
|
|
413
584
|
)
|
|
414
585
|
|
|
415
|
-
return React.createElement('div', {
|
|
586
|
+
return React.createElement('div', {
|
|
587
|
+
key: String(index),
|
|
588
|
+
className: 'iterate-finding',
|
|
589
|
+
'data-selected': isSelected ? '' : undefined,
|
|
590
|
+
onClick: () => setSelected(index),
|
|
591
|
+
},
|
|
416
592
|
React.createElement('div', { className: 'iterate-finding-meta' },
|
|
417
593
|
React.createElement('span', { className: 'iterate-sev-dot', style: { background: SEVERITY_COLOR[severity] || SEVERITY_COLOR.low } }),
|
|
418
594
|
React.createElement('span', {}, String(SEVERITY_LABEL[severity] || 'LOW')),
|
|
@@ -431,12 +607,48 @@ function TriagePanel(props) {
|
|
|
431
607
|
|
|
432
608
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
433
609
|
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=已知有意'),
|
|
610
|
+
React.createElement('span', {}, `Iterate · Findings 分诊 (${filtered.length}/${findings.length})`),
|
|
611
|
+
React.createElement('span', { className: 'iterate-triage-hint' }, 'y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择'),
|
|
612
|
+
),
|
|
613
|
+
React.createElement('div', { className: 'iterate-filter' },
|
|
614
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e) => setSeverityFilter(e.target.value), title: '按严重度筛选' },
|
|
615
|
+
React.createElement('option', { value: '' }, '全部严重度'),
|
|
616
|
+
...options.severities.map((s) =>
|
|
617
|
+
React.createElement('option', { key: s.value, value: s.value }, `${SEVERITY_LABEL[s.value] || s.value} (${s.count})`),
|
|
618
|
+
),
|
|
619
|
+
),
|
|
620
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e) => setDimensionFilter(e.target.value), title: '按维度筛选' },
|
|
621
|
+
React.createElement('option', { value: '' }, '全部维度'),
|
|
622
|
+
...options.dimensions.map((d) =>
|
|
623
|
+
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
624
|
+
),
|
|
625
|
+
),
|
|
626
|
+
React.createElement('input', {
|
|
627
|
+
className: 'iterate-filter-search',
|
|
628
|
+
type: 'search',
|
|
629
|
+
placeholder: '搜索文件 / 摘要…',
|
|
630
|
+
value: filter.search,
|
|
631
|
+
onChange: (e) => setSearchFilter(e.target.value),
|
|
632
|
+
}),
|
|
633
|
+
React.createElement('span', { className: 'iterate-filter-count' },
|
|
634
|
+
filterActive
|
|
635
|
+
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, `清除筛选(显示 ${filtered.length})`)
|
|
636
|
+
: `共 ${findings.length} 项`,
|
|
637
|
+
),
|
|
638
|
+
),
|
|
639
|
+
React.createElement('div', { className: 'iterate-batch' },
|
|
640
|
+
React.createElement('span', { className: 'iterate-batch-label' }, '批量(当前可见):'),
|
|
641
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('keep') }, '全部 y'),
|
|
642
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('skip') }, '全部 n'),
|
|
643
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('ignore') }, '全部 a'),
|
|
644
|
+
React.createElement('span', { className: 'iterate-batch-label', style: { marginLeft: 8 } }, '全部:'),
|
|
645
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatchAll('keep') }, 'y'),
|
|
646
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatchAll('skip') }, 'n'),
|
|
647
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatchAll('ignore') }, 'a'),
|
|
436
648
|
),
|
|
437
649
|
...rows,
|
|
438
650
|
React.createElement('div', { className: 'iterate-triage-foot' },
|
|
439
|
-
React.createElement('span', {},
|
|
651
|
+
React.createElement('span', {}, `y ${counts.keep} · n ${counts.skip} · a ${counts.ignore} · 待写回 known_intentional:${ignoredCount} 条`),
|
|
440
652
|
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
441
653
|
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyYaml }, copied ? '已复制' : '复制 known_intentional'),
|
|
442
654
|
React.createElement('button', { className: 'iterate-btn', onClick: doBuildInstruction }, '生成应用指令'),
|
|
@@ -471,15 +683,18 @@ function TurnTailEntry(props) {
|
|
|
471
683
|
return React.createElement(TriagePanel, { report })
|
|
472
684
|
}
|
|
473
685
|
|
|
474
|
-
/** Progress capsule: briefly surfaces round-completion
|
|
686
|
+
/** Progress capsule: briefly surfaces round-completion (incl. convergence). */
|
|
475
687
|
function ProgressCapsule() {
|
|
476
688
|
const [info, setInfo] = React.useState(null)
|
|
477
689
|
React.useEffect(() => {
|
|
478
690
|
let timer = null
|
|
479
|
-
const listener = (
|
|
480
|
-
|
|
691
|
+
const listener = (payload) => {
|
|
692
|
+
const converged = payload && payload.converged === true
|
|
693
|
+
const round = payload && typeof payload.round === 'number' ? payload.round : '?'
|
|
694
|
+
setInfo({ text: converged ? `Round ${round} 完成 · 已收敛` : `Round ${round} 完成`, ok: converged })
|
|
481
695
|
if (timer) clearTimeout(timer)
|
|
482
|
-
|
|
696
|
+
// Converged notifications stay a bit longer.
|
|
697
|
+
timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400)
|
|
483
698
|
}
|
|
484
699
|
roundPulseListeners.push(listener)
|
|
485
700
|
return () => {
|
|
@@ -489,13 +704,15 @@ function ProgressCapsule() {
|
|
|
489
704
|
}
|
|
490
705
|
}, [])
|
|
491
706
|
if (!info) return null
|
|
492
|
-
return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-capsule' }, info)
|
|
707
|
+
return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-capsule', 'data-ok': info.ok ? '' : undefined }, info.text)
|
|
493
708
|
}
|
|
494
709
|
|
|
495
710
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
496
711
|
function SettingsPanel() {
|
|
497
712
|
const [enabled, setEnabled] = React.useState(themeEnabled)
|
|
498
713
|
const [copied, setCopied] = React.useState(false)
|
|
714
|
+
const [showGuide, setShowGuide] = React.useState(false)
|
|
715
|
+
const guide = buildConfigEditGuide()
|
|
499
716
|
|
|
500
717
|
const toggleTheme = () => {
|
|
501
718
|
const next = !enabled
|
|
@@ -504,12 +721,6 @@ function SettingsPanel() {
|
|
|
504
721
|
}
|
|
505
722
|
|
|
506
723
|
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
724
|
copyText(guide)
|
|
514
725
|
setCopied(true)
|
|
515
726
|
setTimeout(() => setCopied(false), 1600)
|
|
@@ -538,10 +749,16 @@ function SettingsPanel() {
|
|
|
538
749
|
React.createElement('div', { className: 'iterate-settings-row' },
|
|
539
750
|
React.createElement('div', {},
|
|
540
751
|
React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
|
|
541
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数写在项目的 iterate.config.yaml
|
|
752
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数写在项目的 iterate.config.yaml。复制指引让模型为你调整,或展开查看可编辑字段。'),
|
|
753
|
+
),
|
|
754
|
+
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
755
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyGuide }, copied ? '已复制' : '复制指引'),
|
|
756
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setShowGuide((v) => !v) }, showGuide ? '收起' : '查看'),
|
|
542
757
|
),
|
|
543
|
-
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': copied ? '' : undefined, onClick: doCopyGuide }, copied ? '已复制' : '复制指引'),
|
|
544
758
|
),
|
|
759
|
+
showGuide
|
|
760
|
+
? React.createElement('div', { className: 'iterate-settings-guide' }, guide)
|
|
761
|
+
: null,
|
|
545
762
|
)
|
|
546
763
|
}
|
|
547
764
|
|