iterate-plugin 2.12.3 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -76
- package/README.zh-CN.md +129 -80
- package/dist/approval-gate.js +16 -19
- package/dist/config-loader.js +5 -0
- package/dist/index.js +16 -6
- package/dist/session-hooks.js +36 -11
- package/dist/skill-prompt.js +3 -0
- package/dist/tools/checkpoint.js +22 -3
- package/dist/tools/decision-log.js +10 -1
- package/dist/tools/defense-events.js +263 -0
- package/dist/tools/defense-store.js +103 -0
- package/dist/tools/experience-bank.js +251 -0
- package/dist/tools/experience-store.js +138 -0
- package/dist/tools/quality-gate.js +183 -0
- package/dist/tools/quality-store.js +180 -0
- package/dist/tools/transcript.js +8 -1
- package/dist/transcript.js +10 -0
- package/lib/client.js +779 -15
- package/lib/parse.js +352 -0
- package/package.json +6 -6
- package/src/approval-gate.ts +14 -28
- package/src/client/index.ts +564 -16
- package/src/config-loader.ts +5 -0
- 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 +22 -3
- package/src/tools/decision-log.ts +10 -1
- package/src/tools/defense-events.ts +298 -0
- package/src/tools/defense-store.ts +122 -0
- package/src/tools/experience-bank.ts +267 -0
- package/src/tools/experience-store.ts +169 -0
- package/src/tools/quality-gate.ts +196 -0
- package/src/tools/quality-store.ts +208 -0
- package/src/tools/transcript.ts +8 -1
- package/src/transcript.ts +11 -0
- package/src/types.ts +118 -0
package/src/client/index.ts
CHANGED
|
@@ -73,6 +73,9 @@ import {
|
|
|
73
73
|
countSessionImages,
|
|
74
74
|
scanSessionForTranscript,
|
|
75
75
|
normalizeTranscript,
|
|
76
|
+
scanSessionForQualityGate,
|
|
77
|
+
scanSessionForExperienceBank,
|
|
78
|
+
scanSessionForDefenseEvents,
|
|
76
79
|
filterLiveEntries,
|
|
77
80
|
filterTimelineEntries,
|
|
78
81
|
serializeObservatoryExport,
|
|
@@ -228,6 +231,7 @@ interface ObsManifest {
|
|
|
228
231
|
updatedAt?: string
|
|
229
232
|
active?: boolean
|
|
230
233
|
mode?: string
|
|
234
|
+
taskMode?: string | null
|
|
231
235
|
goal?: string
|
|
232
236
|
phases?: string[]
|
|
233
237
|
round?: number
|
|
@@ -243,6 +247,80 @@ interface ObsManifest {
|
|
|
243
247
|
approval?: ObsApproval
|
|
244
248
|
}
|
|
245
249
|
|
|
250
|
+
// ─── Quality command center data types (v3.1+; mirrors src/types.ts) ────────
|
|
251
|
+
// These mirror the JSON the three command-center tools return so the F8/F9/F10
|
|
252
|
+
// tabs can render session-scanned data with full optional chaining.
|
|
253
|
+
|
|
254
|
+
interface ObsQualityGateDimension {
|
|
255
|
+
dimension?: string
|
|
256
|
+
convergenceRate?: number
|
|
257
|
+
findingsCount?: number
|
|
258
|
+
fixedCount?: number
|
|
259
|
+
score?: number
|
|
260
|
+
status?: string
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface ObsQualityGate {
|
|
264
|
+
timestamp?: string
|
|
265
|
+
overallStatus?: string
|
|
266
|
+
overallScore?: number
|
|
267
|
+
verificationPassRate?: number
|
|
268
|
+
totalChecks?: number
|
|
269
|
+
passedChecks?: number
|
|
270
|
+
failedChecks?: number
|
|
271
|
+
failReason?: string | null
|
|
272
|
+
totalFindings?: number
|
|
273
|
+
criticalCount?: number
|
|
274
|
+
highCount?: number
|
|
275
|
+
mediumCount?: number
|
|
276
|
+
lowCount?: number
|
|
277
|
+
dimensions?: ObsQualityGateDimension[]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface ObsExperienceEntry {
|
|
281
|
+
id?: string
|
|
282
|
+
timestamp?: string
|
|
283
|
+
dimension?: string
|
|
284
|
+
pattern?: string
|
|
285
|
+
description?: string
|
|
286
|
+
verifiedFix?: string
|
|
287
|
+
findingSummary?: string
|
|
288
|
+
severity?: string
|
|
289
|
+
hitCount?: number
|
|
290
|
+
lastHitAt?: string | null
|
|
291
|
+
files?: string[]
|
|
292
|
+
tags?: string[]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface ObsExperienceResult {
|
|
296
|
+
operation?: string
|
|
297
|
+
count?: number
|
|
298
|
+
totalHits?: number
|
|
299
|
+
added?: boolean
|
|
300
|
+
entries?: ObsExperienceEntry[]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface ObsDefenseEvent {
|
|
304
|
+
id?: string
|
|
305
|
+
timestamp?: string
|
|
306
|
+
round?: number
|
|
307
|
+
type?: string
|
|
308
|
+
description?: string
|
|
309
|
+
defense?: string
|
|
310
|
+
outcome?: string
|
|
311
|
+
file?: string | null
|
|
312
|
+
line?: number | null
|
|
313
|
+
severity?: string
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
interface ObsDefenseResult {
|
|
317
|
+
operation?: string
|
|
318
|
+
count?: number
|
|
319
|
+
language?: string
|
|
320
|
+
counts?: Record<string, number>
|
|
321
|
+
events?: ObsDefenseEvent[]
|
|
322
|
+
}
|
|
323
|
+
|
|
246
324
|
/** Opaque slot props: heterogeneous per-slot payloads. */
|
|
247
325
|
type SlotProps = Record<string, unknown>
|
|
248
326
|
|
|
@@ -435,6 +513,43 @@ const ITERATE_CSS = `
|
|
|
435
513
|
.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
514
|
.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
515
|
|
|
516
|
+
/* v3.0: task_mode indicator chip */
|
|
517
|
+
.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; }
|
|
518
|
+
.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); }
|
|
519
|
+
.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); }
|
|
520
|
+
|
|
521
|
+
/* v3.0: Quality gate status */
|
|
522
|
+
.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; }
|
|
523
|
+
.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); }
|
|
524
|
+
.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); }
|
|
525
|
+
.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); }
|
|
526
|
+
|
|
527
|
+
/* v3.0: Experience bank hit highlight */
|
|
528
|
+
.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); }
|
|
529
|
+
.iterate-exp-hit b { font-weight: 600; }
|
|
530
|
+
|
|
531
|
+
/* v3.0: Defense event type badges */
|
|
532
|
+
.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; }
|
|
533
|
+
.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); }
|
|
534
|
+
.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); }
|
|
535
|
+
.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); }
|
|
536
|
+
.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); }
|
|
537
|
+
|
|
538
|
+
/* v3.0: Command buttons */
|
|
539
|
+
.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; }
|
|
540
|
+
.iterate-cmd:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
541
|
+
.iterate-cmd[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
542
|
+
.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); }
|
|
543
|
+
.iterate-cmd:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
544
|
+
|
|
545
|
+
/* v3.0: Dimension score bar */
|
|
546
|
+
.iterate-dim-score { display: flex; align-items: center; gap: 6px; font-size: 11px; }
|
|
547
|
+
.iterate-dim-score-bar { width: 60px; height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
|
|
548
|
+
.iterate-dim-score-fill { height: 100%; border-radius: 3px; transition: width 300ms ease; }
|
|
549
|
+
.iterate-dim-score-fill[data-status="pass"] { background: var(--dsw-alias-state-success-primary); }
|
|
550
|
+
.iterate-dim-score-fill[data-status="warn"] { background: var(--dsw-alias-state-warn-primary); }
|
|
551
|
+
.iterate-dim-score-fill[data-status="fail"] { background: var(--dsw-alias-state-error-primary); }
|
|
552
|
+
|
|
438
553
|
/* Accessibility-switch toggle */
|
|
439
554
|
.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; }
|
|
440
555
|
.iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
|
|
@@ -795,6 +910,19 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
795
910
|
}, `${phase} · ${transcript.active === true ? '运行中' : '已结束'}`)
|
|
796
911
|
: null
|
|
797
912
|
|
|
913
|
+
// v3.0: task_mode indicator (code/iterate)
|
|
914
|
+
// task_mode is read from the session transcript if available
|
|
915
|
+
const taskModeTranscript = latestTranscript(session)
|
|
916
|
+
const taskMode = (taskModeTranscript as unknown as { taskMode?: string })?.taskMode
|
|
917
|
+
const taskModeChip = taskMode
|
|
918
|
+
? React.createElement('span', {
|
|
919
|
+
className: 'iterate-chip-taskmode',
|
|
920
|
+
key: 'taskmode',
|
|
921
|
+
'data-mode': taskMode,
|
|
922
|
+
title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
|
|
923
|
+
}, taskMode === 'code' ? '● code' : '● iterate')
|
|
924
|
+
: null
|
|
925
|
+
|
|
798
926
|
const dimNames = Object.keys(dims)
|
|
799
927
|
const dimBadges = dimNames.slice(0, 6).map((dim) =>
|
|
800
928
|
React.createElement(
|
|
@@ -869,6 +997,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
869
997
|
resumeChip,
|
|
870
998
|
imageChip,
|
|
871
999
|
liveChip,
|
|
1000
|
+
taskModeChip,
|
|
872
1001
|
React.createElement(TrendChart, { points: trend.points }),
|
|
873
1002
|
...dimBadges,
|
|
874
1003
|
)
|
|
@@ -974,6 +1103,8 @@ function TriagePanel(props: SlotProps) {
|
|
|
974
1103
|
const [filter, setFilter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>({ severities: [], dimensions: [], search: '' })
|
|
975
1104
|
const [selected, setSelected] = React.useState<number | null>(null)
|
|
976
1105
|
const [selectAll, setSelectAll] = React.useState(false)
|
|
1106
|
+
// v3.0: Command mode for native action buttons
|
|
1107
|
+
const [cmdCopied, setCmdCopied] = React.useState<string | null>(null)
|
|
977
1108
|
|
|
978
1109
|
/** Persist the verdicts to localStorage whenever they change. Kept OUT of
|
|
979
1110
|
* the state updater (updaters must stay pure — React may double-invoke them
|
|
@@ -1059,6 +1190,64 @@ function TriagePanel(props: SlotProps) {
|
|
|
1059
1190
|
const ignoredCount = ignored.length
|
|
1060
1191
|
const counts = countVerdicts(verdicts)
|
|
1061
1192
|
|
|
1193
|
+
// v3.0: Command handlers for native action buttons
|
|
1194
|
+
const doApproveArchitecturalFix = () => {
|
|
1195
|
+
const cmd = '请调用 `iterate_fix` 批准架构修复(设置 is_architectural: true)'
|
|
1196
|
+
copyText(cmd).then((ok) => {
|
|
1197
|
+
if (ok) {
|
|
1198
|
+
setCmdCopied('approve-arch')
|
|
1199
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1200
|
+
}
|
|
1201
|
+
})
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const doTriggerNewRound = () => {
|
|
1205
|
+
const cmd = '请继续执行下一轮迭代审查'
|
|
1206
|
+
copyText(cmd).then((ok) => {
|
|
1207
|
+
if (ok) {
|
|
1208
|
+
setCmdCopied('new-round')
|
|
1209
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1210
|
+
}
|
|
1211
|
+
})
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const doRollbackToCheckpoint = () => {
|
|
1215
|
+
const cmd = '请调用 `iterate_checkpoint` 回滚到上一个检查点'
|
|
1216
|
+
copyText(cmd).then((ok) => {
|
|
1217
|
+
if (ok) {
|
|
1218
|
+
setCmdCopied('rollback')
|
|
1219
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1220
|
+
}
|
|
1221
|
+
})
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// v3.1+ §8: assign the currently scoped findings to the model for fixing.
|
|
1225
|
+
// Scoping mirrors the batch buttons (visible when selectAll is off, all
|
|
1226
|
+
// otherwise); produces one paste-able iterate_fix instruction carrying each
|
|
1227
|
+
// finding's file/line/dimension/severity/summary.
|
|
1228
|
+
const doAssignFindings = () => {
|
|
1229
|
+
const targetAll = selectAll
|
|
1230
|
+
const scopeIndices = targetAll ? allIndices : indices
|
|
1231
|
+
const scopeFindings = (findings as IterateFinding[])
|
|
1232
|
+
.filter((f, i) => scopeIndices.includes(i))
|
|
1233
|
+
.map((f) => ({
|
|
1234
|
+
file: String(f.file || ''),
|
|
1235
|
+
...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
|
|
1236
|
+
dimension: String(f.dimension || ''),
|
|
1237
|
+
severity: String(f.severity || ''),
|
|
1238
|
+
summary: String(f.summary || ''),
|
|
1239
|
+
...(f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}),
|
|
1240
|
+
}))
|
|
1241
|
+
if (scopeFindings.length === 0) return
|
|
1242
|
+
const cmd = `请调用 \`iterate_fix\` 指派并修复以下 findings:\n\n\`\`\`json\n${JSON.stringify(scopeFindings, null, 2)}\n\`\`\``
|
|
1243
|
+
copyText(cmd).then((ok) => {
|
|
1244
|
+
if (ok) {
|
|
1245
|
+
setCmdCopied('assign')
|
|
1246
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1247
|
+
}
|
|
1248
|
+
})
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1062
1251
|
const doCopyYaml = () => {
|
|
1063
1252
|
const yaml = toKnownIntentionalYaml(ignored)
|
|
1064
1253
|
if (!yaml) return
|
|
@@ -1191,6 +1380,33 @@ function TriagePanel(props: SlotProps) {
|
|
|
1191
1380
|
}, '生成应用指令'),
|
|
1192
1381
|
),
|
|
1193
1382
|
),
|
|
1383
|
+
// v3.0: Command buttons for native actions (§8)
|
|
1384
|
+
React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1385
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
|
|
1386
|
+
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1387
|
+
React.createElement('button', {
|
|
1388
|
+
className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1389
|
+
onClick: doApproveArchitecturalFix,
|
|
1390
|
+
title: '批准架构修复(复制指令文本)',
|
|
1391
|
+
}, cmdCopied === 'approve-arch' ? '已复制' : '批准架构修复'),
|
|
1392
|
+
React.createElement('button', {
|
|
1393
|
+
className: 'iterate-cmd', 'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1394
|
+
onClick: doTriggerNewRound,
|
|
1395
|
+
title: '触发新一轮迭代审查',
|
|
1396
|
+
}, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
|
|
1397
|
+
React.createElement('button', {
|
|
1398
|
+
className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'assign' ? '' : undefined,
|
|
1399
|
+
onClick: doAssignFindings,
|
|
1400
|
+
disabled: (selectAll ? allIndices : indices).length === 0,
|
|
1401
|
+
title: selectAll ? `指派全部 ${allIndices.length} 个 findings 修复` : `指派当前可见 ${indices.length} 个 findings 修复`,
|
|
1402
|
+
}, cmdCopied === 'assign' ? '已复制' : '指派修复'),
|
|
1403
|
+
React.createElement('button', {
|
|
1404
|
+
className: 'iterate-cmd', 'data-danger': '', 'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1405
|
+
onClick: doRollbackToCheckpoint,
|
|
1406
|
+
title: '回滚到上一个检查点',
|
|
1407
|
+
}, cmdCopied === 'rollback' ? '已复制' : '回滚检查点'),
|
|
1408
|
+
),
|
|
1409
|
+
),
|
|
1194
1410
|
payload
|
|
1195
1411
|
? React.createElement('div', { className: 'iterate-payload' }, payload)
|
|
1196
1412
|
: null,
|
|
@@ -1281,7 +1497,13 @@ function ProgressCapsule() {
|
|
|
1281
1497
|
}
|
|
1282
1498
|
}, [])
|
|
1283
1499
|
if (!info) return null
|
|
1284
|
-
return React.createElement('div', {
|
|
1500
|
+
return React.createElement('div', {
|
|
1501
|
+
'data-iterate-root': '',
|
|
1502
|
+
className: 'iterate-capsule',
|
|
1503
|
+
'data-ok': info.ok ? '' : undefined,
|
|
1504
|
+
role: 'status',
|
|
1505
|
+
'aria-live': 'polite',
|
|
1506
|
+
}, info.text)
|
|
1285
1507
|
}
|
|
1286
1508
|
|
|
1287
1509
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
@@ -1452,6 +1674,10 @@ const OBS_TABS = [
|
|
|
1452
1674
|
{ key: 'f5', label: '断点恢复' },
|
|
1453
1675
|
{ key: 'f6', label: '运行控制台' },
|
|
1454
1676
|
{ key: 'f7', label: '决策时间线' },
|
|
1677
|
+
// v3.0: Quality Command Center tabs
|
|
1678
|
+
{ key: 'f8', label: '质量门禁' },
|
|
1679
|
+
{ key: 'f9', label: '经验银行' },
|
|
1680
|
+
{ key: 'f10', label: '防御事件' },
|
|
1455
1681
|
] as const
|
|
1456
1682
|
|
|
1457
1683
|
/** Live subagent activity badges: type → Chinese label + dsw token color accent. */
|
|
@@ -1499,6 +1725,24 @@ function latestTranscript(session: unknown): ObsManifest | null {
|
|
|
1499
1725
|
return raw ? normalizeTranscript(raw) as ObsManifest : null
|
|
1500
1726
|
}
|
|
1501
1727
|
|
|
1728
|
+
/** Find the latest quality-gate snapshot inside a session snapshot. */
|
|
1729
|
+
function latestQualityGate(session: unknown): ObsQualityGate | null {
|
|
1730
|
+
if (!session) return null
|
|
1731
|
+
return scanSessionForQualityGate(session) as ObsQualityGate | null
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/** Find the latest experience-bank result inside a session snapshot. */
|
|
1735
|
+
function latestExperienceBank(session: unknown): ObsExperienceResult | null {
|
|
1736
|
+
if (!session) return null
|
|
1737
|
+
return scanSessionForExperienceBank(session) as ObsExperienceResult | null
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** Find the latest defense-events result inside a session snapshot. */
|
|
1741
|
+
function latestDefenseEvents(session: unknown): ObsDefenseResult | null {
|
|
1742
|
+
if (!session) return null
|
|
1743
|
+
return scanSessionForDefenseEvents(session) as ObsDefenseResult | null
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1502
1746
|
/** Build a human-pasteable `iterate_fix` instruction for one finding. */
|
|
1503
1747
|
function buildObsFixInstruction(f: ObsFinding): string {
|
|
1504
1748
|
const payload = JSON.stringify({
|
|
@@ -1523,6 +1767,10 @@ function buildObsFixInstruction(f: ObsFinding): string {
|
|
|
1523
1767
|
function ObservatoryPanel(props: SlotProps) {
|
|
1524
1768
|
const session = props && props.session ? props.session : null
|
|
1525
1769
|
const manifest = latestTranscript(session)
|
|
1770
|
+
// v3.1+: command-center data scanned straight out of the session stream.
|
|
1771
|
+
const qualityGate = latestQualityGate(session)
|
|
1772
|
+
const experienceBank = latestExperienceBank(session)
|
|
1773
|
+
const defenseEvents = latestDefenseEvents(session)
|
|
1526
1774
|
|
|
1527
1775
|
const [open, setOpen] = React.useState(false)
|
|
1528
1776
|
const [tab, setTab] = React.useState('live')
|
|
@@ -1543,6 +1791,13 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1543
1791
|
const [f3Filter, setF3Filter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>(
|
|
1544
1792
|
{ severities: [], dimensions: [], search: '' },
|
|
1545
1793
|
)
|
|
1794
|
+
// v3.0: Experience bank search state
|
|
1795
|
+
const [expSearch, setExpSearch] = React.useState('')
|
|
1796
|
+
// v3.1+: session-scanned experience entries seed the list; expSearch filters
|
|
1797
|
+
// them client-side (matching pattern/description/dimension/tags). The "搜索"
|
|
1798
|
+
// button still copies a back-end iterate_experience search instruction.
|
|
1799
|
+
// v3.0: Defense events filter
|
|
1800
|
+
const [defenseFilter, setDefenseFilter] = React.useState('')
|
|
1546
1801
|
|
|
1547
1802
|
// Single shared "copied" flash timer (reused across all copy buttons).
|
|
1548
1803
|
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -1571,6 +1826,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1571
1826
|
|
|
1572
1827
|
/** Export the full observatory snapshot as JSON (download, then copy fallback). */
|
|
1573
1828
|
const exportObservatory = () => {
|
|
1829
|
+
if (!manifest) return
|
|
1574
1830
|
const json = serializeObservatoryExport(manifest, manifest.live || [])
|
|
1575
1831
|
const filename = `iterate-observatory-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
|
1576
1832
|
const flash = () => {
|
|
@@ -1722,8 +1978,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1722
1978
|
if (findings.length === 0) {
|
|
1723
1979
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
1724
1980
|
}
|
|
1725
|
-
const opts = buildFilterOptions(findings)
|
|
1726
|
-
const { filtered } = filterFindingsWithIndices(findings, f3Filter)
|
|
1981
|
+
const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
|
|
1982
|
+
const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
|
|
1727
1983
|
const filterActive = f3Filter.severities.length > 0 || f3Filter.dimensions.length > 0 || f3Filter.search.trim() !== ''
|
|
1728
1984
|
const toggleSeverity = (sev: string) => {
|
|
1729
1985
|
setF3Filter((prev) => ({
|
|
@@ -1778,31 +2034,32 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1778
2034
|
)
|
|
1779
2035
|
const findingBlocks = filtered.map((f, i) => {
|
|
1780
2036
|
const k = `f3-${i}`
|
|
1781
|
-
const
|
|
2037
|
+
const finding = f as unknown as ObsFinding
|
|
2038
|
+
const loc = `${String(finding.file || '?')}${typeof finding.line === 'number' && finding.line > 0 ? `:${finding.line}` : ''}`
|
|
1782
2039
|
const entry = {
|
|
1783
|
-
file: String(
|
|
1784
|
-
...(typeof
|
|
1785
|
-
dimension: String(
|
|
1786
|
-
reason: String(
|
|
2040
|
+
file: String(finding.file || ''),
|
|
2041
|
+
...(typeof finding.line === 'number' && finding.line > 0 ? { line: finding.line } : {}),
|
|
2042
|
+
dimension: String(finding.dimension || ''),
|
|
2043
|
+
reason: String(finding.summary || ''),
|
|
1787
2044
|
}
|
|
1788
2045
|
const knownInstruction = buildApplyInstruction([entry])
|
|
1789
|
-
const fixInstruction = buildObsFixInstruction(
|
|
2046
|
+
const fixInstruction = buildObsFixInstruction(finding)
|
|
1790
2047
|
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
1791
2048
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1792
|
-
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(
|
|
1793
|
-
React.createElement('span', {}, severityLabel(
|
|
2049
|
+
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(finding.severity) } }),
|
|
2050
|
+
React.createElement('span', {}, severityLabel(finding.severity)),
|
|
1794
2051
|
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
1795
|
-
React.createElement('span', {}, String(
|
|
1796
|
-
|
|
2052
|
+
React.createElement('span', {}, String(finding.dimension || '')),
|
|
2053
|
+
finding.acknowledged === true
|
|
1797
2054
|
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
1798
2055
|
: null,
|
|
1799
2056
|
),
|
|
1800
2057
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1801
|
-
React.createElement('div', { className: 'iterate-obs-msg' }, String(
|
|
2058
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(finding.summary || '')),
|
|
1802
2059
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
1803
2060
|
React.createElement('button', {
|
|
1804
2061
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
1805
|
-
onClick: () => copyInstruction(`${k}-fix`, String(
|
|
2062
|
+
onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
|
|
1806
2063
|
title: '复制该 finding 的建议修复文本',
|
|
1807
2064
|
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
1808
2065
|
React.createElement('button', {
|
|
@@ -2033,6 +2290,293 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2033
2290
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
2034
2291
|
}
|
|
2035
2292
|
|
|
2293
|
+
// ── F8: Quality Gate ──────────────────────────────────────────────────────
|
|
2294
|
+
// v3.1+ renders the latest iterate_quality_gate snapshot found in the
|
|
2295
|
+
// session (dimension convergence, verification pass rate, overall PASS/FAIL);
|
|
2296
|
+
// falls back to copy-able query instructions when no snapshot exists yet.
|
|
2297
|
+
const renderQualityGate = () => {
|
|
2298
|
+
const gateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
|
|
2299
|
+
const gate = qualityGate
|
|
2300
|
+
const dims = gate && gate.dimensions ? gate.dimensions : []
|
|
2301
|
+
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
|
|
2302
|
+
const status = gate && gate.overallStatus ? String(gate.overallStatus) : 'pending'
|
|
2303
|
+
const statusLabel = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'PENDING'
|
|
2304
|
+
const num = (v: number | undefined) => Number(v != null ? v : 0)
|
|
2305
|
+
|
|
2306
|
+
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2307
|
+
React.createElement('b', {}, '质量门禁'),
|
|
2308
|
+
React.createElement('button', {
|
|
2309
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2310
|
+
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2311
|
+
title: '复制 iterate_quality_gate 查询指令',
|
|
2312
|
+
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2313
|
+
)
|
|
2314
|
+
|
|
2315
|
+
if (!hasGate) {
|
|
2316
|
+
return React.createElement('div', {},
|
|
2317
|
+
headerBar,
|
|
2318
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2319
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2320
|
+
React.createElement('span', {}, '质量门禁视图'),
|
|
2321
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2322
|
+
),
|
|
2323
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2324
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
|
|
2325
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2326
|
+
React.createElement('button', {
|
|
2327
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2328
|
+
onClick: () => copyInstruction('qgate-instr', gateInstruction),
|
|
2329
|
+
title: '复制查询指令',
|
|
2330
|
+
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
2331
|
+
),
|
|
2332
|
+
),
|
|
2333
|
+
),
|
|
2334
|
+
)
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
return React.createElement('div', {},
|
|
2338
|
+
headerBar,
|
|
2339
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2340
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2341
|
+
React.createElement('span', { className: 'iterate-gate', 'data-status': status }, statusLabel),
|
|
2342
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2343
|
+
`整体评分 ${num(gate && gate.overallScore)} · 验证 ${num(gate && gate.passedChecks)}/${num(gate && gate.totalChecks)} · 通过率 ${num(gate && gate.verificationPassRate)}%`,
|
|
2344
|
+
),
|
|
2345
|
+
),
|
|
2346
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2347
|
+
dims.length > 0
|
|
2348
|
+
? React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6 } },
|
|
2349
|
+
...dims.map((d, i) =>
|
|
2350
|
+
React.createElement('span', { key: `gdim-${i}`, className: 'iterate-obs-chip' },
|
|
2351
|
+
`${String(d.dimension || '?')} · 收敛 ${num(d.convergenceRate)}% · 评分 ${num(d.score)}`,
|
|
2352
|
+
),
|
|
2353
|
+
),
|
|
2354
|
+
)
|
|
2355
|
+
: null,
|
|
2356
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap' } },
|
|
2357
|
+
...dims.map((d, i) => {
|
|
2358
|
+
const fill = Math.min(100, Math.max(0, num(d.score)))
|
|
2359
|
+
return React.createElement('div', { key: `gbar-${i}`, className: 'iterate-dim-score' },
|
|
2360
|
+
React.createElement('span', {}, String(d.dimension || '?')),
|
|
2361
|
+
React.createElement('div', { className: 'iterate-dim-score-bar' },
|
|
2362
|
+
React.createElement('div', {
|
|
2363
|
+
className: 'iterate-dim-score-fill',
|
|
2364
|
+
'data-status': String(d.status || 'warn'),
|
|
2365
|
+
style: { width: `${fill}%` },
|
|
2366
|
+
}),
|
|
2367
|
+
),
|
|
2368
|
+
React.createElement('span', {}, `评分 ${num(d.score)} · 发现 ${num(d.findingsCount)} · 已修复 ${num(d.fixedCount)}`),
|
|
2369
|
+
)
|
|
2370
|
+
}),
|
|
2371
|
+
),
|
|
2372
|
+
gate && gate.failReason
|
|
2373
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } }, `失败原因:${String(gate.failReason)}`)
|
|
2374
|
+
: null,
|
|
2375
|
+
),
|
|
2376
|
+
),
|
|
2377
|
+
)
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// ── F9: Experience Bank ────────────────────────────────────────────────────
|
|
2381
|
+
// v3.1+ renders experience entries scanned from the session plus a
|
|
2382
|
+
// client-side keyword filter; the copy buttons still reach the back end
|
|
2383
|
+
// (list all / search / adopt one entry).
|
|
2384
|
+
const renderExperienceBank = () => {
|
|
2385
|
+
const expInstruction = expSearch
|
|
2386
|
+
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n${JSON.stringify({ operation: 'search', query: expSearch }, null, 2)}\n\`\`\``
|
|
2387
|
+
: '请调用 `iterate_experience` 列出所有经验'
|
|
2388
|
+
|
|
2389
|
+
const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
|
|
2390
|
+
const query = expSearch.trim().toLowerCase()
|
|
2391
|
+
const visible = query
|
|
2392
|
+
? expEntries.filter((e) =>
|
|
2393
|
+
[e.pattern, e.description, e.id, e.dimension, e.severity, ...(e.tags || [])]
|
|
2394
|
+
.filter(Boolean)
|
|
2395
|
+
.some((t) => String(t).toLowerCase().includes(query)),
|
|
2396
|
+
)
|
|
2397
|
+
: expEntries
|
|
2398
|
+
|
|
2399
|
+
const resultRows = visible.map((e, i) => {
|
|
2400
|
+
const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n${JSON.stringify({ operation: 'get', id: e.id }, null, 2)}\n\`\`\``
|
|
2401
|
+
return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-block' },
|
|
2402
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2403
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
|
|
2404
|
+
React.createElement('span', {}, String(e.pattern || '?')),
|
|
2405
|
+
React.createElement('span', { className: 'iterate-exp-hit' }, `命中 ${e.hitCount ?? 0} 次`),
|
|
2406
|
+
),
|
|
2407
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2408
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
2409
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2410
|
+
e.verifiedFix
|
|
2411
|
+
? React.createElement('button', {
|
|
2412
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `exp-adopt-${i}` ? '' : undefined,
|
|
2413
|
+
onClick: () => copyInstruction(`exp-adopt-${i}`, adoptInstruction),
|
|
2414
|
+
title: '复制采纳指令(查询该经验并应用到当前上下文)',
|
|
2415
|
+
}, copiedKey === `exp-adopt-${i}` ? '已复制' : '采纳')
|
|
2416
|
+
: null,
|
|
2417
|
+
),
|
|
2418
|
+
),
|
|
2419
|
+
)
|
|
2420
|
+
})
|
|
2421
|
+
|
|
2422
|
+
return React.createElement('div', {},
|
|
2423
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2424
|
+
React.createElement('b', {}, '经验银行'),
|
|
2425
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${expEntries.length} 条`),
|
|
2426
|
+
React.createElement('button', {
|
|
2427
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2428
|
+
onClick: () => copyInstruction('exp-list', '请调用 `iterate_experience` 列出所有经验'),
|
|
2429
|
+
title: '复制列出经验指令',
|
|
2430
|
+
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
2431
|
+
),
|
|
2432
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2433
|
+
React.createElement('input', {
|
|
2434
|
+
className: 'iterate-filter-search',
|
|
2435
|
+
type: 'search',
|
|
2436
|
+
placeholder: '搜索经验模式…',
|
|
2437
|
+
'aria-label': '搜索经验',
|
|
2438
|
+
value: expSearch,
|
|
2439
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
|
|
2440
|
+
}),
|
|
2441
|
+
React.createElement('button', {
|
|
2442
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
|
|
2443
|
+
disabled: !expSearch,
|
|
2444
|
+
onClick: () => copyInstruction('exp-search', expInstruction),
|
|
2445
|
+
title: '复制搜索指令',
|
|
2446
|
+
}, copiedKey === 'exp-search' ? '已复制' : '搜索'),
|
|
2447
|
+
),
|
|
2448
|
+
resultRows.length > 0
|
|
2449
|
+
? React.createElement('div', {}, ...resultRows)
|
|
2450
|
+
: expEntries.length > 0
|
|
2451
|
+
? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
|
|
2452
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, '本次会话暂无经验记录,可点击「列出经验」查询历史经验'),
|
|
2453
|
+
)
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// ── F10: Defense Events ────────────────────────────────────────────────────
|
|
2457
|
+
// v3.1+ renders defense events + type counts scanned from the session
|
|
2458
|
+
// (precondition failures, rollbacks, invariant violations, assumption
|
|
2459
|
+
// falsifications); copy buttons reach the back end for the full history.
|
|
2460
|
+
const renderDefenseEvents = () => {
|
|
2461
|
+
const defenseInstruction = defenseFilter
|
|
2462
|
+
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n${JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2)}\n\`\`\``
|
|
2463
|
+
: '请调用 `iterate_defense_events` 列出所有防御事件'
|
|
2464
|
+
|
|
2465
|
+
const typeOptions = [
|
|
2466
|
+
{ value: '', label: '全部类型' },
|
|
2467
|
+
{ value: 'precondition_failed', label: '前置校验失败' },
|
|
2468
|
+
{ value: 'rollback', label: '回滚' },
|
|
2469
|
+
{ value: 'invariant_violated', label: '不变量违反' },
|
|
2470
|
+
{ value: 'assumption_falsified', label: '假设被证伪' },
|
|
2471
|
+
]
|
|
2472
|
+
const TYPE_LABEL: Record<string, string> = {
|
|
2473
|
+
precondition_failed: '前置校验失败',
|
|
2474
|
+
rollback: '回滚',
|
|
2475
|
+
invariant_violated: '不变量违反',
|
|
2476
|
+
assumption_falsified: '假设被证伪',
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
const counts = (defenseEvents && defenseEvents.counts) || {}
|
|
2480
|
+
const allEvents = defenseEvents && defenseEvents.events ? defenseEvents.events : []
|
|
2481
|
+
const totalCount = typeOptions.slice(1).reduce((acc, o) => acc + (counts[o.value] || 0), 0)
|
|
2482
|
+
const visibleEvents = defenseFilter
|
|
2483
|
+
? allEvents.filter((e) => e.type === defenseFilter)
|
|
2484
|
+
: allEvents
|
|
2485
|
+
|
|
2486
|
+
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2487
|
+
React.createElement('b', {}, '防御事件'),
|
|
2488
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${allEvents.length} 条 / 累计 ${totalCount}`),
|
|
2489
|
+
React.createElement('button', {
|
|
2490
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
2491
|
+
onClick: () => copyInstruction('defense-list', '请调用 `iterate_defense_events` 列出所有防御事件'),
|
|
2492
|
+
title: '复制列出防御事件指令',
|
|
2493
|
+
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
2494
|
+
React.createElement('button', {
|
|
2495
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
|
|
2496
|
+
onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
|
|
2497
|
+
title: '复制统计指令',
|
|
2498
|
+
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
2499
|
+
)
|
|
2500
|
+
|
|
2501
|
+
if (allEvents.length === 0 && totalCount === 0) {
|
|
2502
|
+
return React.createElement('div', {},
|
|
2503
|
+
headerBar,
|
|
2504
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2505
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2506
|
+
React.createElement('span', {}, '防御事件流'),
|
|
2507
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2508
|
+
),
|
|
2509
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2510
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
|
|
2511
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2512
|
+
React.createElement('button', {
|
|
2513
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
2514
|
+
onClick: () => copyInstruction('defense-instr', defenseInstruction),
|
|
2515
|
+
title: '复制查询指令',
|
|
2516
|
+
}, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
|
|
2517
|
+
),
|
|
2518
|
+
),
|
|
2519
|
+
),
|
|
2520
|
+
)
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2524
|
+
React.createElement('select', {
|
|
2525
|
+
className: 'iterate-filter-select',
|
|
2526
|
+
value: defenseFilter,
|
|
2527
|
+
'aria-label': '按事件类型筛选',
|
|
2528
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
|
|
2529
|
+
},
|
|
2530
|
+
...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
|
|
2531
|
+
),
|
|
2532
|
+
defenseFilter
|
|
2533
|
+
? React.createElement('button', {
|
|
2534
|
+
className: 'iterate-filter-clear',
|
|
2535
|
+
onClick: () => setDefenseFilter(''),
|
|
2536
|
+
title: '清除类型筛选',
|
|
2537
|
+
}, '清除')
|
|
2538
|
+
: null,
|
|
2539
|
+
)
|
|
2540
|
+
|
|
2541
|
+
let eventRows: React.ReactNode[] = []
|
|
2542
|
+
if (visibleEvents.length > 0) {
|
|
2543
|
+
eventRows = visibleEvents.map((e, i) => {
|
|
2544
|
+
const type = String(e.type || 'unknown')
|
|
2545
|
+
const loc = `${String(e.file || '?')}${typeof e.line === 'number' && e.line > 0 ? `:${e.line}` : ''}`
|
|
2546
|
+
return React.createElement('div', { key: `def-${i}`, className: 'iterate-obs-block' },
|
|
2547
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2548
|
+
React.createElement('span', { className: 'iterate-defense-badge', 'data-type': type }, TYPE_LABEL[type] || type),
|
|
2549
|
+
typeof e.round === 'number'
|
|
2550
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${e.round}`)
|
|
2551
|
+
: null,
|
|
2552
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, loc),
|
|
2553
|
+
),
|
|
2554
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2555
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
2556
|
+
e.defense
|
|
2557
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } }, `防线:${String(e.defense)}`)
|
|
2558
|
+
: null,
|
|
2559
|
+
),
|
|
2560
|
+
)
|
|
2561
|
+
})
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
return React.createElement('div', {},
|
|
2565
|
+
headerBar,
|
|
2566
|
+
filterBar,
|
|
2567
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6, marginBottom: 8 } },
|
|
2568
|
+
...typeOptions.slice(1).map((opt) =>
|
|
2569
|
+
React.createElement('span', { key: `cnt-${opt.value}`, className: 'iterate-defense-badge', 'data-type': opt.value },
|
|
2570
|
+
`${opt.label} ${counts[opt.value] || 0}`,
|
|
2571
|
+
),
|
|
2572
|
+
),
|
|
2573
|
+
),
|
|
2574
|
+
eventRows.length > 0
|
|
2575
|
+
? React.createElement('div', {}, ...eventRows)
|
|
2576
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配该类型的防御事件'),
|
|
2577
|
+
)
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2036
2580
|
// ── Live: real-time secondary-subagent activity stream (newest first) ─────
|
|
2037
2581
|
const renderLive = () => {
|
|
2038
2582
|
const liveEntries = manifest.live || []
|
|
@@ -2060,7 +2604,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2060
2604
|
const meta = OBS_LIVE_META[type] || { label: String(e.type || '?'), color: OBS_LIVE_FALLBACK_COLOR }
|
|
2061
2605
|
const tool = String(e.tool || '')
|
|
2062
2606
|
const target = String(e.target || '')
|
|
2063
|
-
const time = formatObsLiveTime(e.ts)
|
|
2607
|
+
const time = formatObsLiveTime(String(e.ts || ''))
|
|
2064
2608
|
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
2065
2609
|
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: meta.color } }, meta.label),
|
|
2066
2610
|
tool
|
|
@@ -2109,6 +2653,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2109
2653
|
case 'f5': return renderCheckpoint()
|
|
2110
2654
|
case 'f6': return renderConsole()
|
|
2111
2655
|
case 'f7': return renderTimeline()
|
|
2656
|
+
// v3.0: Quality Command Center tabs
|
|
2657
|
+
case 'f8': return renderQualityGate()
|
|
2658
|
+
case 'f9': return renderExperienceBank()
|
|
2659
|
+
case 'f10': return renderDefenseEvents()
|
|
2112
2660
|
default: return null
|
|
2113
2661
|
}
|
|
2114
2662
|
}
|