iterate-plugin 3.5.6 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -20
- package/README.zh-CN.md +53 -17
- package/dist/approval-gate.js +143 -23
- package/dist/atomic-fs.js +148 -17
- package/dist/config-loader.js +509 -34
- package/dist/config-write.js +83 -22
- package/dist/evidence.js +141 -87
- package/dist/file-lock.js +165 -0
- package/dist/index.js +54 -28
- package/dist/jobs.js +56 -11
- package/dist/live.js +52 -13
- package/dist/meta-review.js +132 -28
- package/dist/method-scope.js +31 -8
- package/dist/review-scope.js +32 -9
- package/dist/review.js +96 -17
- package/dist/session-hooks.js +55 -13
- package/dist/skill-prompt.js +417 -67
- package/dist/tools/checkpoint.js +85 -19
- package/dist/tools/config.js +44 -2
- package/dist/tools/context.js +22 -5
- package/dist/tools/decision-log.js +122 -27
- package/dist/tools/defense-events.js +90 -31
- package/dist/tools/defense-store.js +28 -3
- package/dist/tools/experience-bank.js +108 -43
- package/dist/tools/experience-store.js +54 -4
- package/dist/tools/fix.js +347 -119
- package/dist/tools/history.js +68 -14
- package/dist/tools/present.js +80 -0
- package/dist/tools/prune.js +156 -53
- package/dist/tools/quality-gate.js +52 -2
- package/dist/tools/quality-store.js +17 -1
- package/dist/tools/review.js +158 -7
- package/dist/tools/transcript.js +173 -36
- package/dist/tools/triage.js +67 -12
- package/dist/tools/validate.js +115 -24
- package/dist/transcript.js +267 -4
- package/lib/client.js +945 -181
- package/lib/parse.js +760 -57
- package/package.json +15 -12
- package/src/approval-gate.ts +160 -23
- package/src/atomic-fs.ts +152 -17
- package/src/client/index.ts +600 -129
- package/src/config-loader.ts +542 -35
- package/src/config-write.ts +108 -26
- package/src/evidence.ts +194 -92
- package/src/file-lock.ts +184 -0
- package/src/index.ts +53 -29
- package/src/jobs.ts +63 -12
- package/src/live.ts +48 -11
- package/src/meta-review.ts +151 -34
- package/src/method-scope.ts +31 -8
- package/src/review-scope.ts +33 -10
- package/src/review.ts +96 -17
- package/src/session-hooks.ts +54 -12
- package/src/skill-prompt.ts +417 -67
- package/src/tools/checkpoint.ts +81 -19
- package/src/tools/config.ts +44 -2
- package/src/tools/context.ts +25 -5
- package/src/tools/decision-log.ts +127 -22
- package/src/tools/defense-events.ts +87 -31
- package/src/tools/defense-store.ts +30 -3
- package/src/tools/experience-bank.ts +112 -44
- package/src/tools/experience-store.ts +54 -5
- package/src/tools/fix.ts +258 -20
- package/src/tools/history.ts +74 -15
- package/src/tools/present.ts +89 -0
- package/src/tools/prune.ts +160 -51
- package/src/tools/quality-gate.ts +57 -2
- package/src/tools/quality-store.ts +18 -1
- package/src/tools/review.ts +155 -6
- package/src/tools/transcript.ts +178 -36
- package/src/tools/triage.ts +70 -13
- package/src/tools/validate.ts +138 -32
- package/src/transcript.ts +279 -4
- package/src/types.ts +29 -1
package/src/client/index.ts
CHANGED
|
@@ -82,6 +82,28 @@ import {
|
|
|
82
82
|
SEVERITY_LABEL,
|
|
83
83
|
SEVERITY_COLOR,
|
|
84
84
|
stoppedReasonLabel,
|
|
85
|
+
// ── gap fixes ──
|
|
86
|
+
START_INSTRUCTIONS, // #1: registered instruction set (test-locked)
|
|
87
|
+
dashboardRunState, // #5: running vs onboarding empty state
|
|
88
|
+
buildRoundComparison, // #13: cross-round comparison table
|
|
89
|
+
buildDiskSnapshotInstruction, // #4: pull `.iterate/` artifacts into the stream
|
|
90
|
+
diskEmptyStateText, // #4: "not pulled" vs "pulled and empty"
|
|
91
|
+
toolCalledInSession, // #4: which read-side tools ran this session
|
|
92
|
+
buildFixInstruction, // #10: single-finding fix payload w/ content + round
|
|
93
|
+
buildAssignFixesInstruction, // #10: "one call per finding" batch wording
|
|
94
|
+
buildArchitecturalFixInstruction, // #10: force:true still needs 4 required fields
|
|
95
|
+
buildCheckpointResumeInstruction, // #10: resume takes {operation} only
|
|
96
|
+
buildCheckpointClearInstruction, // #10: clear takes {operation} only
|
|
97
|
+
buildRollbackInstruction, // #10: shared rollback payload
|
|
98
|
+
buildQualityGateQueryInstruction, // #10: JSON-carrying query instruction
|
|
99
|
+
buildQualityGateClearInstruction, // #10: JSON-carrying clear instruction
|
|
100
|
+
buildTriageReadbackInstruction, // #7: close the write-back loop
|
|
101
|
+
buildExperienceListInstruction, // #3/#10: JSON list payload for F9
|
|
102
|
+
buildDefenseEventsListInstruction, // #3/#10: JSON list payload for F10
|
|
103
|
+
buildConfigFieldInstruction, // #8: field → iterate_config write instruction
|
|
104
|
+
configFieldByKey, // #8: field lookup for the settings picker
|
|
105
|
+
CONFIG_EDIT_FIELDS, // #8: field list incl. validation.commands
|
|
106
|
+
DISK_SNAPSHOT_SOURCES, // #4/#3: read-side tools listed by the pull instruction
|
|
85
107
|
} from '../../lib/parse.js'
|
|
86
108
|
|
|
87
109
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
@@ -247,6 +269,17 @@ interface ObsManifest {
|
|
|
247
269
|
approval?: ObsApproval
|
|
248
270
|
/** Why the run ended: converged / max_rounds_reached / aborted_by_validation. */
|
|
249
271
|
stoppedReason?: string | null
|
|
272
|
+
/** Gap #6: per-round validation rows persisted by `iterate_transcript capture`. */
|
|
273
|
+
validations?: ObsValidation[]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** One `iterate_validate` outcome for a round (gap #6 contract). */
|
|
277
|
+
interface ObsValidation {
|
|
278
|
+
round?: number
|
|
279
|
+
command?: string
|
|
280
|
+
exitCode?: number | null
|
|
281
|
+
allowed?: boolean
|
|
282
|
+
rejectReason?: string
|
|
250
283
|
}
|
|
251
284
|
|
|
252
285
|
// ─── Quality command center data types (v3.1+; mirrors src/types.ts) ────────
|
|
@@ -426,6 +459,7 @@ const ITERATE_CSS = `
|
|
|
426
459
|
}
|
|
427
460
|
.iterate-btn[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
428
461
|
.iterate-btn[data-copied] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
|
|
462
|
+
.iterate-btn[data-failed], .iterate-cmd[data-failed] { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
|
|
429
463
|
.iterate-payload { width: 100%; margin-top: 8px; padding: 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 11px; white-space: pre-wrap; }
|
|
430
464
|
|
|
431
465
|
.iterate-stats { margin: 10px 0; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); padding: 12px 14px; }
|
|
@@ -610,6 +644,16 @@ const ITERATE_CSS = `
|
|
|
610
644
|
.iterate-obs-block-head[data-click] { cursor: pointer; }
|
|
611
645
|
.iterate-obs-block-body { padding: 8px 10px; }
|
|
612
646
|
.iterate-obs-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
|
647
|
+
/* Gap #6: validation / gate status colors (pass=green, fail=red). */
|
|
648
|
+
.iterate-obs-chip[data-status="pass"], .iterate-obs-badge[data-status="pass"] { 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); }
|
|
649
|
+
.iterate-obs-chip[data-status="fail"], .iterate-obs-badge[data-status="fail"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
650
|
+
.iterate-obs-row[data-status="fail"] { color: var(--dsw-alias-state-error-primary); }
|
|
651
|
+
.iterate-obs-row[data-highlight] { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent); border-radius: 6px; padding-left: 6px; padding-right: 6px; border-bottom-color: transparent; font-weight: 600; }
|
|
652
|
+
/* Gap #13: cross-round comparison table. */
|
|
653
|
+
.iterate-obs-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
654
|
+
.iterate-obs-table th, .iterate-obs-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); white-space: nowrap; }
|
|
655
|
+
.iterate-obs-table th { color: var(--dsw-alias-label-secondary); font-weight: 600; }
|
|
656
|
+
.iterate-obs-table td { color: var(--dsw-alias-label-primary); }
|
|
613
657
|
.iterate-obs-msg { padding: 2px 0; color: var(--dsw-alias-label-secondary); line-height: 1.5; font-size: 11px; word-break: break-word; }
|
|
614
658
|
.iterate-obs-file { font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 10.5px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
|
|
615
659
|
.iterate-obs-bar { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); flex-wrap: wrap; }
|
|
@@ -873,15 +917,37 @@ function TrendChart({ points }: { points: Array<{ round: number; count: number }
|
|
|
873
917
|
}
|
|
874
918
|
|
|
875
919
|
/** Start-iteration launcher shown on the empty dashboard: copies a runnable
|
|
876
|
-
*
|
|
877
|
-
* follows the panel's copy-to-command pattern).
|
|
920
|
+
* instruction (the browser cannot invoke harness tools directly, so every
|
|
921
|
+
* action follows the panel's copy-to-command pattern).
|
|
922
|
+
*
|
|
923
|
+
* Gap #1: the plugin registers NO dsh command (`src/index.ts` only registers
|
|
924
|
+
* 17 tools + a system prompt), so `/iterate` never existed — the copied text
|
|
925
|
+
* is now a natural-language instruction from `START_INSTRUCTIONS`, which the
|
|
926
|
+
* model resolves through the `workflow` tool exactly as the skill prompt
|
|
927
|
+
* teaches. Label/tooltip say "指令", never "命令". */
|
|
878
928
|
function StartIterationButton() {
|
|
879
929
|
const [copied, setCopied] = React.useState<string | null>(null)
|
|
930
|
+
const [failed, setFailed] = React.useState<string | null>(null)
|
|
931
|
+
const timers = React.useRef<number[]>([])
|
|
932
|
+
React.useEffect(
|
|
933
|
+
() => () => {
|
|
934
|
+
for (const t of timers.current) clearTimeout(t)
|
|
935
|
+
timers.current = []
|
|
936
|
+
},
|
|
937
|
+
[],
|
|
938
|
+
)
|
|
880
939
|
const copy = (key: string, text: string) => {
|
|
881
940
|
copyText(text).then((ok) => {
|
|
882
941
|
if (ok) {
|
|
942
|
+
setFailed(null)
|
|
883
943
|
setCopied(key)
|
|
884
|
-
setTimeout(() => setCopied((cur) => (cur === key ? null : cur)), 1600)
|
|
944
|
+
const t = window.setTimeout(() => setCopied((cur) => (cur === key ? null : cur)), 1600)
|
|
945
|
+
timers.current.push(t)
|
|
946
|
+
} else {
|
|
947
|
+
// Copy failure must be visible, not silent — otherwise the button
|
|
948
|
+
// looks like a no-op and the user never learns the instruction text.
|
|
949
|
+
setCopied(null)
|
|
950
|
+
setFailed(key)
|
|
885
951
|
}
|
|
886
952
|
})
|
|
887
953
|
}
|
|
@@ -891,14 +957,15 @@ function StartIterationButton() {
|
|
|
891
957
|
className: 'iterate-cmd',
|
|
892
958
|
'data-primary': primary ? '' : undefined,
|
|
893
959
|
'data-copied': copied === key ? '' : undefined,
|
|
960
|
+
'data-failed': failed === key ? '' : undefined,
|
|
894
961
|
onClick: () => copy(key, command),
|
|
895
962
|
title,
|
|
896
|
-
}, copied === key ? '已复制' : label)
|
|
963
|
+
}, copied === key ? '已复制' : failed === key ? '复制失败' : label)
|
|
897
964
|
return React.createElement(
|
|
898
965
|
'span',
|
|
899
966
|
{ className: 'iterate-dashboard-launch' },
|
|
900
|
-
btn('start-full', '完整迭代',
|
|
901
|
-
btn('start-review', '仅评审',
|
|
967
|
+
btn('start-full', '完整迭代', START_INSTRUCTIONS.full, true, '复制启动指令:完整「审查 → 修复 → 验证 → 复盘」闭环(粘贴给模型即可)'),
|
|
968
|
+
btn('start-review', '仅评审', START_INSTRUCTIONS.reviewOnly, false, '复制启动指令:只审查不修改(dry-run)(粘贴给模型即可)'),
|
|
902
969
|
)
|
|
903
970
|
}
|
|
904
971
|
|
|
@@ -926,13 +993,45 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
926
993
|
}, [report && hashReport(report) + ':' + getCurrentRound(report)])
|
|
927
994
|
|
|
928
995
|
if (!report) {
|
|
996
|
+
// Gap #5: split the old single empty state in two. A run that is already
|
|
997
|
+
// in flight has no aggregated report yet (round 1 is still in
|
|
998
|
+
// review → fix → validate), but showing "运行一次评审后…" + a start button
|
|
999
|
+
// mid-run tells the user nothing is happening — and invites a second
|
|
1000
|
+
// concurrent start. The transcript alone carries round + phase.
|
|
1001
|
+
const runState = dashboardRunState(latestTranscript(session))
|
|
1002
|
+
if (runState.state === 'running') {
|
|
1003
|
+
return React.createElement(
|
|
1004
|
+
'div',
|
|
1005
|
+
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'running', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1006
|
+
React.createElement('span', { className: 'iterate-round-badge', 'data-live': '' }, '运行中'),
|
|
1007
|
+
React.createElement(
|
|
1008
|
+
'span',
|
|
1009
|
+
{ className: 'iterate-empty-hint' },
|
|
1010
|
+
`第 ${String(runState.round)} 轮进行中${runState.phase ? ` · 阶段「${runState.phase}」` : ''}——首轮汇总报告落地后,这里会显示收敛进度与发现统计。`,
|
|
1011
|
+
),
|
|
1012
|
+
// No start button while a run is active.
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
1015
|
+
if (runState.state === 'done') {
|
|
1016
|
+
return React.createElement(
|
|
1017
|
+
'div',
|
|
1018
|
+
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'done', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1019
|
+
React.createElement('span', { className: 'iterate-round-badge' }, '已结束'),
|
|
1020
|
+
React.createElement(
|
|
1021
|
+
'span',
|
|
1022
|
+
{ className: 'iterate-empty-hint' },
|
|
1023
|
+
`上次迭代在第 ${String(runState.round)} 轮后停止${runState.stoppedReason ? `(${stoppedReasonLabel(runState.stoppedReason)})` : ''},但没有可展示的汇总报告。`,
|
|
1024
|
+
),
|
|
1025
|
+
React.createElement(StartIterationButton, null),
|
|
1026
|
+
)
|
|
1027
|
+
}
|
|
929
1028
|
// Empty/onboarding state: first-time users otherwise see nothing and have
|
|
930
|
-
// no idea the plugin exists or how to start. Provide a one-click
|
|
931
|
-
//
|
|
932
|
-
//
|
|
1029
|
+
// no idea the plugin exists or how to start. Provide a one-click copy of
|
|
1030
|
+
// the startup instruction (the browser cannot call harness tools
|
|
1031
|
+
// directly, and dsh exposes no command API to plugins).
|
|
933
1032
|
return React.createElement(
|
|
934
1033
|
'div',
|
|
935
|
-
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1034
|
+
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'empty', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
936
1035
|
React.createElement('span', { className: 'iterate-round-badge' }, 'iterate'),
|
|
937
1036
|
React.createElement('span', { className: 'iterate-empty-hint' }, '运行一次评审后,这里会显示收敛进度与发现统计。'),
|
|
938
1037
|
React.createElement(StartIterationButton, null),
|
|
@@ -1176,6 +1275,31 @@ function TriagePanel(props: SlotProps) {
|
|
|
1176
1275
|
const [selectAll, setSelectAll] = React.useState(false)
|
|
1177
1276
|
// v3.0: Command mode for native action buttons
|
|
1178
1277
|
const [cmdCopied, setCmdCopied] = React.useState<string | null>(null)
|
|
1278
|
+
const [cmdFailed, setCmdFailed] = React.useState<string | null>(null)
|
|
1279
|
+
// Audit: pending copy-reset timers must not outlive the panel (a unmounted
|
|
1280
|
+
// setState is a React warning / leaked timer).
|
|
1281
|
+
const cmdTimers = React.useRef<number[]>([])
|
|
1282
|
+
React.useEffect(() => () => {
|
|
1283
|
+
for (const t of cmdTimers.current) clearTimeout(t)
|
|
1284
|
+
cmdTimers.current = []
|
|
1285
|
+
}, [])
|
|
1286
|
+
|
|
1287
|
+
/** Copy + flip `cmdCopied` for 1.6s, or surface `cmdFailed` on clipboard
|
|
1288
|
+
* denial so the button never looks like a dead control. */
|
|
1289
|
+
const copyWithFeedback = (text: string, key: string) => {
|
|
1290
|
+
copyText(text).then((ok) => {
|
|
1291
|
+
if (ok) {
|
|
1292
|
+
setCmdFailed(null)
|
|
1293
|
+
setCmdCopied(key)
|
|
1294
|
+
cmdTimers.current.push(window.setTimeout(() => setCmdCopied((cur) => (cur === key ? null : cur)), 1600))
|
|
1295
|
+
} else {
|
|
1296
|
+
setCmdCopied(null)
|
|
1297
|
+
setCmdFailed(key)
|
|
1298
|
+
// Fall back to the inline payload box so the text stays copyable by hand.
|
|
1299
|
+
setPayload(text)
|
|
1300
|
+
}
|
|
1301
|
+
})
|
|
1302
|
+
}
|
|
1179
1303
|
|
|
1180
1304
|
/** Persist the verdicts to localStorage whenever they change. Kept OUT of
|
|
1181
1305
|
* the state updater (updaters must stay pure — React may double-invoke them
|
|
@@ -1263,26 +1387,18 @@ function TriagePanel(props: SlotProps) {
|
|
|
1263
1387
|
|
|
1264
1388
|
// v3.0: Command handlers for native action buttons
|
|
1265
1389
|
const doApproveArchitecturalFix = () => {
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1274
|
-
}
|
|
1275
|
-
})
|
|
1390
|
+
// Gap #10: `force: true` exists on `iterate_fix`, but the tool still
|
|
1391
|
+
// requires file / content / finding / round — the old copy text implied a
|
|
1392
|
+
// lone force flag was enough. The builder names all four and marks the two
|
|
1393
|
+
// the model must fill in.
|
|
1394
|
+
const cmd = buildArchitecturalFixInstruction(findings as IterateFinding[])
|
|
1395
|
+
if (!cmd) return
|
|
1396
|
+
copyWithFeedback(cmd, 'approve-arch')
|
|
1276
1397
|
}
|
|
1277
1398
|
|
|
1278
1399
|
const doTriggerNewRound = () => {
|
|
1279
1400
|
const cmd = '请继续执行下一轮迭代审查'
|
|
1280
|
-
|
|
1281
|
-
if (ok) {
|
|
1282
|
-
setCmdCopied('new-round')
|
|
1283
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1284
|
-
}
|
|
1285
|
-
})
|
|
1401
|
+
copyWithFeedback(cmd, 'new-round')
|
|
1286
1402
|
}
|
|
1287
1403
|
|
|
1288
1404
|
const doRollbackToCheckpoint = () => {
|
|
@@ -1290,39 +1406,22 @@ function TriagePanel(props: SlotProps) {
|
|
|
1290
1406
|
// only); reverting applied fixes is done via iterate_rollback by fix id,
|
|
1291
1407
|
// so the paste-able instruction must point there instead.
|
|
1292
1408
|
const cmd = '请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复'
|
|
1293
|
-
|
|
1294
|
-
if (ok) {
|
|
1295
|
-
setCmdCopied('rollback')
|
|
1296
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1297
|
-
}
|
|
1298
|
-
})
|
|
1409
|
+
copyWithFeedback(cmd, 'rollback')
|
|
1299
1410
|
}
|
|
1300
1411
|
|
|
1301
1412
|
// v3.1+ §8: assign the currently scoped findings to the model for fixing.
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1413
|
+
// Gap #10: `iterate_fix` takes ONE finding + required `content` (full new
|
|
1414
|
+
// file) + required `round` — never an array — so the payload now says so
|
|
1415
|
+
// explicitly instead of handing the model a raw array to translate.
|
|
1305
1416
|
const doAssignFindings = () => {
|
|
1306
1417
|
const targetAll = selectAll
|
|
1307
1418
|
const scopeIndices = targetAll ? allIndices : indices
|
|
1308
1419
|
const scopeFindings = (findings as IterateFinding[])
|
|
1309
1420
|
.filter((f, i) => scopeIndices.includes(i))
|
|
1310
|
-
.map((f) => ({
|
|
1311
|
-
file: String(f.file || ''),
|
|
1312
|
-
...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
|
|
1313
|
-
dimension: String(f.dimension || ''),
|
|
1314
|
-
severity: String(f.severity || ''),
|
|
1315
|
-
summary: String(f.summary || ''),
|
|
1316
|
-
...(f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}),
|
|
1317
|
-
}))
|
|
1318
1421
|
if (scopeFindings.length === 0) return
|
|
1319
|
-
const cmd =
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
setCmdCopied('assign')
|
|
1323
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1324
|
-
}
|
|
1325
|
-
})
|
|
1422
|
+
const cmd = buildAssignFixesInstruction(scopeFindings)
|
|
1423
|
+
if (!cmd) return
|
|
1424
|
+
copyWithFeedback(cmd, 'assign')
|
|
1326
1425
|
}
|
|
1327
1426
|
|
|
1328
1427
|
const doCopyYaml = () => {
|
|
@@ -1331,23 +1430,45 @@ function TriagePanel(props: SlotProps) {
|
|
|
1331
1430
|
copyText(yaml).then((ok) => {
|
|
1332
1431
|
if (ok) {
|
|
1333
1432
|
setCopied(true)
|
|
1334
|
-
setTimeout(() => setCopied(false), 1600)
|
|
1433
|
+
cmdTimers.current.push(window.setTimeout(() => setCopied(false), 1600))
|
|
1335
1434
|
} else {
|
|
1336
1435
|
// Copy failed (permissions/unsupported) — reveal the payload as a
|
|
1337
1436
|
// manual-copy fallback instead of claiming success.
|
|
1338
1437
|
setPayload(yaml)
|
|
1438
|
+
setCmdFailed('yaml')
|
|
1339
1439
|
}
|
|
1340
1440
|
})
|
|
1341
1441
|
}
|
|
1342
1442
|
|
|
1443
|
+
// Gap #7: the SAFE path is `iterate_triage apply` (validation, dedupe,
|
|
1444
|
+
// backup, automatic rollback) — so it is now the primary action, and it
|
|
1445
|
+
// tells the user what happens after they paste.
|
|
1343
1446
|
const doBuildInstruction = () => {
|
|
1344
1447
|
const text = buildApplyInstruction(ignored)
|
|
1448
|
+
if (!text) return
|
|
1449
|
+
setPayload(text)
|
|
1450
|
+
copyWithFeedback(text, 'apply')
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Gap #7: closing the write-back loop — copying is not writing, so offer a
|
|
1454
|
+
// readback instruction (`iterate_triage list`) to verify what landed.
|
|
1455
|
+
const doReadback = () => {
|
|
1456
|
+
const text = buildTriageReadbackInstruction()
|
|
1345
1457
|
setPayload(text)
|
|
1346
|
-
|
|
1458
|
+
copyWithFeedback(text, 'readback')
|
|
1347
1459
|
}
|
|
1348
1460
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1461
|
+
// Audit: `filtered` and `indices` are produced together, but an
|
|
1462
|
+
// out-of-range slot would otherwise render a row keyed `undefined` whose
|
|
1463
|
+
// verdict writes to `verdicts["undefined"]`. Pair them up-front so the
|
|
1464
|
+
// mapping index can never drift from the original findings index.
|
|
1465
|
+
const rowPairs: Array<{ index: number; finding: IterateFinding }> = []
|
|
1466
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
1467
|
+
const idx = indices[i]
|
|
1468
|
+
const f = filtered[i]
|
|
1469
|
+
if (typeof idx === 'number' && f) rowPairs.push({ index: idx, finding: f as IterateFinding })
|
|
1470
|
+
}
|
|
1471
|
+
const rows = rowPairs.map(({ index, finding }) => {
|
|
1351
1472
|
const severity = finding.severity || 'low'
|
|
1352
1473
|
const verdict = verdicts[String(index)] || 'keep'
|
|
1353
1474
|
const isSelected = selected === index
|
|
@@ -1445,43 +1566,74 @@ function TriagePanel(props: SlotProps) {
|
|
|
1445
1566
|
...rows,
|
|
1446
1567
|
React.createElement('div', { className: 'iterate-triage-foot' },
|
|
1447
1568
|
React.createElement('span', {}, `y ${counts.keep} · n ${counts.skip} · a ${counts.ignore} · 待写回 known_intentional:${ignoredCount} 条`),
|
|
1448
|
-
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1569
|
+
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1570
|
+
// Gap #7: the apply instruction (safe path) is PRIMARY; the raw YAML
|
|
1571
|
+
// is demoted to an explicit "just the snippet" escape hatch.
|
|
1572
|
+
React.createElement('button', {
|
|
1573
|
+
className: 'iterate-btn', 'data-primary': '',
|
|
1574
|
+
'data-copied': cmdCopied === 'apply' ? '' : undefined,
|
|
1575
|
+
'data-failed': cmdFailed === 'apply' ? '' : undefined,
|
|
1576
|
+
onClick: doBuildInstruction, disabled: ignoredCount === 0,
|
|
1577
|
+
title: ignoredCount === 0
|
|
1578
|
+
? '当前没有标记为「已知有意」的 finding'
|
|
1579
|
+
: '复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)',
|
|
1580
|
+
}, cmdCopied === 'apply' ? '已复制' : cmdFailed === 'apply' ? '复制失败' : `生成应用指令${ignoredCount > 0 ? `(${ignoredCount})` : ''}`),
|
|
1449
1581
|
React.createElement('button', {
|
|
1450
|
-
className: 'iterate-btn',
|
|
1582
|
+
className: 'iterate-btn',
|
|
1583
|
+
'data-copied': copied ? '' : undefined,
|
|
1584
|
+
'data-failed': cmdFailed === 'yaml' ? '' : undefined,
|
|
1451
1585
|
onClick: doCopyYaml, disabled: ignoredCount === 0,
|
|
1452
|
-
title: ignoredCount === 0
|
|
1453
|
-
|
|
1586
|
+
title: ignoredCount === 0
|
|
1587
|
+
? '当前没有标记为「已知有意」的 finding'
|
|
1588
|
+
: '只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)',
|
|
1589
|
+
}, copied ? '已复制' : cmdFailed === 'yaml' ? '复制失败' : '仅复制 YAML 片段'),
|
|
1454
1590
|
React.createElement('button', {
|
|
1455
|
-
className: 'iterate-btn',
|
|
1456
|
-
|
|
1457
|
-
|
|
1591
|
+
className: 'iterate-btn',
|
|
1592
|
+
'data-copied': cmdCopied === 'readback' ? '' : undefined,
|
|
1593
|
+
'data-failed': cmdFailed === 'readback' ? '' : undefined,
|
|
1594
|
+
onClick: doReadback,
|
|
1595
|
+
title: '复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)',
|
|
1596
|
+
}, cmdCopied === 'readback' ? '已复制' : cmdFailed === 'readback' ? '复制失败' : '回读已写入条目'),
|
|
1458
1597
|
),
|
|
1598
|
+
cmdCopied === 'apply'
|
|
1599
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, '已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。')
|
|
1600
|
+
: cmdCopied === 'readback'
|
|
1601
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, '已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。')
|
|
1602
|
+
: null,
|
|
1459
1603
|
),
|
|
1460
1604
|
// v3.0: Command buttons for native actions (§8)
|
|
1461
1605
|
React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1462
1606
|
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
|
|
1463
|
-
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1607
|
+
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1464
1608
|
React.createElement('button', {
|
|
1465
|
-
className: 'iterate-cmd', 'data-primary': '',
|
|
1609
|
+
className: 'iterate-cmd', 'data-primary': '',
|
|
1610
|
+
'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1611
|
+
'data-failed': cmdFailed === 'approve-arch' ? '' : undefined,
|
|
1466
1612
|
onClick: doApproveArchitecturalFix,
|
|
1467
|
-
title: '
|
|
1468
|
-
}, cmdCopied === 'approve-arch' ? '已复制' : '批准架构修复'),
|
|
1613
|
+
title: '批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)',
|
|
1614
|
+
}, cmdCopied === 'approve-arch' ? '已复制' : cmdFailed === 'approve-arch' ? '复制失败' : '批准架构修复'),
|
|
1469
1615
|
React.createElement('button', {
|
|
1470
|
-
className: 'iterate-cmd',
|
|
1616
|
+
className: 'iterate-cmd',
|
|
1617
|
+
'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1618
|
+
'data-failed': cmdFailed === 'new-round' ? '' : undefined,
|
|
1471
1619
|
onClick: doTriggerNewRound,
|
|
1472
1620
|
title: '触发新一轮迭代审查',
|
|
1473
|
-
}, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
|
|
1621
|
+
}, cmdCopied === 'new-round' ? '已复制' : cmdFailed === 'new-round' ? '复制失败' : '触发新一轮'),
|
|
1474
1622
|
React.createElement('button', {
|
|
1475
|
-
className: 'iterate-cmd', 'data-primary': '',
|
|
1623
|
+
className: 'iterate-cmd', 'data-primary': '',
|
|
1624
|
+
'data-copied': cmdCopied === 'assign' ? '' : undefined,
|
|
1625
|
+
'data-failed': cmdFailed === 'assign' ? '' : undefined,
|
|
1476
1626
|
onClick: doAssignFindings,
|
|
1477
1627
|
disabled: (selectAll ? allIndices : indices).length === 0,
|
|
1478
|
-
title: selectAll ? `指派全部 ${allIndices.length} 个 findings
|
|
1479
|
-
}, cmdCopied === 'assign' ? '已复制' : '指派修复'),
|
|
1628
|
+
title: selectAll ? `指派全部 ${allIndices.length} 个 findings 修复(逐个 iterate_fix)` : `指派当前可见 ${indices.length} 个 findings 修复(逐个 iterate_fix)`,
|
|
1629
|
+
}, cmdCopied === 'assign' ? '已复制' : cmdFailed === 'assign' ? '复制失败' : '指派修复'),
|
|
1480
1630
|
React.createElement('button', {
|
|
1481
|
-
className: 'iterate-cmd', 'data-danger': '',
|
|
1631
|
+
className: 'iterate-cmd', 'data-danger': '',
|
|
1632
|
+
'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1633
|
+
'data-failed': cmdFailed === 'rollback' ? '' : undefined,
|
|
1482
1634
|
onClick: doRollbackToCheckpoint,
|
|
1483
1635
|
title: '回滚到上一个检查点',
|
|
1484
|
-
}, cmdCopied === 'rollback' ? '已复制' : '回滚检查点'),
|
|
1636
|
+
}, cmdCopied === 'rollback' ? '已复制' : cmdFailed === 'rollback' ? '复制失败' : '回滚检查点'),
|
|
1485
1637
|
),
|
|
1486
1638
|
),
|
|
1487
1639
|
payload
|
|
@@ -1592,9 +1744,27 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1592
1744
|
const [showStatus, setShowStatus] = React.useState(false)
|
|
1593
1745
|
const [confirming, setConfirming] = React.useState(false)
|
|
1594
1746
|
const [clearedInfo, setClearedInfo] = React.useState<number | null>(null)
|
|
1747
|
+
// Gap #8: "pick a field → generate an iterate_config write instruction".
|
|
1748
|
+
// Defaults to validation.commands, the field the old guide omitted entirely
|
|
1749
|
+
// (and the root cause of the zero-verification P0).
|
|
1750
|
+
const [cfgField, setCfgField] = React.useState<string>(
|
|
1751
|
+
() => (CONFIG_EDIT_FIELDS.some((f) => f.key === 'validation.commands') ? 'validation.commands' : (CONFIG_EDIT_FIELDS[0]?.key ?? 'goal')),
|
|
1752
|
+
)
|
|
1753
|
+
const [cfgCopied, setCfgCopied] = React.useState(false)
|
|
1754
|
+
const [cfgFailed, setCfgFailed] = React.useState(false)
|
|
1755
|
+
const [cfgText, setCfgText] = React.useState<string | null>(null)
|
|
1756
|
+
const [pullCopied, setPullCopied] = React.useState(false)
|
|
1757
|
+
const [copyError, setCopyError] = React.useState<'guide' | 'status' | 'pull' | null>(null)
|
|
1595
1758
|
const guide = buildConfigEditGuide()
|
|
1596
1759
|
const statusGuide = buildRuntimeStatusGuide()
|
|
1597
1760
|
|
|
1761
|
+
// Audit: don't leak flash timers across settings unmounts.
|
|
1762
|
+
const cfgTimers = React.useRef<number[]>([])
|
|
1763
|
+
React.useEffect(() => () => {
|
|
1764
|
+
for (const t of cfgTimers.current) clearTimeout(t)
|
|
1765
|
+
cfgTimers.current = []
|
|
1766
|
+
}, [])
|
|
1767
|
+
|
|
1598
1768
|
const toggleTheme = () => {
|
|
1599
1769
|
const next = !enabled
|
|
1600
1770
|
setEnabled(next)
|
|
@@ -1602,30 +1772,46 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1602
1772
|
}
|
|
1603
1773
|
|
|
1604
1774
|
/** Flash a per-slot "已复制" state for the given duration. */
|
|
1605
|
-
const flashCopied = (slot: 'guide' | 'status') => {
|
|
1606
|
-
const setter = slot === 'guide' ? setGuideCopied : setStatusCopied
|
|
1775
|
+
const flashCopied = (slot: 'guide' | 'status' | 'pull') => {
|
|
1776
|
+
const setter = slot === 'guide' ? setGuideCopied : slot === 'status' ? setStatusCopied : setPullCopied
|
|
1607
1777
|
setter(true)
|
|
1608
|
-
setTimeout(() => setter(false), 1600)
|
|
1778
|
+
cfgTimers.current.push(window.setTimeout(() => setter(false), 1600))
|
|
1609
1779
|
}
|
|
1610
1780
|
|
|
1611
|
-
/** Copy a guide/status block; flash success only when it actually copied
|
|
1612
|
-
|
|
1781
|
+
/** Copy a guide/status block; flash success only when it actually copied,
|
|
1782
|
+
* and surface a visible error when the clipboard write is refused (the old
|
|
1783
|
+
* behaviour was a silent no-op, which reads as a broken button). */
|
|
1784
|
+
const doCopy = (text: string, slot: 'guide' | 'status' | 'pull') => {
|
|
1613
1785
|
copyText(text).then((ok) => {
|
|
1786
|
+
setCopyError(ok ? null : slot)
|
|
1614
1787
|
if (ok) flashCopied(slot)
|
|
1615
1788
|
})
|
|
1616
1789
|
}
|
|
1617
1790
|
|
|
1791
|
+
/** Gap #8: build + copy the write instruction for the selected field. */
|
|
1792
|
+
const doBuildConfigInstruction = () => {
|
|
1793
|
+
const text = buildConfigFieldInstruction(cfgField)
|
|
1794
|
+
setCfgText(text)
|
|
1795
|
+
copyText(text).then((ok) => {
|
|
1796
|
+
setCfgFailed(!ok)
|
|
1797
|
+
setCfgCopied(ok)
|
|
1798
|
+
cfgTimers.current.push(window.setTimeout(() => setCfgCopied(false), 1600))
|
|
1799
|
+
// On failure the raw text stays on screen for a manual copy.
|
|
1800
|
+
if (!ok) return
|
|
1801
|
+
})
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1618
1804
|
/** Two-step destroy guard: first click arms, second click clears. */
|
|
1619
1805
|
const requestClear = () => {
|
|
1620
1806
|
if (confirming) {
|
|
1621
1807
|
const count = removeStorageByPrefix(TRIAGE_STORAGE_PREFIX)
|
|
1622
1808
|
setClearedInfo(count)
|
|
1623
1809
|
setConfirming(false)
|
|
1624
|
-
setTimeout(() => setClearedInfo(null), 3000)
|
|
1810
|
+
cfgTimers.current.push(window.setTimeout(() => setClearedInfo(null), 3000))
|
|
1625
1811
|
return
|
|
1626
1812
|
}
|
|
1627
1813
|
setConfirming(true)
|
|
1628
|
-
setTimeout(() => setConfirming(false), 3000)
|
|
1814
|
+
cfgTimers.current.push(window.setTimeout(() => setConfirming(false), 3000))
|
|
1629
1815
|
}
|
|
1630
1816
|
|
|
1631
1817
|
const clearButton = clearedInfo !== null
|
|
@@ -1675,13 +1861,50 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1675
1861
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1676
1862
|
React.createElement('div', {},
|
|
1677
1863
|
React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
|
|
1678
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 /
|
|
1864
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。'),
|
|
1679
1865
|
),
|
|
1680
1866
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1681
1867
|
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': guideCopied ? '' : undefined, onClick: () => doCopy(guide, 'guide') }, guideCopied ? '已复制' : '复制指引'),
|
|
1682
1868
|
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowGuide((v) => !v) }, showGuide ? '收起' : '展开'),
|
|
1683
1869
|
),
|
|
1684
1870
|
),
|
|
1871
|
+
// Gap #8: wire up the previously-dead `buildConfigFieldInstruction` — pick
|
|
1872
|
+
// a field, generate the `iterate_config` write payload for it, paste it to
|
|
1873
|
+
// the model. `validation.commands` is the default selection.
|
|
1874
|
+
React.createElement('div', { className: 'iterate-guide-bar', style: { marginTop: 8, flexWrap: 'wrap' } },
|
|
1875
|
+
React.createElement('select', {
|
|
1876
|
+
className: 'iterate-filter-select',
|
|
1877
|
+
value: cfgField,
|
|
1878
|
+
'aria-label': '选择要修改的配置字段',
|
|
1879
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setCfgField(e.target.value),
|
|
1880
|
+
},
|
|
1881
|
+
...CONFIG_EDIT_FIELDS.map((f) =>
|
|
1882
|
+
React.createElement('option', { key: f.key, value: f.key }, `${f.label}(${f.key})`),
|
|
1883
|
+
),
|
|
1884
|
+
),
|
|
1885
|
+
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
1886
|
+
configFieldByKey(cfgField)?.hint ?? '',
|
|
1887
|
+
),
|
|
1888
|
+
React.createElement('button', {
|
|
1889
|
+
className: 'iterate-btn', 'data-primary': '',
|
|
1890
|
+
'data-copied': cfgCopied ? '' : undefined,
|
|
1891
|
+
'data-failed': cfgFailed ? '' : undefined,
|
|
1892
|
+
onClick: doBuildConfigInstruction,
|
|
1893
|
+
title: '生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)',
|
|
1894
|
+
}, cfgCopied ? '已复制' : cfgFailed ? '复制失败' : '生成写入指令'),
|
|
1895
|
+
cfgFailed
|
|
1896
|
+
? React.createElement('span', { className: 'iterate-settings-desc' }, '剪贴板被拒绝——指令已展示在下方,请手动复制。')
|
|
1897
|
+
: null,
|
|
1898
|
+
),
|
|
1899
|
+
cfgText
|
|
1900
|
+
? React.createElement('div', { className: 'iterate-guide' },
|
|
1901
|
+
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1902
|
+
React.createElement('span', {}, '写入指令(粘贴给模型)'),
|
|
1903
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgText, 'guide') }, '复制'),
|
|
1904
|
+
),
|
|
1905
|
+
React.createElement('div', { className: 'iterate-guide-body' }, cfgText),
|
|
1906
|
+
)
|
|
1907
|
+
: null,
|
|
1685
1908
|
showGuide
|
|
1686
1909
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1687
1910
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
@@ -1701,6 +1924,13 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1701
1924
|
),
|
|
1702
1925
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1703
1926
|
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': statusCopied ? '' : undefined, onClick: () => doCopy(statusGuide, 'status') }, statusCopied ? '已复制' : '复制指引'),
|
|
1927
|
+
// Gap #4: RUNTIME_ARTIFACTS stops being documentation-only — this
|
|
1928
|
+
// copies the instruction that makes the model echo those artifacts
|
|
1929
|
+
// into the session so the observatory panels can read them.
|
|
1930
|
+
React.createElement('button', {
|
|
1931
|
+
className: 'iterate-btn', 'data-copied': pullCopied ? '' : undefined,
|
|
1932
|
+
onClick: () => doCopy(buildDiskSnapshotInstruction(), 'pull'),
|
|
1933
|
+
}, pullCopied ? '已复制' : '拉取磁盘快照'),
|
|
1704
1934
|
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowStatus((v) => !v) }, showStatus ? '收起' : '展开'),
|
|
1705
1935
|
),
|
|
1706
1936
|
),
|
|
@@ -1731,6 +1961,13 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1731
1961
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'settings', className: 'iterate-settings' },
|
|
1732
1962
|
React.createElement('div', { className: 'iterate-settings-title', style: { fontSize: 15, fontWeight: 700 } }, 'iterate 设置'),
|
|
1733
1963
|
banner,
|
|
1964
|
+
copyError
|
|
1965
|
+
? React.createElement('div', { key: 'copy-err', className: 'iterate-scard', role: 'alert' },
|
|
1966
|
+
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
1967
|
+
'剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。',
|
|
1968
|
+
),
|
|
1969
|
+
)
|
|
1970
|
+
: null,
|
|
1734
1971
|
themeCard,
|
|
1735
1972
|
dataCard,
|
|
1736
1973
|
guideCard,
|
|
@@ -1740,23 +1977,43 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1740
1977
|
|
|
1741
1978
|
// ─── Runtime observatory panel (F1–F7) ──────────────────────────────────────
|
|
1742
1979
|
|
|
1743
|
-
/** Tab id → label for the observatory's
|
|
1744
|
-
* stream) is first so it is the default view; F-panels follow.
|
|
1980
|
+
/** Tab id → label for the observatory's eleven panels. Live (real-time
|
|
1981
|
+
* activity stream) is first so it is the default view; F-panels follow.
|
|
1982
|
+
* Gap #14: labels carry the "Fn" number used everywhere else (README,
|
|
1983
|
+
* code comments, the copy instructions), so the docs and the UI line up. */
|
|
1745
1984
|
const OBS_TABS = [
|
|
1746
|
-
{ key: 'live', label: '实时活动流' },
|
|
1747
|
-
{ key: 'f1', label: '审查线程' },
|
|
1748
|
-
{ key: 'f2', label: '收敛趋势' },
|
|
1749
|
-
{ key: 'f3', label: '发现定位' },
|
|
1750
|
-
{ key: 'f4', label: '修复与回滚' },
|
|
1751
|
-
{ key: 'f5', label: '断点恢复' },
|
|
1752
|
-
{ key: 'f6', label: '运行控制台' },
|
|
1753
|
-
{ key: 'f7', label: '决策时间线' },
|
|
1985
|
+
{ key: 'live', label: '实时活动流', hotkey: '' },
|
|
1986
|
+
{ key: 'f1', label: '审查线程', hotkey: '1' },
|
|
1987
|
+
{ key: 'f2', label: '收敛趋势', hotkey: '2' },
|
|
1988
|
+
{ key: 'f3', label: '发现定位', hotkey: '3' },
|
|
1989
|
+
{ key: 'f4', label: '修复与回滚', hotkey: '4' },
|
|
1990
|
+
{ key: 'f5', label: '断点恢复', hotkey: '5' },
|
|
1991
|
+
{ key: 'f6', label: '运行控制台', hotkey: '6' },
|
|
1992
|
+
{ key: 'f7', label: '决策时间线', hotkey: '7' },
|
|
1754
1993
|
// v3.0: Quality Command Center tabs
|
|
1755
|
-
{ key: 'f8', label: '质量门禁' },
|
|
1756
|
-
{ key: 'f9', label: '经验银行' },
|
|
1757
|
-
{ key: 'f10', label: '防御事件' },
|
|
1994
|
+
{ key: 'f8', label: '质量门禁', hotkey: '8' },
|
|
1995
|
+
{ key: 'f9', label: '经验银行', hotkey: '9' },
|
|
1996
|
+
{ key: 'f10', label: '防御事件', hotkey: '0' },
|
|
1758
1997
|
] as const
|
|
1759
1998
|
|
|
1999
|
+
/** Digit → tab id. `1-9` map to F1–F9, `0` to F10, `-` back to the live feed
|
|
2000
|
+
* (the default view). Digits are used instead of F1–F10 to avoid clashing
|
|
2001
|
+
* with browser/system F-key bindings. */
|
|
2002
|
+
const OBS_HOTKEY_TABS: Record<string, (typeof OBS_TABS)[number]['key']> = {
|
|
2003
|
+
1: 'f1', 2: 'f2', 3: 'f3', 4: 'f4', 5: 'f5',
|
|
2004
|
+
6: 'f6', 7: 'f7', 8: 'f8', 9: 'f9', 0: 'f10',
|
|
2005
|
+
'-': 'live',
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/** Visible label for a tab (keeps the number prefix in sync with `OBS_TABS`). */
|
|
2009
|
+
function obsTabLabel(t: (typeof OBS_TABS)[number]): string {
|
|
2010
|
+
if (t.key === 'live') return t.label
|
|
2011
|
+
return `${t.key.toUpperCase()} ${t.label}`
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/** Hotkey hint shown in the collapsed header, e.g. "11 页 · 1-9/0/- 切换". */
|
|
2015
|
+
const OBS_HOTKEY_HINT = `${String(OBS_TABS.length)} 页 · 按 1-9/0 切 Fn、- 回实时流`
|
|
2016
|
+
|
|
1760
2017
|
/** Live subagent activity badges: type → Chinese label + dsw token color accent. */
|
|
1761
2018
|
const OBS_LIVE_META: Record<string, { label: string; color: string }> = {
|
|
1762
2019
|
read: { label: '阅读', color: 'var(--dsw-alias-brand-primary)' },
|
|
@@ -1820,16 +2077,13 @@ function latestDefenseEvents(session: unknown): ObsDefenseResult | null {
|
|
|
1820
2077
|
return scanSessionForDefenseEvents(session) as ObsDefenseResult | null
|
|
1821
2078
|
}
|
|
1822
2079
|
|
|
1823
|
-
/** Build a human-pasteable `iterate_fix` instruction for one finding.
|
|
2080
|
+
/** Build a human-pasteable `iterate_fix` instruction for one finding.
|
|
2081
|
+
* Gap #10: delegates to the shared builder so the payload always names the
|
|
2082
|
+
* four required `iterate_fix` params (file / content / finding / round) and
|
|
2083
|
+
* marks `content` + `round` as model-supplied instead of shipping a partial
|
|
2084
|
+
* call the model has to guess at. */
|
|
1824
2085
|
function buildObsFixInstruction(f: ObsFinding): string {
|
|
1825
|
-
|
|
1826
|
-
file: String(f.file || ''),
|
|
1827
|
-
...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
|
|
1828
|
-
dimension: String(f.dimension || ''),
|
|
1829
|
-
summary: String(f.summary || ''),
|
|
1830
|
-
...(f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}),
|
|
1831
|
-
}, null, 2)
|
|
1832
|
-
return `请调用 \`iterate_fix\` 修复以下 finding:\n\n\`\`\`json\n${payload}\n\`\`\``
|
|
2086
|
+
return buildFixInstruction(f as unknown as Record<string, unknown>)
|
|
1833
2087
|
}
|
|
1834
2088
|
|
|
1835
2089
|
/**
|
|
@@ -1880,6 +2134,40 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1880
2134
|
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1881
2135
|
React.useEffect(() => () => { if (copyTimer.current) clearTimeout(copyTimer.current) }, [])
|
|
1882
2136
|
|
|
2137
|
+
// Gap #14: digit shortcuts for tab switching. Digit keys (1-9 / 0 / -) are
|
|
2138
|
+
// used instead of F1–F10 so we never fight the browser's own F-key handling.
|
|
2139
|
+
// Guards mirror the triage panel: no modifier keys, no editable focus, and
|
|
2140
|
+
// only while the observatory is expanded AND focus is inside it — so the
|
|
2141
|
+
// keys never steal input from the composer or another panel.
|
|
2142
|
+
React.useEffect(() => {
|
|
2143
|
+
const doc = typeof document !== 'undefined' ? document : null
|
|
2144
|
+
if (!doc || !open) return
|
|
2145
|
+
const onKeyDown = (ev: KeyboardEvent) => {
|
|
2146
|
+
if (ev.metaKey || ev.ctrlKey || ev.altKey || ev.shiftKey) return
|
|
2147
|
+
const t = ev.target as HTMLElement | null
|
|
2148
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
|
|
2149
|
+
if (t && typeof t.isContentEditable === 'boolean' && t.isContentEditable) return
|
|
2150
|
+
const rootEl = doc.querySelector('[data-iterate="obs"]')
|
|
2151
|
+
if (!rootEl) return
|
|
2152
|
+
const activeEl = doc.activeElement as HTMLElement | null
|
|
2153
|
+
// Act when focus is INSIDE the observatory, or nowhere in particular
|
|
2154
|
+
// (body/null — some browsers never focus a <button> on click). Focus
|
|
2155
|
+
// parked on a non-editable element outside the panel keeps standard
|
|
2156
|
+
// behavior; focus in the composer was already excluded above.
|
|
2157
|
+
const inPanel = activeEl === rootEl ||
|
|
2158
|
+
(activeEl !== null && typeof rootEl.contains === 'function' && rootEl.contains(activeEl))
|
|
2159
|
+
const idle = activeEl === null || activeEl === doc.body
|
|
2160
|
+
if (!inPanel && !idle) return
|
|
2161
|
+
const next = OBS_HOTKEY_TABS[ev.key]
|
|
2162
|
+
if (!next) return
|
|
2163
|
+
ev.preventDefault()
|
|
2164
|
+
setTab(next)
|
|
2165
|
+
}
|
|
2166
|
+
doc.addEventListener('keydown', onKeyDown)
|
|
2167
|
+
return () => doc.removeEventListener('keydown', onKeyDown)
|
|
2168
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2169
|
+
}, [open])
|
|
2170
|
+
|
|
1883
2171
|
/** Copy an instruction, flashing "已复制" on the originating button only when the copy actually succeeded. */
|
|
1884
2172
|
const copyInstruction = (key: string, text: string) => {
|
|
1885
2173
|
if (!text) return
|
|
@@ -1891,6 +2179,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1891
2179
|
})
|
|
1892
2180
|
}
|
|
1893
2181
|
|
|
2182
|
+
// Gap #4: `.iterate/` artifacts are unreachable from the browser — this is
|
|
2183
|
+
// the one instruction that makes the model echo them into the session
|
|
2184
|
+
// stream, which is what every panel scans.
|
|
2185
|
+
const pullInstruction = buildDiskSnapshotInstruction()
|
|
2186
|
+
const pullTitle = `复制拉取磁盘快照指令:让模型逐个调用 ${DISK_SNAPSHOT_SOURCES.map((s) => s.tool).join(' / ')} 并回显完整结果`
|
|
2187
|
+
|
|
1894
2188
|
/** Toggle one thread; a manual toggle exits any forced all/none mode. */
|
|
1895
2189
|
const toggleThread = (key: string) => {
|
|
1896
2190
|
setThreadMode('auto')
|
|
@@ -1901,10 +2195,19 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1901
2195
|
})
|
|
1902
2196
|
}
|
|
1903
2197
|
|
|
1904
|
-
/** Export the full observatory snapshot as JSON (download, then copy fallback).
|
|
2198
|
+
/** Export the full observatory snapshot as JSON (download, then copy fallback).
|
|
2199
|
+
* Gap #11: the payload also carries the four on-disk artifacts the panels
|
|
2200
|
+
* render (quality gate, experience bank, defense events, final report) —
|
|
2201
|
+
* previously only `manifest` + `live` were exported, so a review could not
|
|
2202
|
+
* be reconstructed from the file. */
|
|
1905
2203
|
const exportObservatory = () => {
|
|
1906
2204
|
if (!manifest) return
|
|
1907
|
-
const json = serializeObservatoryExport(manifest, manifest.live || []
|
|
2205
|
+
const json = serializeObservatoryExport(manifest, manifest.live || [], {
|
|
2206
|
+
qualityGate,
|
|
2207
|
+
experienceBank,
|
|
2208
|
+
defenseEvents,
|
|
2209
|
+
report: latestReport(session),
|
|
2210
|
+
})
|
|
1908
2211
|
const filename = `iterate-observatory-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
|
1909
2212
|
const flash = () => {
|
|
1910
2213
|
setCopiedKey('export')
|
|
@@ -1938,7 +2241,15 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1938
2241
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
1939
2242
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': '' },
|
|
1940
2243
|
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
1941
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2244
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `暂无运行时观测数据 · ${OBS_HOTKEY_HINT}`),
|
|
2245
|
+
React.createElement('button', {
|
|
2246
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
2247
|
+
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
2248
|
+
e.stopPropagation()
|
|
2249
|
+
copyInstruction('pull-snapshot', pullInstruction)
|
|
2250
|
+
},
|
|
2251
|
+
title: '复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充',
|
|
2252
|
+
}, copiedKey === 'pull-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
1942
2253
|
),
|
|
1943
2254
|
)
|
|
1944
2255
|
}
|
|
@@ -2041,11 +2352,66 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2041
2352
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无收敛数据')
|
|
2042
2353
|
}
|
|
2043
2354
|
const points = conv.map((n, i) => ({ round: i + 1, count: n }))
|
|
2355
|
+
// Gap #13 (step ①): single-session cross-round comparison. One row per
|
|
2356
|
+
// round — findings raised / fixed / severity mix / validation outcome.
|
|
2357
|
+
// Cross-SESSION comparison rides on the #4 pull instruction instead (the
|
|
2358
|
+
// client has no filesystem access, so `iterate_history` aggregates have to
|
|
2359
|
+
// be echoed back into the session first).
|
|
2360
|
+
const report = latestReport(session)
|
|
2361
|
+
const comparison = buildRoundComparison(report, manifest.validations || [])
|
|
2362
|
+
const comparisonTable = comparison.length === 0
|
|
2363
|
+
? null
|
|
2364
|
+
: React.createElement('table', { className: 'iterate-obs-table' },
|
|
2365
|
+
React.createElement('thead', {},
|
|
2366
|
+
React.createElement('tr', {},
|
|
2367
|
+
React.createElement('th', {}, '轮次'),
|
|
2368
|
+
React.createElement('th', {}, '发现'),
|
|
2369
|
+
React.createElement('th', {}, '已修复'),
|
|
2370
|
+
React.createElement('th', {}, '严重度分布'),
|
|
2371
|
+
React.createElement('th', {}, '验证'),
|
|
2372
|
+
),
|
|
2373
|
+
),
|
|
2374
|
+
React.createElement('tbody', {},
|
|
2375
|
+
...comparison.map((row) => {
|
|
2376
|
+
const sevText = Object.entries(row.severities)
|
|
2377
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
2378
|
+
.join(' · ')
|
|
2379
|
+
const anyFailed = row.validations.some((v) => v.allowed !== true)
|
|
2380
|
+
const valText = row.validations.length === 0
|
|
2381
|
+
? '—'
|
|
2382
|
+
: `${row.validations.filter((v) => v.allowed === true).length}/${row.validations.length} 通过${anyFailed ? '(含失败)' : ''}`
|
|
2383
|
+
return React.createElement('tr', { key: `cmp-${row.round}` },
|
|
2384
|
+
React.createElement('td', {}, `Round ${row.round}`),
|
|
2385
|
+
React.createElement('td', {}, String(row.findings)),
|
|
2386
|
+
React.createElement('td', {}, String(row.fixed)),
|
|
2387
|
+
React.createElement('td', {}, sevText || '—'),
|
|
2388
|
+
React.createElement('td', {
|
|
2389
|
+
style: anyFailed
|
|
2390
|
+
? { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 600 }
|
|
2391
|
+
: row.validations.length > 0
|
|
2392
|
+
? { color: 'var(--dsw-alias-state-success-primary)' }
|
|
2393
|
+
: undefined,
|
|
2394
|
+
}, valText),
|
|
2395
|
+
)
|
|
2396
|
+
}),
|
|
2397
|
+
),
|
|
2398
|
+
)
|
|
2044
2399
|
return React.createElement('div', {},
|
|
2045
2400
|
React.createElement(TrendChart, { points }),
|
|
2046
2401
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2047
2402
|
`各轮发现数量:${conv.join(' → ')}${typeof manifest.round === 'number' ? ` · 当前 Round ${manifest.round}` : ''}`,
|
|
2048
2403
|
),
|
|
2404
|
+
comparisonTable
|
|
2405
|
+
? React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2406
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2407
|
+
React.createElement('span', {}, '跨轮对比(本会话)'),
|
|
2408
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2409
|
+
'跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行',
|
|
2410
|
+
),
|
|
2411
|
+
),
|
|
2412
|
+
React.createElement('div', { className: 'iterate-obs-block-body' }, comparisonTable),
|
|
2413
|
+
)
|
|
2414
|
+
: null,
|
|
2049
2415
|
)
|
|
2050
2416
|
}
|
|
2051
2417
|
|
|
@@ -2171,7 +2537,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2171
2537
|
return React.createElement('div', {}, ...fixes.map((f, i) => {
|
|
2172
2538
|
const k = `f4-${i}`
|
|
2173
2539
|
const id = String(f.id || `fix#${i + 1}`)
|
|
2174
|
-
const rollbackText =
|
|
2540
|
+
const rollbackText = buildRollbackInstruction(id)
|
|
2175
2541
|
const added = typeof f.linesAdded === 'number' ? f.linesAdded : 0
|
|
2176
2542
|
const removed = typeof f.linesRemoved === 'number' ? f.linesRemoved : 0
|
|
2177
2543
|
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
@@ -2203,18 +2569,19 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2203
2569
|
const renderCheckpoint = () => {
|
|
2204
2570
|
const cp = manifest.checkpoint
|
|
2205
2571
|
if (!cp) {
|
|
2206
|
-
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2572
|
+
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2573
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_transcript'), '断点'),
|
|
2574
|
+
)
|
|
2207
2575
|
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}, null, 2)}\n\`\`\``
|
|
2576
|
+
// Gap #10: `resume` accepts ONLY `operation` — `mode` / `round` /
|
|
2577
|
+
// `maxRounds` are save-side inputs and `maxRounds: null` was an
|
|
2578
|
+
// invalid-integer field on a positive-integer schema.
|
|
2579
|
+
const resumeText = buildCheckpointResumeInstruction()
|
|
2213
2580
|
// Clearing a stale checkpoint after an interrupted run — iterate_checkpoint
|
|
2214
2581
|
// exposes `clear`, but before this button it was only reachable by typing
|
|
2215
2582
|
// the command manually (an interrupted run otherwise shows a permanent
|
|
2216
2583
|
// "可恢复" chip with no companion reset action).
|
|
2217
|
-
const clearText =
|
|
2584
|
+
const clearText = buildCheckpointClearInstruction()
|
|
2218
2585
|
const item = (label: string, value: unknown) =>
|
|
2219
2586
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
2220
2587
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
@@ -2259,6 +2626,50 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2259
2626
|
// text:null. Copied as a paste-able instruction, mirroring the panel's
|
|
2260
2627
|
// copy-to-command pattern — it cannot be cleared from the client directly.
|
|
2261
2628
|
const activeClearInstruction = `请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n${JSON.stringify({ operation: 'nudge', text: null }, null, 2)}\n\`\`\``
|
|
2629
|
+
// Gap #6: per-round validation rows (command / exitCode / allowed) live on
|
|
2630
|
+
// the manifest; when the run stopped on a validation failure the failing
|
|
2631
|
+
// command is called out explicitly instead of being buried in a list.
|
|
2632
|
+
const validations = Array.isArray(manifest.validations) ? manifest.validations : []
|
|
2633
|
+
const abortedByValidation = String(manifest.stoppedReason || '') === 'aborted_by_validation'
|
|
2634
|
+
const validationRows = validations.map((v, i) => {
|
|
2635
|
+
const allowed = v.allowed === true
|
|
2636
|
+
const failed = !allowed
|
|
2637
|
+
const highlight = failed && abortedByValidation
|
|
2638
|
+
const exit = typeof v.exitCode === 'number' ? `exit ${v.exitCode}` : 'exit —'
|
|
2639
|
+
return React.createElement('div', {
|
|
2640
|
+
key: `val-${i}`,
|
|
2641
|
+
className: 'iterate-obs-row',
|
|
2642
|
+
'data-status': allowed ? 'pass' : 'fail',
|
|
2643
|
+
'data-highlight': highlight ? '' : undefined,
|
|
2644
|
+
title: v.rejectReason ? String(v.rejectReason) : undefined,
|
|
2645
|
+
},
|
|
2646
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${typeof v.round === 'number' ? v.round : '?'}`),
|
|
2647
|
+
React.createElement('code', { className: 'iterate-obs-mono' }, String(v.command || '')),
|
|
2648
|
+
React.createElement('span', { className: 'iterate-obs-chip', 'data-status': allowed ? 'pass' : 'fail' },
|
|
2649
|
+
allowed ? '通过' : '失败',
|
|
2650
|
+
),
|
|
2651
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, exit),
|
|
2652
|
+
highlight ? React.createElement('span', { className: 'iterate-obs-chip', 'data-status': 'fail' }, '导致中止') : null,
|
|
2653
|
+
)
|
|
2654
|
+
})
|
|
2655
|
+
const validationsBlock = React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2656
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2657
|
+
React.createElement('span', {}, '验证结果'),
|
|
2658
|
+
validations.length === 0
|
|
2659
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, '本轮尚无验证记录')
|
|
2660
|
+
: React.createElement('span', { className: 'iterate-obs-head-meta' }, `${validations.length} 条命令`),
|
|
2661
|
+
abortedByValidation
|
|
2662
|
+
? React.createElement('span', { className: 'iterate-obs-badge', 'data-status': 'fail' }, '因验证失败中止')
|
|
2663
|
+
: null,
|
|
2664
|
+
),
|
|
2665
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2666
|
+
validations.length === 0
|
|
2667
|
+
? React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2668
|
+
'尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。',
|
|
2669
|
+
)
|
|
2670
|
+
: React.createElement(React.Fragment, null, ...validationRows),
|
|
2671
|
+
),
|
|
2672
|
+
)
|
|
2262
2673
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2263
2674
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2264
2675
|
React.createElement('span', {}, '运行控制台'),
|
|
@@ -2293,6 +2704,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2293
2704
|
title: '复制 iterate_transcript nudge 指令文本',
|
|
2294
2705
|
}, copiedKey === 'nudge' ? '已复制' : '复制 nudge 指令'),
|
|
2295
2706
|
),
|
|
2707
|
+
validationsBlock,
|
|
2296
2708
|
),
|
|
2297
2709
|
)
|
|
2298
2710
|
}
|
|
@@ -2387,29 +2799,40 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2387
2799
|
// session (dimension convergence, verification pass rate, overall PASS/FAIL);
|
|
2388
2800
|
// falls back to copy-able query instructions when no snapshot exists yet.
|
|
2389
2801
|
const renderQualityGate = () => {
|
|
2390
|
-
|
|
2802
|
+
// Gap #10: the query instruction now carries explicit JSON args, matching
|
|
2803
|
+
// the rest of the panel's copy-to-command payloads (the model no longer
|
|
2804
|
+
// has to infer `operation`).
|
|
2805
|
+
const gateInstruction = buildQualityGateQueryInstruction()
|
|
2391
2806
|
// Reset a stale FAIL certificate before a fresh iteration — the tool's
|
|
2392
2807
|
// `clear` operation existed since v3.5.1 but had no client entry point.
|
|
2393
|
-
const gateClearInstruction =
|
|
2808
|
+
const gateClearInstruction = buildQualityGateClearInstruction()
|
|
2394
2809
|
const gate = qualityGate
|
|
2395
2810
|
const dims = gate && gate.dimensions ? gate.dimensions : []
|
|
2396
2811
|
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
|
|
2397
2812
|
const status = gate && gate.overallStatus ? String(gate.overallStatus) : 'pending'
|
|
2398
2813
|
const statusLabel = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'PENDING'
|
|
2399
2814
|
const num = (v: number | undefined) => Number(v != null ? v : 0)
|
|
2815
|
+
const pulled = toolCalledInSession(session, 'iterate_quality_gate')
|
|
2400
2816
|
|
|
2401
2817
|
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2402
2818
|
React.createElement('b', {}, '质量门禁'),
|
|
2403
2819
|
React.createElement('button', {
|
|
2404
2820
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2405
2821
|
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2406
|
-
title: '复制 iterate_quality_gate
|
|
2822
|
+
title: '复制 iterate_quality_gate 查询指令(含 JSON 参数)',
|
|
2407
2823
|
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2408
2824
|
React.createElement('button', {
|
|
2409
2825
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
|
|
2410
2826
|
onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
|
|
2411
2827
|
title: '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)',
|
|
2412
2828
|
}, copiedKey === 'qgate-clear' ? '已复制' : '清除门禁'),
|
|
2829
|
+
// Gap #4: the browser cannot read `.iterate/` directly — one click
|
|
2830
|
+
// copies the pull instruction that makes the model echo it into the stream.
|
|
2831
|
+
React.createElement('button', {
|
|
2832
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot' ? '' : undefined,
|
|
2833
|
+
onClick: () => copyInstruction('disk-snapshot', pullInstruction),
|
|
2834
|
+
title: '复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript',
|
|
2835
|
+
}, copiedKey === 'disk-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
2413
2836
|
)
|
|
2414
2837
|
|
|
2415
2838
|
if (!hasGate) {
|
|
@@ -2421,13 +2844,25 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2421
2844
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2422
2845
|
),
|
|
2423
2846
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2424
|
-
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
2847
|
+
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
2848
|
+
// Gap #4: distinguish "we pulled it and it is empty" from "we
|
|
2849
|
+
// never asked the model to read the disk".
|
|
2850
|
+
diskEmptyStateText(pulled, '质量门禁证书'),
|
|
2851
|
+
),
|
|
2852
|
+
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
2853
|
+
'质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。',
|
|
2854
|
+
),
|
|
2425
2855
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2426
2856
|
React.createElement('button', {
|
|
2427
2857
|
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2428
2858
|
onClick: () => copyInstruction('qgate-instr', gateInstruction),
|
|
2429
2859
|
title: '复制查询指令',
|
|
2430
2860
|
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
2861
|
+
React.createElement('button', {
|
|
2862
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot-2' ? '' : undefined,
|
|
2863
|
+
onClick: () => copyInstruction('disk-snapshot-2', pullInstruction),
|
|
2864
|
+
title: '复制拉取磁盘快照指令',
|
|
2865
|
+
}, copiedKey === 'disk-snapshot-2' ? '已复制' : '拉取磁盘快照'),
|
|
2431
2866
|
),
|
|
2432
2867
|
),
|
|
2433
2868
|
),
|
|
@@ -2484,7 +2919,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2484
2919
|
const renderExperienceBank = () => {
|
|
2485
2920
|
const expInstruction = expSearch
|
|
2486
2921
|
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n${JSON.stringify({ operation: 'search', query: expSearch }, null, 2)}\n\`\`\``
|
|
2487
|
-
:
|
|
2922
|
+
: buildExperienceListInstruction()
|
|
2488
2923
|
|
|
2489
2924
|
const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
|
|
2490
2925
|
const query = expSearch.trim().toLowerCase()
|
|
@@ -2495,6 +2930,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2495
2930
|
.some((t) => String(t).toLowerCase().includes(query)),
|
|
2496
2931
|
)
|
|
2497
2932
|
: expEntries
|
|
2933
|
+
const pulled = toolCalledInSession(session, 'iterate_experience')
|
|
2498
2934
|
|
|
2499
2935
|
const resultRows = visible.map((e, i) => {
|
|
2500
2936
|
const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n${JSON.stringify({ operation: 'get', id: e.id }, null, 2)}\n\`\`\``
|
|
@@ -2525,9 +2961,14 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2525
2961
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${expEntries.length} 条`),
|
|
2526
2962
|
React.createElement('button', {
|
|
2527
2963
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2528
|
-
onClick: () => copyInstruction('exp-list',
|
|
2529
|
-
title: '
|
|
2964
|
+
onClick: () => copyInstruction('exp-list', buildExperienceListInstruction()),
|
|
2965
|
+
title: '复制列出经验指令(含 JSON 参数)',
|
|
2530
2966
|
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
2967
|
+
React.createElement('button', {
|
|
2968
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-snapshot' ? '' : undefined,
|
|
2969
|
+
onClick: () => copyInstruction('exp-snapshot', pullInstruction),
|
|
2970
|
+
title: '复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话',
|
|
2971
|
+
}, copiedKey === 'exp-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
2531
2972
|
),
|
|
2532
2973
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2533
2974
|
React.createElement('input', {
|
|
@@ -2549,7 +2990,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2549
2990
|
? React.createElement('div', {}, ...resultRows)
|
|
2550
2991
|
: expEntries.length > 0
|
|
2551
2992
|
? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
|
|
2552
|
-
: React.createElement('div', {
|
|
2993
|
+
: React.createElement('div', {},
|
|
2994
|
+
// Gap #4: separate "pulled and empty" from "never pulled".
|
|
2995
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, diskEmptyStateText(pulled, '经验银行')),
|
|
2996
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, '也可点「列出经验」查询磁盘上的全部历史经验。'),
|
|
2997
|
+
),
|
|
2553
2998
|
)
|
|
2554
2999
|
}
|
|
2555
3000
|
|
|
@@ -2560,7 +3005,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2560
3005
|
const renderDefenseEvents = () => {
|
|
2561
3006
|
const defenseInstruction = defenseFilter
|
|
2562
3007
|
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n${JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2)}\n\`\`\``
|
|
2563
|
-
:
|
|
3008
|
+
: buildDefenseEventsListInstruction()
|
|
2564
3009
|
|
|
2565
3010
|
const typeOptions = [
|
|
2566
3011
|
{ value: '', label: '全部类型' },
|
|
@@ -2588,13 +3033,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2588
3033
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${allEvents.length} 条 / 累计 ${totalCount}`),
|
|
2589
3034
|
React.createElement('button', {
|
|
2590
3035
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
2591
|
-
onClick: () => copyInstruction('defense-list',
|
|
2592
|
-
title: '
|
|
3036
|
+
onClick: () => copyInstruction('defense-list', buildDefenseEventsListInstruction()),
|
|
3037
|
+
title: '复制列出防御事件指令(含 JSON 参数)',
|
|
2593
3038
|
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
3039
|
+
React.createElement('button', {
|
|
3040
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-snapshot' ? '' : undefined,
|
|
3041
|
+
onClick: () => copyInstruction('defense-snapshot', pullInstruction),
|
|
3042
|
+
title: '复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话',
|
|
3043
|
+
}, copiedKey === 'defense-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
2594
3044
|
React.createElement('button', {
|
|
2595
3045
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
|
|
2596
|
-
onClick: () => copyInstruction('defense-counts',
|
|
2597
|
-
title: '
|
|
3046
|
+
onClick: () => copyInstruction('defense-counts', `请调用 \`iterate_defense_events\` 查询事件统计:\n\n\`\`\`json\n${JSON.stringify({ operation: 'counts' }, null, 2)}\n\`\`\``),
|
|
3047
|
+
title: '复制统计指令(含 JSON 参数)',
|
|
2598
3048
|
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
2599
3049
|
React.createElement('button', {
|
|
2600
3050
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'defense-clear' ? '' : undefined,
|
|
@@ -2612,7 +3062,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2612
3062
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2613
3063
|
),
|
|
2614
3064
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2615
|
-
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3065
|
+
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3066
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_defense_events'), '防御事件流'),
|
|
3067
|
+
),
|
|
3068
|
+
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3069
|
+
'防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。',
|
|
3070
|
+
),
|
|
2616
3071
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2617
3072
|
React.createElement('button', {
|
|
2618
3073
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
@@ -2771,6 +3226,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2771
3226
|
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
2772
3227
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2773
3228
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, headMeta || 'runtime'),
|
|
3229
|
+
// Gap #14: make the tab system discoverable without expanding it.
|
|
3230
|
+
React.createElement('span', {
|
|
3231
|
+
className: 'iterate-obs-head-meta',
|
|
3232
|
+
title: OBS_HOTKEY_HINT,
|
|
3233
|
+
}, OBS_HOTKEY_HINT),
|
|
2774
3234
|
React.createElement('button', {
|
|
2775
3235
|
className: 'iterate-btn', 'data-ghost': '',
|
|
2776
3236
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setOpen((v) => !v) },
|
|
@@ -2800,15 +3260,26 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2800
3260
|
onClick: exportObservatory,
|
|
2801
3261
|
title: '将观测台全部数据导出为 JSON(优先下载,失败则复制)',
|
|
2802
3262
|
}, copiedKey === 'export' ? '已导出' : '导出 JSON'),
|
|
3263
|
+
// Gap #4: `.iterate/` artifacts are unreachable from the browser;
|
|
3264
|
+
// this copies the one instruction that makes the model echo them
|
|
3265
|
+
// into the session stream (which is what every panel scans).
|
|
3266
|
+
React.createElement('button', {
|
|
3267
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
3268
|
+
onClick: () => copyInstruction('pull-snapshot', pullInstruction),
|
|
3269
|
+
title: pullTitle,
|
|
3270
|
+
}, copiedKey === 'pull-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
2803
3271
|
),
|
|
2804
|
-
React.createElement('div', { className: 'iterate-obs-tabs' },
|
|
3272
|
+
React.createElement('div', { className: 'iterate-obs-tabs', role: 'tablist', 'aria-label': '观测台页签(快捷键 1-9 / 0 / -)' },
|
|
2805
3273
|
...OBS_TABS.map((t) =>
|
|
2806
3274
|
React.createElement('button', {
|
|
2807
3275
|
key: t.key,
|
|
2808
3276
|
className: 'iterate-obs-tab',
|
|
3277
|
+
role: 'tab',
|
|
3278
|
+
'aria-selected': tab === t.key,
|
|
2809
3279
|
'data-active': tab === t.key ? '' : undefined,
|
|
2810
3280
|
onClick: () => setTab(t.key),
|
|
2811
|
-
|
|
3281
|
+
title: t.hotkey ? `${obsTabLabel(t)}(按 ${t.hotkey} 切换)` : `${obsTabLabel(t)}(按 - 切换)`,
|
|
3282
|
+
}, obsTabLabel(t)),
|
|
2812
3283
|
),
|
|
2813
3284
|
),
|
|
2814
3285
|
React.createElement('div', { className: 'iterate-obs-body' }, renderBody()),
|