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 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
- /** Emit a "round changed" pulse to subscribed components. */
218
- function emitRoundPulse(round) {
219
- for (const fn of roundPulseListeners.slice()) fn(round)
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
- emitRoundPulse(cur)
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 localStorage persistence. */
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 = findings.map((finding, index) => {
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', { key: String(index), className: 'iterate-finding' },
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', {}, `待写回 known_intentional:${ignoredCount} 条`),
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 in a frame overlay. */
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 = (round) => {
480
- setInfo(`Round ${round} 完成`)
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
- timer = setTimeout(() => setInfo(null), 2400)
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