iterate-plugin 2.12.2 → 3.2.1
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 +28 -12
- package/README.zh-CN.md +2 -1
- package/dist/approval-gate.js +16 -2
- package/dist/config-loader.js +5 -0
- package/dist/git-scope.js +61 -7
- package/dist/index.js +16 -6
- package/dist/session-hooks.js +36 -11
- package/dist/skill-prompt.js +3 -0
- package/dist/tools/decision-log.js +10 -1
- package/dist/tools/defense-events.js +260 -0
- package/dist/tools/defense-store.js +97 -0
- package/dist/tools/experience-bank.js +248 -0
- package/dist/tools/experience-store.js +132 -0
- package/dist/tools/quality-gate.js +180 -0
- package/dist/tools/quality-store.js +174 -0
- package/lib/client.js +662 -103
- package/lib/parse.js +93 -0
- package/package.json +7 -6
- package/src/approval-gate.ts +14 -2
- package/src/client/index.ts +542 -49
- package/src/config-loader.ts +5 -0
- package/src/git-scope.ts +48 -7
- package/src/index.ts +16 -6
- package/src/session-hooks.ts +33 -11
- package/src/skill-prompt.ts +3 -0
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/config.ts +1 -1
- package/src/tools/decision-log.ts +11 -2
- package/src/tools/defense-events.ts +295 -0
- package/src/tools/defense-store.ts +113 -0
- package/src/tools/experience-bank.ts +264 -0
- package/src/tools/experience-store.ts +160 -0
- package/src/tools/fix.ts +1 -1
- package/src/tools/history.ts +1 -1
- package/src/tools/prune.ts +1 -1
- package/src/tools/quality-gate.ts +193 -0
- package/src/tools/quality-store.ts +199 -0
- package/src/tools/review.ts +1 -1
- package/src/tools/transcript.ts +1 -1
- package/src/tools/triage.ts +1 -1
- package/src/types.ts +118 -0
package/src/client/index.ts
CHANGED
|
@@ -73,6 +73,10 @@ import {
|
|
|
73
73
|
countSessionImages,
|
|
74
74
|
scanSessionForTranscript,
|
|
75
75
|
normalizeTranscript,
|
|
76
|
+
filterLiveEntries,
|
|
77
|
+
filterTimelineEntries,
|
|
78
|
+
serializeObservatoryExport,
|
|
79
|
+
latestPhase,
|
|
76
80
|
SEVERITY_LABEL,
|
|
77
81
|
SEVERITY_COLOR,
|
|
78
82
|
} from '../../lib/parse.js'
|
|
@@ -370,6 +374,11 @@ const ITERATE_CSS = `
|
|
|
370
374
|
.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; }
|
|
371
375
|
.iterate-filter-search::placeholder { color: var(--dsw-alias-label-secondary); }
|
|
372
376
|
.iterate-filter-count { margin-left: auto; white-space: nowrap; }
|
|
377
|
+
.iterate-filter-chip { padding: 3px 9px; border-radius: 999px; 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; }
|
|
378
|
+
.iterate-filter-chip:hover { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
379
|
+
.iterate-filter-chip[data-active] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); font-weight: 600; }
|
|
380
|
+
.iterate-filter-clear { padding: 3px 9px; border-radius: 999px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent); color: var(--dsw-alias-state-warn-primary); background: transparent; font-size: 11px; cursor: pointer; }
|
|
381
|
+
.iterate-filter-clear:hover { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent); }
|
|
373
382
|
.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); }
|
|
374
383
|
.iterate-batch-label { margin-right: 2px; }
|
|
375
384
|
.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; }
|
|
@@ -422,6 +431,46 @@ const ITERATE_CSS = `
|
|
|
422
431
|
/* Interruption / resume + attachment chips (dashboard) */
|
|
423
432
|
.iterate-chip-resume { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
424
433
|
.iterate-chip-images { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
434
|
+
/* Live workflow-phase chip (dashboard) */
|
|
435
|
+
.iterate-chip-phase { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-label-primary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 14%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 26%, transparent); }
|
|
436
|
+
.iterate-chip-phase[data-live] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
437
|
+
|
|
438
|
+
/* v3.0: task_mode indicator chip */
|
|
439
|
+
.iterate-chip-taskmode { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
440
|
+
.iterate-chip-taskmode[data-mode="code"] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
441
|
+
.iterate-chip-taskmode[data-mode="iterate"] { color: #B45309; background: color-mix(in srgb, #B45309 12%, transparent); border: 1px solid color-mix(in srgb, #B45309 28%, transparent); }
|
|
442
|
+
|
|
443
|
+
/* v3.0: Quality gate status */
|
|
444
|
+
.iterate-gate { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
445
|
+
.iterate-gate[data-status="pass"] { 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); }
|
|
446
|
+
.iterate-gate[data-status="fail"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
447
|
+
.iterate-gate[data-status="pending"] { color: var(--dsw-alias-label-secondary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 28%, transparent); }
|
|
448
|
+
|
|
449
|
+
/* v3.0: Experience bank hit highlight */
|
|
450
|
+
.iterate-exp-hit { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; 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); font-size: 11px; color: var(--dsw-alias-state-success-primary); }
|
|
451
|
+
.iterate-exp-hit b { font-weight: 600; }
|
|
452
|
+
|
|
453
|
+
/* v3.0: Defense event type badges */
|
|
454
|
+
.iterate-defense-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
455
|
+
.iterate-defense-badge[data-type="precondition_failed"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
456
|
+
.iterate-defense-badge[data-type="rollback"] { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
457
|
+
.iterate-defense-badge[data-type="invariant_violated"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
458
|
+
.iterate-defense-badge[data-type="assumption_falsified"] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
459
|
+
|
|
460
|
+
/* v3.0: Command buttons */
|
|
461
|
+
.iterate-cmd { padding: 4px 10px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 11px; cursor: pointer; font-weight: 500; }
|
|
462
|
+
.iterate-cmd:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
463
|
+
.iterate-cmd[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
464
|
+
.iterate-cmd[data-danger] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); color: var(--dsw-alias-state-error-primary); }
|
|
465
|
+
.iterate-cmd:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
466
|
+
|
|
467
|
+
/* v3.0: Dimension score bar */
|
|
468
|
+
.iterate-dim-score { display: flex; align-items: center; gap: 6px; font-size: 11px; }
|
|
469
|
+
.iterate-dim-score-bar { width: 60px; height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
|
|
470
|
+
.iterate-dim-score-fill { height: 100%; border-radius: 3px; transition: width 300ms ease; }
|
|
471
|
+
.iterate-dim-score-fill[data-status="pass"] { background: var(--dsw-alias-state-success-primary); }
|
|
472
|
+
.iterate-dim-score-fill[data-status="warn"] { background: var(--dsw-alias-state-warn-primary); }
|
|
473
|
+
.iterate-dim-score-fill[data-status="fail"] { background: var(--dsw-alias-state-error-primary); }
|
|
425
474
|
|
|
426
475
|
/* Accessibility-switch toggle */
|
|
427
476
|
.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; }
|
|
@@ -563,6 +612,31 @@ function copyText(text: string): Promise<boolean> {
|
|
|
563
612
|
return Promise.resolve(false)
|
|
564
613
|
}
|
|
565
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Trigger a browser download of `text` as a file. Resolves to whether the
|
|
617
|
+
* download was actually initiated (fails silently when the DOM/Blob APIs are
|
|
618
|
+
* unavailable, e.g. in a sandboxed renderer without scripting).
|
|
619
|
+
*/
|
|
620
|
+
function downloadTextFile(filename: string, text: string): boolean {
|
|
621
|
+
if (typeof document === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined') return false
|
|
622
|
+
try {
|
|
623
|
+
const blob = new Blob([text], { type: 'application/json;charset=utf-8' })
|
|
624
|
+
const url = URL.createObjectURL(blob)
|
|
625
|
+
const a = document.createElement('a')
|
|
626
|
+
a.href = url
|
|
627
|
+
a.download = filename
|
|
628
|
+
a.rel = 'noopener'
|
|
629
|
+
document.body.appendChild(a)
|
|
630
|
+
a.click()
|
|
631
|
+
document.body.removeChild(a)
|
|
632
|
+
// Release the object URL on the next tick so the download has time to start.
|
|
633
|
+
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
634
|
+
return true
|
|
635
|
+
} catch {
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
566
640
|
/** Literal severity keys recognized by SEVERITY_LABEL / SEVERITY_COLOR. */
|
|
567
641
|
const SEVERITY_KEYS = ['critical', 'high', 'medium', 'low'] as const
|
|
568
642
|
|
|
@@ -742,6 +816,35 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
742
816
|
}, `附件图片 ${String(imageCount)}`)
|
|
743
817
|
: null
|
|
744
818
|
|
|
819
|
+
// Live run-state awareness: surface the transcript's current workflow phase
|
|
820
|
+
// (plan / review / fix / validate / report …) plus run liveness, so a
|
|
821
|
+
// long-running round never looks stale on the persistent dashboard.
|
|
822
|
+
const transcript = latestTranscript(session)
|
|
823
|
+
const phase = transcript ? latestPhase(transcript.phases) : ''
|
|
824
|
+
const liveChip = transcript && phase
|
|
825
|
+
? React.createElement('span', {
|
|
826
|
+
className: 'iterate-chip-phase',
|
|
827
|
+
key: 'phase',
|
|
828
|
+
'data-live': transcript.active === true ? '' : undefined,
|
|
829
|
+
title: transcript.active === true
|
|
830
|
+
? `当前处于「${phase}」阶段(运行中)`
|
|
831
|
+
: `最近一次执行停留在「${phase}」阶段(已结束)`,
|
|
832
|
+
}, `${phase} · ${transcript.active === true ? '运行中' : '已结束'}`)
|
|
833
|
+
: null
|
|
834
|
+
|
|
835
|
+
// v3.0: task_mode indicator (code/iterate)
|
|
836
|
+
// task_mode is read from the session transcript if available
|
|
837
|
+
const taskModeTranscript = latestTranscript(session)
|
|
838
|
+
const taskMode = (taskModeTranscript as unknown as { taskMode?: string })?.taskMode
|
|
839
|
+
const taskModeChip = taskMode
|
|
840
|
+
? React.createElement('span', {
|
|
841
|
+
className: 'iterate-chip-taskmode',
|
|
842
|
+
key: 'taskmode',
|
|
843
|
+
'data-mode': taskMode,
|
|
844
|
+
title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
|
|
845
|
+
}, taskMode === 'code' ? '● code' : '● iterate')
|
|
846
|
+
: null
|
|
847
|
+
|
|
745
848
|
const dimNames = Object.keys(dims)
|
|
746
849
|
const dimBadges = dimNames.slice(0, 6).map((dim) =>
|
|
747
850
|
React.createElement(
|
|
@@ -815,6 +918,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
815
918
|
fixBadge,
|
|
816
919
|
resumeChip,
|
|
817
920
|
imageChip,
|
|
921
|
+
liveChip,
|
|
922
|
+
taskModeChip,
|
|
818
923
|
React.createElement(TrendChart, { points: trend.points }),
|
|
819
924
|
...dimBadges,
|
|
820
925
|
)
|
|
@@ -920,6 +1025,8 @@ function TriagePanel(props: SlotProps) {
|
|
|
920
1025
|
const [filter, setFilter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>({ severities: [], dimensions: [], search: '' })
|
|
921
1026
|
const [selected, setSelected] = React.useState<number | null>(null)
|
|
922
1027
|
const [selectAll, setSelectAll] = React.useState(false)
|
|
1028
|
+
// v3.0: Command mode for native action buttons
|
|
1029
|
+
const [cmdCopied, setCmdCopied] = React.useState<string | null>(null)
|
|
923
1030
|
|
|
924
1031
|
/** Persist the verdicts to localStorage whenever they change. Kept OUT of
|
|
925
1032
|
* the state updater (updaters must stay pure — React may double-invoke them
|
|
@@ -1005,6 +1112,37 @@ function TriagePanel(props: SlotProps) {
|
|
|
1005
1112
|
const ignoredCount = ignored.length
|
|
1006
1113
|
const counts = countVerdicts(verdicts)
|
|
1007
1114
|
|
|
1115
|
+
// v3.0: Command handlers for native action buttons
|
|
1116
|
+
const doApproveArchitecturalFix = () => {
|
|
1117
|
+
const cmd = '请调用 `iterate_fix` 批准架构修复(设置 is_architectural: true)'
|
|
1118
|
+
copyText(cmd).then((ok) => {
|
|
1119
|
+
if (ok) {
|
|
1120
|
+
setCmdCopied('approve-arch')
|
|
1121
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const doTriggerNewRound = () => {
|
|
1127
|
+
const cmd = '请继续执行下一轮迭代审查'
|
|
1128
|
+
copyText(cmd).then((ok) => {
|
|
1129
|
+
if (ok) {
|
|
1130
|
+
setCmdCopied('new-round')
|
|
1131
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1132
|
+
}
|
|
1133
|
+
})
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const doRollbackToCheckpoint = () => {
|
|
1137
|
+
const cmd = '请调用 `iterate_checkpoint` 回滚到上一个检查点'
|
|
1138
|
+
copyText(cmd).then((ok) => {
|
|
1139
|
+
if (ok) {
|
|
1140
|
+
setCmdCopied('rollback')
|
|
1141
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1142
|
+
}
|
|
1143
|
+
})
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1008
1146
|
const doCopyYaml = () => {
|
|
1009
1147
|
const yaml = toKnownIntentionalYaml(ignored)
|
|
1010
1148
|
if (!yaml) return
|
|
@@ -1137,6 +1275,27 @@ function TriagePanel(props: SlotProps) {
|
|
|
1137
1275
|
}, '生成应用指令'),
|
|
1138
1276
|
),
|
|
1139
1277
|
),
|
|
1278
|
+
// v3.0: Command buttons for native actions (§8)
|
|
1279
|
+
React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1280
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
|
|
1281
|
+
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1282
|
+
React.createElement('button', {
|
|
1283
|
+
className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1284
|
+
onClick: doApproveArchitecturalFix,
|
|
1285
|
+
title: '批准架构修复(复制指令文本)',
|
|
1286
|
+
}, cmdCopied === 'approve-arch' ? '已复制' : '批准架构修复'),
|
|
1287
|
+
React.createElement('button', {
|
|
1288
|
+
className: 'iterate-cmd', 'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1289
|
+
onClick: doTriggerNewRound,
|
|
1290
|
+
title: '触发新一轮迭代审查',
|
|
1291
|
+
}, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
|
|
1292
|
+
React.createElement('button', {
|
|
1293
|
+
className: 'iterate-cmd', 'data-danger': '', 'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1294
|
+
onClick: doRollbackToCheckpoint,
|
|
1295
|
+
title: '回滚到上一个检查点',
|
|
1296
|
+
}, cmdCopied === 'rollback' ? '已复制' : '回滚检查点'),
|
|
1297
|
+
),
|
|
1298
|
+
),
|
|
1140
1299
|
payload
|
|
1141
1300
|
? React.createElement('div', { className: 'iterate-payload' }, payload)
|
|
1142
1301
|
: null,
|
|
@@ -1227,7 +1386,13 @@ function ProgressCapsule() {
|
|
|
1227
1386
|
}
|
|
1228
1387
|
}, [])
|
|
1229
1388
|
if (!info) return null
|
|
1230
|
-
return React.createElement('div', {
|
|
1389
|
+
return React.createElement('div', {
|
|
1390
|
+
'data-iterate-root': '',
|
|
1391
|
+
className: 'iterate-capsule',
|
|
1392
|
+
'data-ok': info.ok ? '' : undefined,
|
|
1393
|
+
role: 'status',
|
|
1394
|
+
'aria-live': 'polite',
|
|
1395
|
+
}, info.text)
|
|
1231
1396
|
}
|
|
1232
1397
|
|
|
1233
1398
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
@@ -1398,6 +1563,10 @@ const OBS_TABS = [
|
|
|
1398
1563
|
{ key: 'f5', label: '断点恢复' },
|
|
1399
1564
|
{ key: 'f6', label: '运行控制台' },
|
|
1400
1565
|
{ key: 'f7', label: '决策时间线' },
|
|
1566
|
+
// v3.0: Quality Command Center tabs
|
|
1567
|
+
{ key: 'f8', label: '质量门禁' },
|
|
1568
|
+
{ key: 'f9', label: '经验银行' },
|
|
1569
|
+
{ key: 'f10', label: '防御事件' },
|
|
1401
1570
|
] as const
|
|
1402
1571
|
|
|
1403
1572
|
/** Live subagent activity badges: type → Chinese label + dsw token color accent. */
|
|
@@ -1473,6 +1642,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1473
1642
|
const [open, setOpen] = React.useState(false)
|
|
1474
1643
|
const [tab, setTab] = React.useState('live')
|
|
1475
1644
|
const [expandedThreads, setExpandedThreads] = React.useState<Set<string>>(new Set())
|
|
1645
|
+
// 'auto' follows the per-thread set; 'all'/'none' force every thread open/closed.
|
|
1646
|
+
const [threadMode, setThreadMode] = React.useState<'auto' | 'all' | 'none'>('auto')
|
|
1476
1647
|
const [copiedKey, setCopiedKey] = React.useState<string | null>(null)
|
|
1477
1648
|
// When a clipboard write is blocked (permissions/unsupported), reveal the
|
|
1478
1649
|
// raw instruction text so the user can copy it manually instead of silently
|
|
@@ -1480,7 +1651,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1480
1651
|
const [copyFailText, setCopyFailText] = React.useState<string | null>(null)
|
|
1481
1652
|
const [nudgeText, setNudgeText] = React.useState('')
|
|
1482
1653
|
const [timelineType, setTimelineType] = React.useState('')
|
|
1654
|
+
const [timelineRound, setTimelineRound] = React.useState('')
|
|
1483
1655
|
const [timelineSearch, setTimelineSearch] = React.useState('')
|
|
1656
|
+
const [liveType, setLiveType] = React.useState('')
|
|
1657
|
+
// F3 findings triage-light filter (severity / dimension / search).
|
|
1658
|
+
const [f3Filter, setF3Filter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>(
|
|
1659
|
+
{ severities: [], dimensions: [], search: '' },
|
|
1660
|
+
)
|
|
1661
|
+
// v3.0: Experience bank search state
|
|
1662
|
+
const [expSearch, setExpSearch] = React.useState('')
|
|
1663
|
+
const [expResults, setExpResults] = React.useState<unknown[]>([])
|
|
1664
|
+
// v3.0: Defense events filter
|
|
1665
|
+
const [defenseFilter, setDefenseFilter] = React.useState('')
|
|
1484
1666
|
|
|
1485
1667
|
// Single shared "copied" flash timer (reused across all copy buttons).
|
|
1486
1668
|
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -1497,7 +1679,9 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1497
1679
|
})
|
|
1498
1680
|
}
|
|
1499
1681
|
|
|
1682
|
+
/** Toggle one thread; a manual toggle exits any forced all/none mode. */
|
|
1500
1683
|
const toggleThread = (key: string) => {
|
|
1684
|
+
setThreadMode('auto')
|
|
1501
1685
|
setExpandedThreads((prev) => {
|
|
1502
1686
|
const next = new Set(prev)
|
|
1503
1687
|
if (next.has(key)) next.delete(key); else next.add(key)
|
|
@@ -1505,6 +1689,38 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1505
1689
|
})
|
|
1506
1690
|
}
|
|
1507
1691
|
|
|
1692
|
+
/** Export the full observatory snapshot as JSON (download, then copy fallback). */
|
|
1693
|
+
const exportObservatory = () => {
|
|
1694
|
+
if (!manifest) return
|
|
1695
|
+
const json = serializeObservatoryExport(manifest, manifest.live || [])
|
|
1696
|
+
const filename = `iterate-observatory-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
|
1697
|
+
const flash = () => {
|
|
1698
|
+
setCopiedKey('export')
|
|
1699
|
+
if (copyTimer.current) clearTimeout(copyTimer.current)
|
|
1700
|
+
copyTimer.current = setTimeout(() => setCopiedKey((cur) => (cur === 'export' ? null : cur)), 1600)
|
|
1701
|
+
}
|
|
1702
|
+
if (downloadTextFile(filename, json)) { flash(); return }
|
|
1703
|
+
// Download unavailable: fall back to copying the payload to the clipboard,
|
|
1704
|
+
// and reveal the raw text if even the clipboard write is blocked.
|
|
1705
|
+
copyText(json).then((ok) => {
|
|
1706
|
+
if (!ok) { setCopyFailText(json); return }
|
|
1707
|
+
flash()
|
|
1708
|
+
})
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** Force every thread open (or closed) and drop per-thread state. */
|
|
1712
|
+
const setAllThreads = (mode: 'all' | 'none') => {
|
|
1713
|
+
setThreadMode(mode)
|
|
1714
|
+
setExpandedThreads(new Set())
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/** Whether a thread should render expanded given the forced mode + per-thread set. */
|
|
1718
|
+
const isThreadExpanded = (key: string): boolean => {
|
|
1719
|
+
if (threadMode === 'all') return true
|
|
1720
|
+
if (threadMode === 'none') return false
|
|
1721
|
+
return expandedThreads.has(key)
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1508
1724
|
// No observable transcript yet: show a compact, collapsed header row.
|
|
1509
1725
|
if (!manifest) {
|
|
1510
1726
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
@@ -1538,14 +1754,23 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1538
1754
|
if (rounds.length === 0) {
|
|
1539
1755
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无审查线程')
|
|
1540
1756
|
}
|
|
1541
|
-
return React.createElement('div', {},
|
|
1757
|
+
return React.createElement('div', {},
|
|
1758
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
1759
|
+
React.createElement('b', {}, '审查线程'),
|
|
1760
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('all'), title: '展开全部线程' }, '全部展开'),
|
|
1761
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('none'), title: '收起全部线程' }, '全部收起'),
|
|
1762
|
+
React.createElement('span', { className: 'iterate-filter-count' },
|
|
1763
|
+
`${threadMode === 'all' ? '全部展开' : threadMode === 'none' ? '全部收起' : '自由切换'} · ${rounds.length} 轮`,
|
|
1764
|
+
),
|
|
1765
|
+
),
|
|
1766
|
+
...rounds.map((r, ri) => {
|
|
1542
1767
|
const threads = r.threads || []
|
|
1543
1768
|
const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0)
|
|
1544
1769
|
const threadBlocks = threads.length === 0
|
|
1545
1770
|
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, '本轮无线程')]
|
|
1546
1771
|
: threads.map((t, ti) => {
|
|
1547
1772
|
const key = `${ri}-${ti}`
|
|
1548
|
-
const expanded =
|
|
1773
|
+
const expanded = isThreadExpanded(key)
|
|
1549
1774
|
const dim = t.dimension || '未命名维度'
|
|
1550
1775
|
const tFindings = t.findings || []
|
|
1551
1776
|
const files = t.readFiles || []
|
|
@@ -1618,33 +1843,88 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1618
1843
|
if (findings.length === 0) {
|
|
1619
1844
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
1620
1845
|
}
|
|
1621
|
-
|
|
1846
|
+
const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
|
|
1847
|
+
const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
|
|
1848
|
+
const filterActive = f3Filter.severities.length > 0 || f3Filter.dimensions.length > 0 || f3Filter.search.trim() !== ''
|
|
1849
|
+
const toggleSeverity = (sev: string) => {
|
|
1850
|
+
setF3Filter((prev) => ({
|
|
1851
|
+
...prev,
|
|
1852
|
+
severities: prev.severities.includes(sev)
|
|
1853
|
+
? prev.severities.filter((s) => s !== sev)
|
|
1854
|
+
: [...prev.severities, sev],
|
|
1855
|
+
}))
|
|
1856
|
+
}
|
|
1857
|
+
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
1858
|
+
React.createElement('b', {}, '发现'),
|
|
1859
|
+
...opts.severities.map((s) =>
|
|
1860
|
+
React.createElement('button', {
|
|
1861
|
+
key: `sev-${s.value}`,
|
|
1862
|
+
className: 'iterate-filter-chip',
|
|
1863
|
+
'data-active': f3Filter.severities.includes(s.value) ? '' : undefined,
|
|
1864
|
+
title: `${s.value}(${s.count} 项)`,
|
|
1865
|
+
onClick: () => toggleSeverity(s.value),
|
|
1866
|
+
}, `${s.value} ${s.count}`),
|
|
1867
|
+
),
|
|
1868
|
+
React.createElement('select', {
|
|
1869
|
+
className: 'iterate-filter-select',
|
|
1870
|
+
value: f3Filter.dimensions[0] || '',
|
|
1871
|
+
'aria-label': '按维度筛选',
|
|
1872
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
1873
|
+
const v = e.target.value
|
|
1874
|
+
setF3Filter((prev) => ({ ...prev, dimensions: v ? [v] : [] }))
|
|
1875
|
+
},
|
|
1876
|
+
},
|
|
1877
|
+
React.createElement('option', { value: '' }, '全部维度'),
|
|
1878
|
+
...opts.dimensions.map((d) =>
|
|
1879
|
+
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
1880
|
+
),
|
|
1881
|
+
),
|
|
1882
|
+
React.createElement('input', {
|
|
1883
|
+
className: 'iterate-filter-search',
|
|
1884
|
+
type: 'search',
|
|
1885
|
+
placeholder: '搜索发现…',
|
|
1886
|
+
'aria-label': '搜索发现',
|
|
1887
|
+
value: f3Filter.search,
|
|
1888
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
1889
|
+
setF3Filter((prev) => ({ ...prev, search: e.target.value })),
|
|
1890
|
+
}),
|
|
1891
|
+
filterActive
|
|
1892
|
+
? React.createElement('button', {
|
|
1893
|
+
className: 'iterate-filter-clear',
|
|
1894
|
+
onClick: () => setF3Filter({ severities: [], dimensions: [], search: '' }),
|
|
1895
|
+
title: '清除全部筛选',
|
|
1896
|
+
}, '清除筛选')
|
|
1897
|
+
: null,
|
|
1898
|
+
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${findings.length}`),
|
|
1899
|
+
)
|
|
1900
|
+
const findingBlocks = filtered.map((f, i) => {
|
|
1622
1901
|
const k = `f3-${i}`
|
|
1623
|
-
const
|
|
1902
|
+
const finding = f as unknown as ObsFinding
|
|
1903
|
+
const loc = `${String(finding.file || '?')}${typeof finding.line === 'number' && finding.line > 0 ? `:${finding.line}` : ''}`
|
|
1624
1904
|
const entry = {
|
|
1625
|
-
file: String(
|
|
1626
|
-
...(typeof
|
|
1627
|
-
dimension: String(
|
|
1628
|
-
reason: String(
|
|
1905
|
+
file: String(finding.file || ''),
|
|
1906
|
+
...(typeof finding.line === 'number' && finding.line > 0 ? { line: finding.line } : {}),
|
|
1907
|
+
dimension: String(finding.dimension || ''),
|
|
1908
|
+
reason: String(finding.summary || ''),
|
|
1629
1909
|
}
|
|
1630
1910
|
const knownInstruction = buildApplyInstruction([entry])
|
|
1631
|
-
const fixInstruction = buildObsFixInstruction(
|
|
1911
|
+
const fixInstruction = buildObsFixInstruction(finding)
|
|
1632
1912
|
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
1633
1913
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1634
|
-
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(
|
|
1635
|
-
React.createElement('span', {}, severityLabel(
|
|
1914
|
+
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(finding.severity) } }),
|
|
1915
|
+
React.createElement('span', {}, severityLabel(finding.severity)),
|
|
1636
1916
|
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
1637
|
-
React.createElement('span', {}, String(
|
|
1638
|
-
|
|
1917
|
+
React.createElement('span', {}, String(finding.dimension || '')),
|
|
1918
|
+
finding.acknowledged === true
|
|
1639
1919
|
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
1640
1920
|
: null,
|
|
1641
1921
|
),
|
|
1642
1922
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1643
|
-
React.createElement('div', { className: 'iterate-obs-msg' }, String(
|
|
1923
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(finding.summary || '')),
|
|
1644
1924
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
1645
1925
|
React.createElement('button', {
|
|
1646
1926
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
1647
|
-
onClick: () => copyInstruction(`${k}-fix`, String(
|
|
1927
|
+
onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
|
|
1648
1928
|
title: '复制该 finding 的建议修复文本',
|
|
1649
1929
|
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
1650
1930
|
React.createElement('button', {
|
|
@@ -1660,7 +1940,14 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1660
1940
|
),
|
|
1661
1941
|
),
|
|
1662
1942
|
)
|
|
1663
|
-
})
|
|
1943
|
+
})
|
|
1944
|
+
if (filtered.length === 0) {
|
|
1945
|
+
return React.createElement('div', {},
|
|
1946
|
+
filterBar,
|
|
1947
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的发现'),
|
|
1948
|
+
)
|
|
1949
|
+
}
|
|
1950
|
+
return React.createElement('div', {}, filterBar, ...findingBlocks)
|
|
1664
1951
|
}
|
|
1665
1952
|
|
|
1666
1953
|
// ── F4: fixes + rollback ───────────────────────────────────────────────
|
|
@@ -1786,23 +2073,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1786
2073
|
)
|
|
1787
2074
|
}
|
|
1788
2075
|
|
|
1789
|
-
// ── F7: decision timeline (type filter + search, newest first)
|
|
2076
|
+
// ── F7: decision timeline (type/round filter + search, newest first) ────
|
|
1790
2077
|
const renderTimeline = () => {
|
|
1791
2078
|
const entries = manifest.timeline || []
|
|
1792
2079
|
if (entries.length === 0) {
|
|
1793
2080
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无时间线条目')
|
|
1794
2081
|
}
|
|
1795
2082
|
const types = Array.from(new Set(entries.map((t) => String(t.type || 'unknown')).filter(Boolean))).sort()
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
2083
|
+
const rounds = Array.from(
|
|
2084
|
+
new Set(entries.map((t) => (typeof t.round === 'number' ? String(t.round) : '')).filter(Boolean)),
|
|
2085
|
+
).sort((a, b) => Number(a) - Number(b))
|
|
2086
|
+
const filtered = filterTimelineEntries(entries, {
|
|
2087
|
+
type: timelineType,
|
|
2088
|
+
round: timelineRound,
|
|
2089
|
+
search: timelineSearch,
|
|
1802
2090
|
})
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2091
|
+
const filterActive = Boolean(timelineType || timelineRound || timelineSearch.trim())
|
|
2092
|
+
const clearTimelineFilter = () => {
|
|
2093
|
+
setTimelineType('')
|
|
2094
|
+
setTimelineRound('')
|
|
2095
|
+
setTimelineSearch('')
|
|
2096
|
+
}
|
|
2097
|
+
// filterTimelineEntries already returns entries newest-first.
|
|
2098
|
+
const sorted = filtered.slice()
|
|
1806
2099
|
const rows = sorted.map((t, i) => {
|
|
1807
2100
|
const k = `f7-${i}`
|
|
1808
2101
|
const dataText = t.data && typeof t.data === 'object' ? JSON.stringify(t.data) : ''
|
|
@@ -1816,31 +2109,195 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1816
2109
|
dataText ? React.createElement('div', { className: 'iterate-obs-code' }, dataText) : null,
|
|
1817
2110
|
)
|
|
1818
2111
|
})
|
|
2112
|
+
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2113
|
+
React.createElement('b', {}, '时间线'),
|
|
2114
|
+
React.createElement('select', {
|
|
2115
|
+
className: 'iterate-filter-select',
|
|
2116
|
+
value: timelineType,
|
|
2117
|
+
'aria-label': '按时间线类型筛选',
|
|
2118
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineType(e.target.value),
|
|
2119
|
+
},
|
|
2120
|
+
React.createElement('option', { value: '' }, '全部类型'),
|
|
2121
|
+
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
2122
|
+
),
|
|
2123
|
+
React.createElement('select', {
|
|
2124
|
+
className: 'iterate-filter-select',
|
|
2125
|
+
value: timelineRound,
|
|
2126
|
+
'aria-label': '按轮次筛选',
|
|
2127
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineRound(e.target.value),
|
|
2128
|
+
},
|
|
2129
|
+
React.createElement('option', { value: '' }, '全部轮次'),
|
|
2130
|
+
...rounds.map((r) => React.createElement('option', { key: r, value: r }, `Round ${r}`)),
|
|
2131
|
+
),
|
|
2132
|
+
React.createElement('input', {
|
|
2133
|
+
className: 'iterate-filter-search',
|
|
2134
|
+
type: 'search',
|
|
2135
|
+
placeholder: '搜索时间线…',
|
|
2136
|
+
'aria-label': '搜索时间线',
|
|
2137
|
+
value: timelineSearch,
|
|
2138
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setTimelineSearch(e.target.value),
|
|
2139
|
+
}),
|
|
2140
|
+
filterActive
|
|
2141
|
+
? React.createElement('button', {
|
|
2142
|
+
className: 'iterate-filter-clear',
|
|
2143
|
+
onClick: clearTimelineFilter,
|
|
2144
|
+
title: '清除全部筛选',
|
|
2145
|
+
}, '清除')
|
|
2146
|
+
: null,
|
|
2147
|
+
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${entries.length}`),
|
|
2148
|
+
)
|
|
1819
2149
|
if (rows.length === 0) {
|
|
1820
|
-
return React.createElement('div', {
|
|
2150
|
+
return React.createElement('div', {},
|
|
2151
|
+
filterBar,
|
|
2152
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的时间线条目'),
|
|
2153
|
+
)
|
|
1821
2154
|
}
|
|
2155
|
+
return React.createElement('div', {}, filterBar, ...rows)
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// ── F8: Quality Gate ──────────────────────────────────────────────────────
|
|
2159
|
+
// v3.0: Quality command center - shows dimension convergence, verification pass rate, and overall PASS/FAIL
|
|
2160
|
+
const renderQualityGate = () => {
|
|
2161
|
+
// Quality gate data would come from manifest or be computed on-the-fly
|
|
2162
|
+
// For now, show a placeholder with instructions
|
|
2163
|
+
const qualityGateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
|
|
1822
2164
|
return React.createElement('div', {},
|
|
1823
2165
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
1824
|
-
React.createElement('
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
},
|
|
1830
|
-
|
|
1831
|
-
|
|
2166
|
+
React.createElement('b', {}, '质量门禁'),
|
|
2167
|
+
React.createElement('button', {
|
|
2168
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2169
|
+
onClick: () => copyInstruction('qgate', qualityGateInstruction),
|
|
2170
|
+
title: '复制 iterate_quality_gate 查询指令',
|
|
2171
|
+
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2172
|
+
),
|
|
2173
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2174
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2175
|
+
React.createElement('span', {}, '质量门禁视图'),
|
|
2176
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
1832
2177
|
),
|
|
2178
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2179
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
|
|
2180
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2181
|
+
React.createElement('button', {
|
|
2182
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2183
|
+
onClick: () => copyInstruction('qgate-instr', qualityGateInstruction),
|
|
2184
|
+
title: '复制查询指令',
|
|
2185
|
+
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
2186
|
+
),
|
|
2187
|
+
),
|
|
2188
|
+
),
|
|
2189
|
+
)
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// ── F9: Experience Bank ────────────────────────────────────────────────────
|
|
2193
|
+
// v3.0: Browse/search historical fixes and patterns
|
|
2194
|
+
const renderExperienceBank = () => {
|
|
2195
|
+
const expInstruction = expSearch
|
|
2196
|
+
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n${JSON.stringify({ operation: 'search', query: expSearch }, null, 2)}\n\`\`\``
|
|
2197
|
+
: '请调用 `iterate_experience` 列出所有经验'
|
|
2198
|
+
|
|
2199
|
+
const resultRows = expResults.map((entry: unknown, i: number) => {
|
|
2200
|
+
const e = entry as { id?: string; pattern?: string; description?: string; hitCount?: number; dimension?: string }
|
|
2201
|
+
return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-row' },
|
|
2202
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
|
|
2203
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, String(e.pattern || '')),
|
|
2204
|
+
React.createElement('span', { className: 'iterate-exp-hit' }, `命中 ${e.hitCount ?? 0} 次`),
|
|
2205
|
+
)
|
|
2206
|
+
})
|
|
2207
|
+
|
|
2208
|
+
return React.createElement('div', {},
|
|
2209
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2210
|
+
React.createElement('b', {}, '经验银行'),
|
|
2211
|
+
React.createElement('button', {
|
|
2212
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2213
|
+
onClick: () => copyInstruction('exp-list', '请调用 `iterate_experience` 列出所有经验'),
|
|
2214
|
+
title: '复制列出经验指令',
|
|
2215
|
+
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
2216
|
+
),
|
|
2217
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
1833
2218
|
React.createElement('input', {
|
|
1834
2219
|
className: 'iterate-filter-search',
|
|
1835
2220
|
type: 'search',
|
|
1836
|
-
placeholder: '
|
|
1837
|
-
'aria-label': '
|
|
1838
|
-
value:
|
|
1839
|
-
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
2221
|
+
placeholder: '搜索经验模式…',
|
|
2222
|
+
'aria-label': '搜索经验',
|
|
2223
|
+
value: expSearch,
|
|
2224
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
|
|
1840
2225
|
}),
|
|
1841
|
-
React.createElement('
|
|
2226
|
+
React.createElement('button', {
|
|
2227
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
|
|
2228
|
+
disabled: !expSearch,
|
|
2229
|
+
onClick: () => copyInstruction('exp-search', expInstruction),
|
|
2230
|
+
title: '复制搜索指令',
|
|
2231
|
+
}, copiedKey === 'exp-search' ? '已复制' : '搜索'),
|
|
2232
|
+
),
|
|
2233
|
+
resultRows.length > 0
|
|
2234
|
+
? React.createElement('div', {}, ...resultRows)
|
|
2235
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, '输入关键词搜索历史经验,或点击「列出经验」查看全部'),
|
|
2236
|
+
)
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// ── F10: Defense Events ────────────────────────────────────────────────────
|
|
2240
|
+
// v3.0: Shows precondition failures, rollbacks, invariant violations, assumption falsifications
|
|
2241
|
+
const renderDefenseEvents = () => {
|
|
2242
|
+
const defenseInstruction = defenseFilter
|
|
2243
|
+
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n${JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2)}\n\`\`\``
|
|
2244
|
+
: '请调用 `iterate_defense_events` 列出所有防御事件'
|
|
2245
|
+
|
|
2246
|
+
const typeOptions = [
|
|
2247
|
+
{ value: '', label: '全部类型' },
|
|
2248
|
+
{ value: 'precondition_failed', label: '前置校验失败' },
|
|
2249
|
+
{ value: 'rollback', label: '回滚' },
|
|
2250
|
+
{ value: 'invariant_violated', label: '不变量违反' },
|
|
2251
|
+
{ value: 'assumption_falsified', label: '假设被证伪' },
|
|
2252
|
+
]
|
|
2253
|
+
|
|
2254
|
+
return React.createElement('div', {},
|
|
2255
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2256
|
+
React.createElement('b', {}, '防御事件'),
|
|
2257
|
+
React.createElement('button', {
|
|
2258
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
2259
|
+
onClick: () => copyInstruction('defense-list', '请调用 `iterate_defense_events` 列出所有防御事件'),
|
|
2260
|
+
title: '复制列出防御事件指令',
|
|
2261
|
+
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
2262
|
+
React.createElement('button', {
|
|
2263
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
|
|
2264
|
+
onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
|
|
2265
|
+
title: '复制统计指令',
|
|
2266
|
+
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
2267
|
+
),
|
|
2268
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2269
|
+
React.createElement('select', {
|
|
2270
|
+
className: 'iterate-filter-select',
|
|
2271
|
+
value: defenseFilter,
|
|
2272
|
+
'aria-label': '按事件类型筛选',
|
|
2273
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
|
|
2274
|
+
},
|
|
2275
|
+
...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
|
|
2276
|
+
),
|
|
2277
|
+
defenseFilter
|
|
2278
|
+
? React.createElement('button', {
|
|
2279
|
+
className: 'iterate-filter-clear',
|
|
2280
|
+
onClick: () => setDefenseFilter(''),
|
|
2281
|
+
title: '清除类型筛选',
|
|
2282
|
+
}, '清除')
|
|
2283
|
+
: null,
|
|
2284
|
+
),
|
|
2285
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2286
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2287
|
+
React.createElement('span', {}, '防御事件流'),
|
|
2288
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2289
|
+
),
|
|
2290
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2291
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
|
|
2292
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2293
|
+
React.createElement('button', {
|
|
2294
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
2295
|
+
onClick: () => copyInstruction('defense-instr', defenseInstruction),
|
|
2296
|
+
title: '复制查询指令',
|
|
2297
|
+
}, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
|
|
2298
|
+
),
|
|
2299
|
+
),
|
|
1842
2300
|
),
|
|
1843
|
-
...rows,
|
|
1844
2301
|
)
|
|
1845
2302
|
}
|
|
1846
2303
|
|
|
@@ -1864,12 +2321,14 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1864
2321
|
}
|
|
1865
2322
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无实时活动')
|
|
1866
2323
|
}
|
|
1867
|
-
const
|
|
2324
|
+
const types = Array.from(new Set(liveEntries.map((e) => String(e.type || 'unknown')).filter(Boolean))).sort()
|
|
2325
|
+
const filtered = filterLiveEntries(liveEntries, liveType)
|
|
2326
|
+
const rows = filtered.map((e, i) => {
|
|
1868
2327
|
const type = String(e.type || 'unknown')
|
|
1869
2328
|
const meta = OBS_LIVE_META[type] || { label: String(e.type || '?'), color: OBS_LIVE_FALLBACK_COLOR }
|
|
1870
2329
|
const tool = String(e.tool || '')
|
|
1871
2330
|
const target = String(e.target || '')
|
|
1872
|
-
const time = formatObsLiveTime(e.ts)
|
|
2331
|
+
const time = formatObsLiveTime(String(e.ts || ''))
|
|
1873
2332
|
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
1874
2333
|
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: meta.color } }, meta.label),
|
|
1875
2334
|
tool
|
|
@@ -1879,13 +2338,33 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1879
2338
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, time),
|
|
1880
2339
|
)
|
|
1881
2340
|
})
|
|
1882
|
-
|
|
1883
|
-
React.createElement('
|
|
1884
|
-
|
|
1885
|
-
|
|
2341
|
+
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2342
|
+
React.createElement('b', {}, '实时活动'),
|
|
2343
|
+
React.createElement('select', {
|
|
2344
|
+
className: 'iterate-filter-select',
|
|
2345
|
+
value: liveType,
|
|
2346
|
+
'aria-label': '按活动类型筛选',
|
|
2347
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setLiveType(e.target.value),
|
|
2348
|
+
},
|
|
2349
|
+
React.createElement('option', { value: '' }, '全部活动'),
|
|
2350
|
+
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
1886
2351
|
),
|
|
1887
|
-
|
|
2352
|
+
liveType
|
|
2353
|
+
? React.createElement('button', {
|
|
2354
|
+
className: 'iterate-filter-clear',
|
|
2355
|
+
onClick: () => setLiveType(''),
|
|
2356
|
+
title: '清除类型筛选',
|
|
2357
|
+
}, '清除')
|
|
2358
|
+
: null,
|
|
2359
|
+
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${liveEntries.length}`),
|
|
1888
2360
|
)
|
|
2361
|
+
if (rows.length === 0) {
|
|
2362
|
+
return React.createElement('div', {},
|
|
2363
|
+
filterBar,
|
|
2364
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的实时活动'),
|
|
2365
|
+
)
|
|
2366
|
+
}
|
|
2367
|
+
return React.createElement('div', {}, filterBar, ...rows)
|
|
1889
2368
|
}
|
|
1890
2369
|
|
|
1891
2370
|
const renderBody = () => {
|
|
@@ -1898,6 +2377,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1898
2377
|
case 'f5': return renderCheckpoint()
|
|
1899
2378
|
case 'f6': return renderConsole()
|
|
1900
2379
|
case 'f7': return renderTimeline()
|
|
2380
|
+
// v3.0: Quality Command Center tabs
|
|
2381
|
+
case 'f8': return renderQualityGate()
|
|
2382
|
+
case 'f9': return renderExperienceBank()
|
|
2383
|
+
case 'f10': return renderDefenseEvents()
|
|
1901
2384
|
default: return null
|
|
1902
2385
|
}
|
|
1903
2386
|
}
|
|
@@ -1927,6 +2410,16 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1927
2410
|
React.createElement('div', { className: 'iterate-obs-code' }, copyFailText),
|
|
1928
2411
|
)
|
|
1929
2412
|
: null,
|
|
2413
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
2414
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2415
|
+
`${manifest.mode || 'runtime'} · ${String(manifest.updatedAt || '')}`,
|
|
2416
|
+
),
|
|
2417
|
+
React.createElement('button', {
|
|
2418
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'export' ? '' : undefined,
|
|
2419
|
+
onClick: exportObservatory,
|
|
2420
|
+
title: '将观测台全部数据导出为 JSON(优先下载,失败则复制)',
|
|
2421
|
+
}, copiedKey === 'export' ? '已导出' : '导出 JSON'),
|
|
2422
|
+
),
|
|
1930
2423
|
React.createElement('div', { className: 'iterate-obs-tabs' },
|
|
1931
2424
|
...OBS_TABS.map((t) =>
|
|
1932
2425
|
React.createElement('button', {
|