iterate-plugin 3.5.7 → 3.7.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 +97 -22
- package/README.zh-CN.md +79 -19
- package/dist/approval-gate.js +143 -23
- package/dist/atomic-fs.js +148 -17
- package/dist/config-loader.js +516 -35
- package/dist/config-write.js +83 -22
- package/dist/evidence.js +141 -87
- package/dist/file-lock.js +193 -0
- package/dist/index.js +61 -28
- package/dist/jobs.js +56 -11
- package/dist/live.js +54 -14
- 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 +61 -14
- package/dist/skill-prompt.js +396 -66
- 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 +106 -28
- 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 +150 -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/dist/working-directory.js +114 -0
- package/lib/client.js +1912 -512
- package/lib/parse.js +1001 -103
- package/package.json +16 -9
- package/src/approval-gate.ts +160 -23
- package/src/atomic-fs.ts +152 -17
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +1031 -369
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +549 -36
- package/src/config-write.ts +108 -26
- package/src/evidence.ts +194 -92
- package/src/file-lock.ts +211 -0
- package/src/index.ts +60 -29
- package/src/jobs.ts +63 -12
- package/src/live.ts +50 -12
- 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 +60 -13
- package/src/skill-prompt.ts +396 -66
- 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 +114 -23
- 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 +154 -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/working-directory.ts +126 -0
package/src/client/index.ts
CHANGED
|
@@ -82,7 +82,45 @@ 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
|
+
buildConfigReadInstruction, // #8b: read instruction (write -> read-back loop)
|
|
107
|
+
scanSessionForConfigRead, // #8b: latest config echoed into the session
|
|
108
|
+
configValueByPath, // #8b: current value of the selected dotted key
|
|
109
|
+
DISK_SNAPSHOT_SOURCES, // #4/#3: read-side tools listed by the pull instruction
|
|
85
110
|
} from '../../lib/parse.js'
|
|
111
|
+
import {
|
|
112
|
+
configureLangStorage,
|
|
113
|
+
detectLang,
|
|
114
|
+
getLang,
|
|
115
|
+
setLang,
|
|
116
|
+
subscribeLang,
|
|
117
|
+
t,
|
|
118
|
+
tf,
|
|
119
|
+
LANG_NAMES,
|
|
120
|
+
LANG_STORAGE_KEY,
|
|
121
|
+
type Lang,
|
|
122
|
+
} from './i18n.ts'
|
|
123
|
+
import { noteSession, readSession, subscribeSession } from './session-cache.ts'
|
|
86
124
|
|
|
87
125
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
88
126
|
|
|
@@ -247,6 +285,17 @@ interface ObsManifest {
|
|
|
247
285
|
approval?: ObsApproval
|
|
248
286
|
/** Why the run ended: converged / max_rounds_reached / aborted_by_validation. */
|
|
249
287
|
stoppedReason?: string | null
|
|
288
|
+
/** Gap #6: per-round validation rows persisted by `iterate_transcript capture`. */
|
|
289
|
+
validations?: ObsValidation[]
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** One `iterate_validate` outcome for a round (gap #6 contract). */
|
|
293
|
+
interface ObsValidation {
|
|
294
|
+
round?: number
|
|
295
|
+
command?: string
|
|
296
|
+
exitCode?: number | null
|
|
297
|
+
allowed?: boolean
|
|
298
|
+
rejectReason?: string
|
|
250
299
|
}
|
|
251
300
|
|
|
252
301
|
// ─── Quality command center data types (v3.1+; mirrors src/types.ts) ────────
|
|
@@ -426,6 +475,7 @@ const ITERATE_CSS = `
|
|
|
426
475
|
}
|
|
427
476
|
.iterate-btn[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
428
477
|
.iterate-btn[data-copied] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
|
|
478
|
+
.iterate-btn[data-failed], .iterate-cmd[data-failed] { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
|
|
429
479
|
.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
480
|
|
|
431
481
|
.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 +660,16 @@ const ITERATE_CSS = `
|
|
|
610
660
|
.iterate-obs-block-head[data-click] { cursor: pointer; }
|
|
611
661
|
.iterate-obs-block-body { padding: 8px 10px; }
|
|
612
662
|
.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; }
|
|
663
|
+
/* Gap #6: validation / gate status colors (pass=green, fail=red). */
|
|
664
|
+
.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); }
|
|
665
|
+
.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); }
|
|
666
|
+
.iterate-obs-row[data-status="fail"] { color: var(--dsw-alias-state-error-primary); }
|
|
667
|
+
.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; }
|
|
668
|
+
/* Gap #13: cross-round comparison table. */
|
|
669
|
+
.iterate-obs-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
670
|
+
.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; }
|
|
671
|
+
.iterate-obs-table th { color: var(--dsw-alias-label-secondary); font-weight: 600; }
|
|
672
|
+
.iterate-obs-table td { color: var(--dsw-alias-label-primary); }
|
|
613
673
|
.iterate-obs-msg { padding: 2px 0; color: var(--dsw-alias-label-secondary); line-height: 1.5; font-size: 11px; word-break: break-word; }
|
|
614
674
|
.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
675
|
.iterate-obs-bar { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); flex-wrap: wrap; }
|
|
@@ -682,6 +742,37 @@ function removeStorageByPrefix(prefix: string): number {
|
|
|
682
742
|
return removed
|
|
683
743
|
}
|
|
684
744
|
|
|
745
|
+
// ─── Language (bilingual UI) ─────────────────────────────────────────────────
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Subscribe a component to language changes. Components that render `t()` /
|
|
749
|
+
* `tf()` must call this so a language switch repaints them — the slot
|
|
750
|
+
* wrappers registered in `apply()` apply it once per slot, so no panel can be
|
|
751
|
+
* forgotten.
|
|
752
|
+
*/
|
|
753
|
+
function useLang(): Lang {
|
|
754
|
+
const [lang, set] = React.useState<Lang>(getLang())
|
|
755
|
+
React.useEffect(() => subscribeLang(set), [])
|
|
756
|
+
return lang
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Newest session published by the session-scoped slots (session-cache.ts).
|
|
761
|
+
* `useSyncExternalStore` keeps a root-scoped panel consistent with the store
|
|
762
|
+
* even when a session lands between render and effect.
|
|
763
|
+
*/
|
|
764
|
+
function useSessionSnapshot(): unknown {
|
|
765
|
+
return React.useSyncExternalStore(subscribeSession, readSession, readSession)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Wrap a slot component so its whole subtree re-renders on a language change. */
|
|
769
|
+
function withLang<P extends object>(Component: React.ComponentType<P>): React.ComponentType<P> {
|
|
770
|
+
return function LangScoped(props: P) {
|
|
771
|
+
useLang()
|
|
772
|
+
return React.createElement(Component, props)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
685
776
|
/** Copy text to the clipboard, resolving to whether it actually succeeded. */
|
|
686
777
|
function copyText(text: string): Promise<boolean> {
|
|
687
778
|
if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
@@ -738,11 +829,28 @@ function severityLabel(severity: string | undefined): string {
|
|
|
738
829
|
return SEVERITY_LABEL[coerceSeverity(severity)]
|
|
739
830
|
}
|
|
740
831
|
|
|
832
|
+
/** Render a config value read back from the session (arrays stay inline JSON). */
|
|
833
|
+
function formatConfigValue(value: unknown): string {
|
|
834
|
+
if (typeof value === 'string') return value
|
|
835
|
+
if (value === undefined) return '—'
|
|
836
|
+
if (value === null) return 'null'
|
|
837
|
+
try {
|
|
838
|
+
return JSON.stringify(value) ?? String(value)
|
|
839
|
+
} catch {
|
|
840
|
+
return String(value)
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** Wrap a badge in full-width (zh) or ASCII (en) parentheses. */
|
|
845
|
+
function parenthesize(label: string): string {
|
|
846
|
+
return getLang() === 'en' ? ` (${label})` : `(${label})`
|
|
847
|
+
}
|
|
848
|
+
|
|
741
849
|
/** Badge text for the run's live/finished state, annotated with the stop reason. */
|
|
742
850
|
function runStatusText(live: boolean, manifest: ObsManifest | null | undefined): string {
|
|
743
|
-
if (live) return '运行中'
|
|
851
|
+
if (live) return t('运行中')
|
|
744
852
|
const reason = manifest && typeof manifest.stoppedReason === 'string' ? manifest.stoppedReason : ''
|
|
745
|
-
return reason ? stoppedReasonLabel(reason) : '已结束'
|
|
853
|
+
return reason ? stoppedReasonLabel(reason, getLang()) : t('已结束')
|
|
746
854
|
}
|
|
747
855
|
|
|
748
856
|
/** Resolve the slots service: prefer the typed `ctx.slots`, fall back to `ctx.get`. */
|
|
@@ -858,7 +966,7 @@ function TrendChart({ points }: { points: Array<{ round: number; count: number }
|
|
|
858
966
|
key: p.round,
|
|
859
967
|
className: 'iterate-trend-bar',
|
|
860
968
|
'data-hot': p.count > 0 ? '' : undefined,
|
|
861
|
-
title: `Round
|
|
969
|
+
title: tf(`Round {a0}:{a1} 项`, { a0: p.round, a1: p.count }),
|
|
862
970
|
style: { height: `${Math.max(4, Math.round((p.count / max) * 24))}px` },
|
|
863
971
|
}),
|
|
864
972
|
)
|
|
@@ -868,20 +976,42 @@ function TrendChart({ points }: { points: Array<{ round: number; count: number }
|
|
|
868
976
|
return React.createElement('div', {
|
|
869
977
|
className: 'iterate-trend',
|
|
870
978
|
role: 'img',
|
|
871
|
-
'aria-label':
|
|
979
|
+
'aria-label': tf(`各轮发现数量趋势:{a0}`, { a0: summary }),
|
|
872
980
|
}, ...bars)
|
|
873
981
|
}
|
|
874
982
|
|
|
875
983
|
/** Start-iteration launcher shown on the empty dashboard: copies a runnable
|
|
876
|
-
*
|
|
877
|
-
* follows the panel's copy-to-command pattern).
|
|
984
|
+
* instruction (the browser cannot invoke harness tools directly, so every
|
|
985
|
+
* action follows the panel's copy-to-command pattern).
|
|
986
|
+
*
|
|
987
|
+
* Gap #1: the plugin registers NO dsh command (`src/index.ts` only registers
|
|
988
|
+
* 17 tools + a system prompt), so `/iterate` never existed — the copied text
|
|
989
|
+
* is now a natural-language instruction from `START_INSTRUCTIONS`, which the
|
|
990
|
+
* model resolves through the `workflow` tool exactly as the skill prompt
|
|
991
|
+
* teaches. Label/tooltip say "指令", never "命令". */
|
|
878
992
|
function StartIterationButton() {
|
|
879
993
|
const [copied, setCopied] = React.useState<string | null>(null)
|
|
994
|
+
const [failed, setFailed] = React.useState<string | null>(null)
|
|
995
|
+
const timers = React.useRef<number[]>([])
|
|
996
|
+
React.useEffect(
|
|
997
|
+
() => () => {
|
|
998
|
+
for (const t of timers.current) clearTimeout(t)
|
|
999
|
+
timers.current = []
|
|
1000
|
+
},
|
|
1001
|
+
[],
|
|
1002
|
+
)
|
|
880
1003
|
const copy = (key: string, text: string) => {
|
|
881
1004
|
copyText(text).then((ok) => {
|
|
882
1005
|
if (ok) {
|
|
1006
|
+
setFailed(null)
|
|
883
1007
|
setCopied(key)
|
|
884
|
-
setTimeout(() => setCopied((cur) => (cur === key ? null : cur)), 1600)
|
|
1008
|
+
const t = window.setTimeout(() => setCopied((cur) => (cur === key ? null : cur)), 1600)
|
|
1009
|
+
timers.current.push(t)
|
|
1010
|
+
} else {
|
|
1011
|
+
// Copy failure must be visible, not silent — otherwise the button
|
|
1012
|
+
// looks like a no-op and the user never learns the instruction text.
|
|
1013
|
+
setCopied(null)
|
|
1014
|
+
setFailed(key)
|
|
885
1015
|
}
|
|
886
1016
|
})
|
|
887
1017
|
}
|
|
@@ -891,14 +1021,15 @@ function StartIterationButton() {
|
|
|
891
1021
|
className: 'iterate-cmd',
|
|
892
1022
|
'data-primary': primary ? '' : undefined,
|
|
893
1023
|
'data-copied': copied === key ? '' : undefined,
|
|
1024
|
+
'data-failed': failed === key ? '' : undefined,
|
|
894
1025
|
onClick: () => copy(key, command),
|
|
895
1026
|
title,
|
|
896
|
-
}, copied === key ? '已复制' : label)
|
|
1027
|
+
}, copied === key ? t('已复制') : failed === key ? t('复制失败') : label)
|
|
897
1028
|
return React.createElement(
|
|
898
1029
|
'span',
|
|
899
1030
|
{ className: 'iterate-dashboard-launch' },
|
|
900
|
-
btn('start-full', '完整迭代',
|
|
901
|
-
btn('start-review', '仅评审',
|
|
1031
|
+
btn('start-full', t('完整迭代'), START_INSTRUCTIONS.full, true, t('复制启动指令:完整「审查 → 修复 → 验证 → 复盘」闭环(粘贴给模型即可)')),
|
|
1032
|
+
btn('start-review', t('仅评审'), START_INSTRUCTIONS.reviewOnly, false, t('复制启动指令:只审查不修改(dry-run)(粘贴给模型即可)')),
|
|
902
1033
|
)
|
|
903
1034
|
}
|
|
904
1035
|
|
|
@@ -915,6 +1046,12 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
915
1046
|
// The `conversation.input.dock` slot owner share (InputZone) provides
|
|
916
1047
|
// `session` as a point-in-time snapshot — read it directly, never subscribe.
|
|
917
1048
|
const session = props && props.session ? props.session : null
|
|
1049
|
+
// Publish it for ROOT-scoped panels (the settings card reads the config
|
|
1050
|
+
// read-back through this cache). In an effect, not during render: notifying
|
|
1051
|
+
// subscribers while another component is rendering is a React error.
|
|
1052
|
+
React.useEffect(() => {
|
|
1053
|
+
noteSession(session)
|
|
1054
|
+
}, [session])
|
|
918
1055
|
const report = latestReport(session)
|
|
919
1056
|
|
|
920
1057
|
React.useEffect(() => {
|
|
@@ -926,15 +1063,47 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
926
1063
|
}, [report && hashReport(report) + ':' + getCurrentRound(report)])
|
|
927
1064
|
|
|
928
1065
|
if (!report) {
|
|
1066
|
+
// Gap #5: split the old single empty state in two. A run that is already
|
|
1067
|
+
// in flight has no aggregated report yet (round 1 is still in
|
|
1068
|
+
// review → fix → validate), but showing "运行一次评审后…" + a start button
|
|
1069
|
+
// mid-run tells the user nothing is happening — and invites a second
|
|
1070
|
+
// concurrent start. The transcript alone carries round + phase.
|
|
1071
|
+
const runState = dashboardRunState(latestTranscript(session))
|
|
1072
|
+
if (runState.state === 'running') {
|
|
1073
|
+
return React.createElement(
|
|
1074
|
+
'div',
|
|
1075
|
+
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'running', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1076
|
+
React.createElement('span', { className: 'iterate-round-badge', 'data-live': '' }, t('运行中')),
|
|
1077
|
+
React.createElement(
|
|
1078
|
+
'span',
|
|
1079
|
+
{ className: 'iterate-empty-hint' },
|
|
1080
|
+
tf(`第 {a0} 轮进行中{a1}——首轮汇总报告落地后,这里会显示收敛进度与发现统计。`, { a0: String(runState.round), a1: runState.phase ? tf(` · 阶段「{a0}」`, { a0: runState.phase }) : '' }),
|
|
1081
|
+
),
|
|
1082
|
+
// No start button while a run is active.
|
|
1083
|
+
)
|
|
1084
|
+
}
|
|
1085
|
+
if (runState.state === 'done') {
|
|
1086
|
+
return React.createElement(
|
|
1087
|
+
'div',
|
|
1088
|
+
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'done', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1089
|
+
React.createElement('span', { className: 'iterate-round-badge' }, t('已结束')),
|
|
1090
|
+
React.createElement(
|
|
1091
|
+
'span',
|
|
1092
|
+
{ className: 'iterate-empty-hint' },
|
|
1093
|
+
tf(`上次迭代在第 {a0} 轮后停止{a1},但没有可展示的汇总报告。`, { a0: String(runState.round), a1: runState.stoppedReason ? parenthesize(stoppedReasonLabel(runState.stoppedReason, getLang())) : '' }),
|
|
1094
|
+
),
|
|
1095
|
+
React.createElement(StartIterationButton, null),
|
|
1096
|
+
)
|
|
1097
|
+
}
|
|
929
1098
|
// 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
|
-
//
|
|
1099
|
+
// no idea the plugin exists or how to start. Provide a one-click copy of
|
|
1100
|
+
// the startup instruction (the browser cannot call harness tools
|
|
1101
|
+
// directly, and dsh exposes no command API to plugins).
|
|
933
1102
|
return React.createElement(
|
|
934
1103
|
'div',
|
|
935
|
-
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1104
|
+
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'empty', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
936
1105
|
React.createElement('span', { className: 'iterate-round-badge' }, 'iterate'),
|
|
937
|
-
React.createElement('span', { className: 'iterate-empty-hint' }, '运行一次评审后,这里会显示收敛进度与发现统计。'),
|
|
1106
|
+
React.createElement('span', { className: 'iterate-empty-hint' }, t('运行一次评审后,这里会显示收敛进度与发现统计。')),
|
|
938
1107
|
React.createElement(StartIterationButton, null),
|
|
939
1108
|
)
|
|
940
1109
|
}
|
|
@@ -954,15 +1123,15 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
954
1123
|
? React.createElement('span', {
|
|
955
1124
|
className: 'iterate-chip-resume',
|
|
956
1125
|
key: 'resume',
|
|
957
|
-
title: '本次迭代从上一次中断的断点继续执行',
|
|
958
|
-
}, `已中断恢复
|
|
1126
|
+
title: t('本次迭代从上一次中断的断点继续执行'),
|
|
1127
|
+
}, tf(`已中断恢复 ×{a0}`, { a0: String(resumeCount) }))
|
|
959
1128
|
: null
|
|
960
1129
|
const imageChip = imageCount > 0
|
|
961
1130
|
? React.createElement('span', {
|
|
962
1131
|
className: 'iterate-chip-images',
|
|
963
1132
|
key: 'images',
|
|
964
|
-
title: '会话中检测到用户附带的图片,评审将作为视觉证据参考',
|
|
965
|
-
}, `附件图片
|
|
1133
|
+
title: t('会话中检测到用户附带的图片,评审将作为视觉证据参考'),
|
|
1134
|
+
}, tf(`附件图片 {a0}`, { a0: String(imageCount) }))
|
|
966
1135
|
: null
|
|
967
1136
|
|
|
968
1137
|
// Live run-state awareness: surface the transcript's current workflow phase
|
|
@@ -976,8 +1145,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
976
1145
|
key: 'phase',
|
|
977
1146
|
'data-live': transcript.active === true ? '' : undefined,
|
|
978
1147
|
title: transcript.active === true
|
|
979
|
-
?
|
|
980
|
-
:
|
|
1148
|
+
? tf(`当前处于「{a0}」阶段(运行中)`, { a0: phase })
|
|
1149
|
+
: tf(`最近一次执行停留在「{a0}」阶段(已结束)`, { a0: phase }),
|
|
981
1150
|
}, `${phase} · ${runStatusText(transcript.active === true, transcript)}`)
|
|
982
1151
|
: null
|
|
983
1152
|
|
|
@@ -990,7 +1159,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
990
1159
|
className: 'iterate-chip-taskmode',
|
|
991
1160
|
key: 'taskmode',
|
|
992
1161
|
'data-mode': taskMode,
|
|
993
|
-
title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
|
|
1162
|
+
title: taskMode === 'code' ? t('代码模式:直接修改文件') : t('迭代模式:审查-修复循环'),
|
|
994
1163
|
}, taskMode === 'code' ? '● code' : '● iterate')
|
|
995
1164
|
: null
|
|
996
1165
|
|
|
@@ -1009,7 +1178,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1009
1178
|
React.createElement(
|
|
1010
1179
|
'span',
|
|
1011
1180
|
{ key: '+more', className: 'iterate-dim-badge iterate-dim-more', title: dimNames.slice(6).join(', ') },
|
|
1012
|
-
|
|
1181
|
+
tf(`+{a0} 更多`, { a0: overflow }),
|
|
1013
1182
|
),
|
|
1014
1183
|
)
|
|
1015
1184
|
}
|
|
@@ -1024,7 +1193,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1024
1193
|
? React.createElement('span', {
|
|
1025
1194
|
className: 'iterate-metric',
|
|
1026
1195
|
key: 'fixes',
|
|
1027
|
-
title: '本轮已应用的原子修复数(正常模式)',
|
|
1196
|
+
title: t('本轮已应用的原子修复数(正常模式)'),
|
|
1028
1197
|
}, `${String(fixCount)} fixes`)
|
|
1029
1198
|
: null
|
|
1030
1199
|
|
|
@@ -1035,8 +1204,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1035
1204
|
? React.createElement('span', {
|
|
1036
1205
|
className: 'iterate-chip-resume',
|
|
1037
1206
|
key: 'converged',
|
|
1038
|
-
title: '审查已收敛:最后一轮未发现新问题',
|
|
1039
|
-
}, '✓ 已收敛')
|
|
1207
|
+
title: t('审查已收敛:最后一轮未发现新问题'),
|
|
1208
|
+
}, t('✓ 已收敛'))
|
|
1040
1209
|
: null
|
|
1041
1210
|
|
|
1042
1211
|
const sevMetric = (key: 'critical' | 'high' | 'medium' | 'low', label: string) =>
|
|
@@ -1104,14 +1273,14 @@ function StatsCard(props: SlotProps) {
|
|
|
1104
1273
|
React.createElement('td', { style: { color: SEVERITY_COLOR.low } }, String(h.low)),
|
|
1105
1274
|
),
|
|
1106
1275
|
)
|
|
1107
|
-
const trendLine = `首轮
|
|
1108
|
-
const completion = buildCompletionSummary(report)
|
|
1276
|
+
const trendLine = tf(`首轮 {a0} → 末轮 {a1} 项,降幅 {a2}%{a3}`, { a0: trend.firstRound, a1: trend.lastRound, a2: trend.reductionPercent, a3: trend.converged ? t(',已收敛') : '' })
|
|
1277
|
+
const completion = buildCompletionSummary(report, getLang())
|
|
1109
1278
|
|
|
1110
1279
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'stats', className: 'iterate-stats' },
|
|
1111
1280
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
1112
|
-
React.createElement('span', {}, 'Iterate · 收敛统计'),
|
|
1281
|
+
React.createElement('span', {}, t('Iterate · 收敛统计')),
|
|
1113
1282
|
React.createElement('span', { className: 'iterate-triage-hint' },
|
|
1114
|
-
`Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? '已收敛' : '未收敛'}`,
|
|
1283
|
+
`Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? t('已收敛') : t('未收敛')}`,
|
|
1115
1284
|
),
|
|
1116
1285
|
),
|
|
1117
1286
|
React.createElement('div', { className: 'iterate-stats-grid' }, ...rows),
|
|
@@ -1122,15 +1291,15 @@ function StatsCard(props: SlotProps) {
|
|
|
1122
1291
|
className: 'iterate-btn',
|
|
1123
1292
|
'data-primary': showHistory ? '' : undefined,
|
|
1124
1293
|
onClick: () => setShowHistory((v) => !v),
|
|
1125
|
-
}, showHistory ? '收起历史' : '历史 / 趋势'),
|
|
1294
|
+
}, showHistory ? t('收起历史') : t('历史 / 趋势')),
|
|
1126
1295
|
),
|
|
1127
1296
|
showHistory
|
|
1128
1297
|
? React.createElement('div', { className: 'iterate-history-body' },
|
|
1129
1298
|
React.createElement('table', { className: 'iterate-history-table' },
|
|
1130
1299
|
React.createElement('thead', {},
|
|
1131
1300
|
React.createElement('tr', {},
|
|
1132
|
-
React.createElement('th', {}, '轮次'),
|
|
1133
|
-
React.createElement('th', {}, '发现'),
|
|
1301
|
+
React.createElement('th', {}, t('轮次')),
|
|
1302
|
+
React.createElement('th', {}, t('发现')),
|
|
1134
1303
|
React.createElement('th', { style: { color: SEVERITY_COLOR.critical } }, 'CRIT'),
|
|
1135
1304
|
React.createElement('th', { style: { color: SEVERITY_COLOR.high } }, 'HIGH'),
|
|
1136
1305
|
React.createElement('th', { style: { color: SEVERITY_COLOR.medium } }, 'MED'),
|
|
@@ -1176,6 +1345,31 @@ function TriagePanel(props: SlotProps) {
|
|
|
1176
1345
|
const [selectAll, setSelectAll] = React.useState(false)
|
|
1177
1346
|
// v3.0: Command mode for native action buttons
|
|
1178
1347
|
const [cmdCopied, setCmdCopied] = React.useState<string | null>(null)
|
|
1348
|
+
const [cmdFailed, setCmdFailed] = React.useState<string | null>(null)
|
|
1349
|
+
// Audit: pending copy-reset timers must not outlive the panel (a unmounted
|
|
1350
|
+
// setState is a React warning / leaked timer).
|
|
1351
|
+
const cmdTimers = React.useRef<number[]>([])
|
|
1352
|
+
React.useEffect(() => () => {
|
|
1353
|
+
for (const t of cmdTimers.current) clearTimeout(t)
|
|
1354
|
+
cmdTimers.current = []
|
|
1355
|
+
}, [])
|
|
1356
|
+
|
|
1357
|
+
/** Copy + flip `cmdCopied` for 1.6s, or surface `cmdFailed` on clipboard
|
|
1358
|
+
* denial so the button never looks like a dead control. */
|
|
1359
|
+
const copyWithFeedback = (text: string, key: string) => {
|
|
1360
|
+
copyText(text).then((ok) => {
|
|
1361
|
+
if (ok) {
|
|
1362
|
+
setCmdFailed(null)
|
|
1363
|
+
setCmdCopied(key)
|
|
1364
|
+
cmdTimers.current.push(window.setTimeout(() => setCmdCopied((cur) => (cur === key ? null : cur)), 1600))
|
|
1365
|
+
} else {
|
|
1366
|
+
setCmdCopied(null)
|
|
1367
|
+
setCmdFailed(key)
|
|
1368
|
+
// Fall back to the inline payload box so the text stays copyable by hand.
|
|
1369
|
+
setPayload(text)
|
|
1370
|
+
}
|
|
1371
|
+
})
|
|
1372
|
+
}
|
|
1179
1373
|
|
|
1180
1374
|
/** Persist the verdicts to localStorage whenever they change. Kept OUT of
|
|
1181
1375
|
* the state updater (updaters must stay pure — React may double-invoke them
|
|
@@ -1263,66 +1457,41 @@ function TriagePanel(props: SlotProps) {
|
|
|
1263
1457
|
|
|
1264
1458
|
// v3.0: Command handlers for native action buttons
|
|
1265
1459
|
const doApproveArchitecturalFix = () => {
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1274
|
-
}
|
|
1275
|
-
})
|
|
1460
|
+
// Gap #10: `force: true` exists on `iterate_fix`, but the tool still
|
|
1461
|
+
// requires file / content / finding / round — the old copy text implied a
|
|
1462
|
+
// lone force flag was enough. The builder names all four and marks the two
|
|
1463
|
+
// the model must fill in.
|
|
1464
|
+
const cmd = buildArchitecturalFixInstruction(findings as IterateFinding[])
|
|
1465
|
+
if (!cmd) return
|
|
1466
|
+
copyWithFeedback(cmd, 'approve-arch')
|
|
1276
1467
|
}
|
|
1277
1468
|
|
|
1278
1469
|
const doTriggerNewRound = () => {
|
|
1279
|
-
const cmd = '请继续执行下一轮迭代审查'
|
|
1280
|
-
|
|
1281
|
-
if (ok) {
|
|
1282
|
-
setCmdCopied('new-round')
|
|
1283
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1284
|
-
}
|
|
1285
|
-
})
|
|
1470
|
+
const cmd = t('请继续执行下一轮迭代审查')
|
|
1471
|
+
copyWithFeedback(cmd, 'new-round')
|
|
1286
1472
|
}
|
|
1287
1473
|
|
|
1288
1474
|
const doRollbackToCheckpoint = () => {
|
|
1289
1475
|
// iterate_checkpoint has NO rollback operation (save/load/resume/clear
|
|
1290
1476
|
// only); reverting applied fixes is done via iterate_rollback by fix id,
|
|
1291
1477
|
// so the paste-able instruction must point there instead.
|
|
1292
|
-
const cmd = '请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复'
|
|
1293
|
-
|
|
1294
|
-
if (ok) {
|
|
1295
|
-
setCmdCopied('rollback')
|
|
1296
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1297
|
-
}
|
|
1298
|
-
})
|
|
1478
|
+
const cmd = t('请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复')
|
|
1479
|
+
copyWithFeedback(cmd, 'rollback')
|
|
1299
1480
|
}
|
|
1300
1481
|
|
|
1301
1482
|
// v3.1+ §8: assign the currently scoped findings to the model for fixing.
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1483
|
+
// Gap #10: `iterate_fix` takes ONE finding + required `content` (full new
|
|
1484
|
+
// file) + required `round` — never an array — so the payload now says so
|
|
1485
|
+
// explicitly instead of handing the model a raw array to translate.
|
|
1305
1486
|
const doAssignFindings = () => {
|
|
1306
1487
|
const targetAll = selectAll
|
|
1307
1488
|
const scopeIndices = targetAll ? allIndices : indices
|
|
1308
1489
|
const scopeFindings = (findings as IterateFinding[])
|
|
1309
1490
|
.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
1491
|
if (scopeFindings.length === 0) return
|
|
1319
|
-
const cmd =
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
setCmdCopied('assign')
|
|
1323
|
-
setTimeout(() => setCmdCopied(null), 1600)
|
|
1324
|
-
}
|
|
1325
|
-
})
|
|
1492
|
+
const cmd = buildAssignFixesInstruction(scopeFindings)
|
|
1493
|
+
if (!cmd) return
|
|
1494
|
+
copyWithFeedback(cmd, 'assign')
|
|
1326
1495
|
}
|
|
1327
1496
|
|
|
1328
1497
|
const doCopyYaml = () => {
|
|
@@ -1331,23 +1500,45 @@ function TriagePanel(props: SlotProps) {
|
|
|
1331
1500
|
copyText(yaml).then((ok) => {
|
|
1332
1501
|
if (ok) {
|
|
1333
1502
|
setCopied(true)
|
|
1334
|
-
setTimeout(() => setCopied(false), 1600)
|
|
1503
|
+
cmdTimers.current.push(window.setTimeout(() => setCopied(false), 1600))
|
|
1335
1504
|
} else {
|
|
1336
1505
|
// Copy failed (permissions/unsupported) — reveal the payload as a
|
|
1337
1506
|
// manual-copy fallback instead of claiming success.
|
|
1338
1507
|
setPayload(yaml)
|
|
1508
|
+
setCmdFailed('yaml')
|
|
1339
1509
|
}
|
|
1340
1510
|
})
|
|
1341
1511
|
}
|
|
1342
1512
|
|
|
1513
|
+
// Gap #7: the SAFE path is `iterate_triage apply` (validation, dedupe,
|
|
1514
|
+
// backup, automatic rollback) — so it is now the primary action, and it
|
|
1515
|
+
// tells the user what happens after they paste.
|
|
1343
1516
|
const doBuildInstruction = () => {
|
|
1344
1517
|
const text = buildApplyInstruction(ignored)
|
|
1518
|
+
if (!text) return
|
|
1519
|
+
setPayload(text)
|
|
1520
|
+
copyWithFeedback(text, 'apply')
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Gap #7: closing the write-back loop — copying is not writing, so offer a
|
|
1524
|
+
// readback instruction (`iterate_triage list`) to verify what landed.
|
|
1525
|
+
const doReadback = () => {
|
|
1526
|
+
const text = buildTriageReadbackInstruction()
|
|
1345
1527
|
setPayload(text)
|
|
1346
|
-
|
|
1528
|
+
copyWithFeedback(text, 'readback')
|
|
1347
1529
|
}
|
|
1348
1530
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1531
|
+
// Audit: `filtered` and `indices` are produced together, but an
|
|
1532
|
+
// out-of-range slot would otherwise render a row keyed `undefined` whose
|
|
1533
|
+
// verdict writes to `verdicts["undefined"]`. Pair them up-front so the
|
|
1534
|
+
// mapping index can never drift from the original findings index.
|
|
1535
|
+
const rowPairs: Array<{ index: number; finding: IterateFinding }> = []
|
|
1536
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
1537
|
+
const idx = indices[i]
|
|
1538
|
+
const f = filtered[i]
|
|
1539
|
+
if (typeof idx === 'number' && f) rowPairs.push({ index: idx, finding: f as IterateFinding })
|
|
1540
|
+
}
|
|
1541
|
+
const rows = rowPairs.map(({ index, finding }) => {
|
|
1351
1542
|
const severity = finding.severity || 'low'
|
|
1352
1543
|
const verdict = verdicts[String(index)] || 'keep'
|
|
1353
1544
|
const isSelected = selected === index
|
|
@@ -1386,27 +1577,27 @@ function TriagePanel(props: SlotProps) {
|
|
|
1386
1577
|
),
|
|
1387
1578
|
React.createElement('div', { className: 'iterate-finding-summary' }, String(finding.summary || '')),
|
|
1388
1579
|
React.createElement('div', { className: 'iterate-finding-actions' },
|
|
1389
|
-
btn('y 修复', 'keep', '保留该 finding,进入修复'),
|
|
1390
|
-
btn('n 跳过', 'skip', '跳过该 finding'),
|
|
1391
|
-
btn('a 已知有意', 'ignore', '标记为已知有意并写入 known_intentional'),
|
|
1580
|
+
btn(t('y 修复'), 'keep', t('保留该 finding,进入修复')),
|
|
1581
|
+
btn(t('n 跳过'), 'skip', t('跳过该 finding')),
|
|
1582
|
+
btn(t('a 已知有意'), 'ignore', t('标记为已知有意并写入 known_intentional')),
|
|
1392
1583
|
),
|
|
1393
1584
|
)
|
|
1394
1585
|
})
|
|
1395
1586
|
|
|
1396
1587
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
1397
1588
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
1398
|
-
React.createElement('span', { role: 'heading', 'aria-level': 3 }, `Iterate · Findings 分诊 (
|
|
1399
|
-
React.createElement('span', { className: 'iterate-triage-hint' }, 'y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择'),
|
|
1589
|
+
React.createElement('span', { role: 'heading', 'aria-level': 3 }, tf(`Iterate · Findings 分诊 ({a0}/{a1})`, { a0: filtered.length, a1: findings.length })),
|
|
1590
|
+
React.createElement('span', { className: 'iterate-triage-hint' }, t('y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择')),
|
|
1400
1591
|
),
|
|
1401
1592
|
React.createElement('div', { className: 'iterate-filter' },
|
|
1402
|
-
React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setSeverityFilter(e.target.value), 'aria-label': '按严重度筛选' },
|
|
1403
|
-
React.createElement('option', { value: '' }, '全部严重度'),
|
|
1593
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setSeverityFilter(e.target.value), 'aria-label': t('按严重度筛选') },
|
|
1594
|
+
React.createElement('option', { value: '' }, t('全部严重度')),
|
|
1404
1595
|
...options.severities.map((s) =>
|
|
1405
1596
|
React.createElement('option', { key: s.value, value: s.value }, `${severityLabel(s.value)} (${s.count})`),
|
|
1406
1597
|
),
|
|
1407
1598
|
),
|
|
1408
|
-
React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDimensionFilter(e.target.value), 'aria-label': '按维度筛选' },
|
|
1409
|
-
React.createElement('option', { value: '' }, '全部维度'),
|
|
1599
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDimensionFilter(e.target.value), 'aria-label': t('按维度筛选') },
|
|
1600
|
+
React.createElement('option', { value: '' }, t('全部维度')),
|
|
1410
1601
|
...options.dimensions.map((d) =>
|
|
1411
1602
|
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
1412
1603
|
),
|
|
@@ -1414,74 +1605,105 @@ function TriagePanel(props: SlotProps) {
|
|
|
1414
1605
|
React.createElement('input', {
|
|
1415
1606
|
className: 'iterate-filter-search',
|
|
1416
1607
|
type: 'search',
|
|
1417
|
-
placeholder: '搜索文件 / 摘要…',
|
|
1418
|
-
'aria-label': '搜索文件或摘要',
|
|
1608
|
+
placeholder: t('搜索文件 / 摘要…'),
|
|
1609
|
+
'aria-label': t('搜索文件或摘要'),
|
|
1419
1610
|
value: filter.search,
|
|
1420
1611
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setSearchFilter(e.target.value),
|
|
1421
1612
|
}),
|
|
1422
1613
|
React.createElement('span', { className: 'iterate-filter-count' },
|
|
1423
1614
|
isFilterActive
|
|
1424
|
-
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, `清除筛选(显示
|
|
1425
|
-
: `共
|
|
1615
|
+
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, tf(`清除筛选(显示 {a0})`, { a0: filtered.length }))
|
|
1616
|
+
: tf(`共 {a0} 项`, { a0: findings.length }),
|
|
1426
1617
|
),
|
|
1427
1618
|
),
|
|
1428
1619
|
React.createElement('div', { className: 'iterate-batch' },
|
|
1429
|
-
React.createElement('span', { className: 'iterate-batch-label' }, '批量作用于:'),
|
|
1620
|
+
React.createElement('span', { className: 'iterate-batch-label' }, t('批量作用于:')),
|
|
1430
1621
|
React.createElement('button', {
|
|
1431
1622
|
className: selectAll ? 'iterate-batch-btn iterate-batch-scope' : 'iterate-batch-btn iterate-batch-scope iterate-batch-scope-on',
|
|
1432
1623
|
onClick: () => setSelectAll(false),
|
|
1433
|
-
title: '批量按钮仅作用于当前筛选可见的 findings',
|
|
1434
|
-
}, `可见
|
|
1624
|
+
title: t('批量按钮仅作用于当前筛选可见的 findings'),
|
|
1625
|
+
}, tf(`可见 {a0}`, { a0: indices.length })),
|
|
1435
1626
|
React.createElement('button', {
|
|
1436
1627
|
className: selectAll ? 'iterate-batch-btn iterate-batch-scope iterate-batch-scope-on' : 'iterate-batch-btn iterate-batch-scope',
|
|
1437
1628
|
onClick: () => setSelectAll(true),
|
|
1438
|
-
title: '批量按钮作用于全部 findings',
|
|
1439
|
-
}, `全部
|
|
1629
|
+
title: t('批量按钮作用于全部 findings'),
|
|
1630
|
+
}, tf(`全部 {a0}`, { a0: allIndices.length })),
|
|
1440
1631
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('keep') }, 'y'),
|
|
1441
1632
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('skip') }, 'n'),
|
|
1442
1633
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('ignore') }, 'a'),
|
|
1443
|
-
React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: '把所有判定恢复为默认 y(修复)' }, '重置'),
|
|
1634
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: t('把所有判定恢复为默认 y(修复)') }, t('重置')),
|
|
1444
1635
|
),
|
|
1445
1636
|
...rows,
|
|
1446
1637
|
React.createElement('div', { className: 'iterate-triage-foot' },
|
|
1447
|
-
React.createElement('span', {}, `y
|
|
1448
|
-
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1638
|
+
React.createElement('span', {}, tf(`y {a0} · n {a1} · a {a2} · 待写回 known_intentional:{a3} 条`, { a0: counts.keep, a1: counts.skip, a2: counts.ignore, a3: ignoredCount })),
|
|
1639
|
+
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1640
|
+
// Gap #7: the apply instruction (safe path) is PRIMARY; the raw YAML
|
|
1641
|
+
// is demoted to an explicit "just the snippet" escape hatch.
|
|
1642
|
+
React.createElement('button', {
|
|
1643
|
+
className: 'iterate-btn', 'data-primary': '',
|
|
1644
|
+
'data-copied': cmdCopied === 'apply' ? '' : undefined,
|
|
1645
|
+
'data-failed': cmdFailed === 'apply' ? '' : undefined,
|
|
1646
|
+
onClick: doBuildInstruction, disabled: ignoredCount === 0,
|
|
1647
|
+
title: ignoredCount === 0
|
|
1648
|
+
? t('当前没有标记为「已知有意」的 finding')
|
|
1649
|
+
: t('复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)'),
|
|
1650
|
+
}, cmdCopied === 'apply' ? t('已复制') : cmdFailed === 'apply' ? t('复制失败') : tf(`生成应用指令{a0}`, { a0: ignoredCount > 0 ? `(${ignoredCount})` : '' })),
|
|
1449
1651
|
React.createElement('button', {
|
|
1450
|
-
className: 'iterate-btn',
|
|
1652
|
+
className: 'iterate-btn',
|
|
1653
|
+
'data-copied': copied ? '' : undefined,
|
|
1654
|
+
'data-failed': cmdFailed === 'yaml' ? '' : undefined,
|
|
1451
1655
|
onClick: doCopyYaml, disabled: ignoredCount === 0,
|
|
1452
|
-
title: ignoredCount === 0
|
|
1453
|
-
|
|
1656
|
+
title: ignoredCount === 0
|
|
1657
|
+
? t('当前没有标记为「已知有意」的 finding')
|
|
1658
|
+
: t('只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)'),
|
|
1659
|
+
}, copied ? t('已复制') : cmdFailed === 'yaml' ? t('复制失败') : t('仅复制 YAML 片段')),
|
|
1454
1660
|
React.createElement('button', {
|
|
1455
|
-
className: 'iterate-btn',
|
|
1456
|
-
|
|
1457
|
-
|
|
1661
|
+
className: 'iterate-btn',
|
|
1662
|
+
'data-copied': cmdCopied === 'readback' ? '' : undefined,
|
|
1663
|
+
'data-failed': cmdFailed === 'readback' ? '' : undefined,
|
|
1664
|
+
onClick: doReadback,
|
|
1665
|
+
title: t('复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)'),
|
|
1666
|
+
}, cmdCopied === 'readback' ? t('已复制') : cmdFailed === 'readback' ? t('复制失败') : t('回读已写入条目')),
|
|
1458
1667
|
),
|
|
1668
|
+
cmdCopied === 'apply'
|
|
1669
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, t('已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。'))
|
|
1670
|
+
: cmdCopied === 'readback'
|
|
1671
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, t('已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。'))
|
|
1672
|
+
: null,
|
|
1459
1673
|
),
|
|
1460
1674
|
// v3.0: Command buttons for native actions (§8)
|
|
1461
1675
|
React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1462
|
-
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
|
|
1463
|
-
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1676
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('指挥操作')),
|
|
1677
|
+
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1464
1678
|
React.createElement('button', {
|
|
1465
|
-
className: 'iterate-cmd', 'data-primary': '',
|
|
1679
|
+
className: 'iterate-cmd', 'data-primary': '',
|
|
1680
|
+
'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1681
|
+
'data-failed': cmdFailed === 'approve-arch' ? '' : undefined,
|
|
1466
1682
|
onClick: doApproveArchitecturalFix,
|
|
1467
|
-
title: '
|
|
1468
|
-
}, cmdCopied === 'approve-arch' ? '已复制' : '批准架构修复'),
|
|
1683
|
+
title: t('批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)'),
|
|
1684
|
+
}, cmdCopied === 'approve-arch' ? t('已复制') : cmdFailed === 'approve-arch' ? t('复制失败') : t('批准架构修复')),
|
|
1469
1685
|
React.createElement('button', {
|
|
1470
|
-
className: 'iterate-cmd',
|
|
1686
|
+
className: 'iterate-cmd',
|
|
1687
|
+
'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1688
|
+
'data-failed': cmdFailed === 'new-round' ? '' : undefined,
|
|
1471
1689
|
onClick: doTriggerNewRound,
|
|
1472
|
-
title: '触发新一轮迭代审查',
|
|
1473
|
-
}, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
|
|
1690
|
+
title: t('触发新一轮迭代审查'),
|
|
1691
|
+
}, cmdCopied === 'new-round' ? t('已复制') : cmdFailed === 'new-round' ? t('复制失败') : t('触发新一轮')),
|
|
1474
1692
|
React.createElement('button', {
|
|
1475
|
-
className: 'iterate-cmd', 'data-primary': '',
|
|
1693
|
+
className: 'iterate-cmd', 'data-primary': '',
|
|
1694
|
+
'data-copied': cmdCopied === 'assign' ? '' : undefined,
|
|
1695
|
+
'data-failed': cmdFailed === 'assign' ? '' : undefined,
|
|
1476
1696
|
onClick: doAssignFindings,
|
|
1477
1697
|
disabled: (selectAll ? allIndices : indices).length === 0,
|
|
1478
|
-
title: selectAll ? `指派全部
|
|
1479
|
-
}, cmdCopied === 'assign' ? '已复制' : '指派修复'),
|
|
1698
|
+
title: selectAll ? tf(`指派全部 {a0} 个 findings 修复(逐个 iterate_fix)`, { a0: allIndices.length }) : tf(`指派当前可见 {a0} 个 findings 修复(逐个 iterate_fix)`, { a0: indices.length }),
|
|
1699
|
+
}, cmdCopied === 'assign' ? t('已复制') : cmdFailed === 'assign' ? t('复制失败') : t('指派修复')),
|
|
1480
1700
|
React.createElement('button', {
|
|
1481
|
-
className: 'iterate-cmd', 'data-danger': '',
|
|
1701
|
+
className: 'iterate-cmd', 'data-danger': '',
|
|
1702
|
+
'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1703
|
+
'data-failed': cmdFailed === 'rollback' ? '' : undefined,
|
|
1482
1704
|
onClick: doRollbackToCheckpoint,
|
|
1483
|
-
title: '回滚到上一个检查点',
|
|
1484
|
-
}, cmdCopied === 'rollback' ? '已复制' : '回滚检查点'),
|
|
1705
|
+
title: t('回滚到上一个检查点'),
|
|
1706
|
+
}, cmdCopied === 'rollback' ? t('已复制') : cmdFailed === 'rollback' ? t('复制失败') : t('回滚检查点')),
|
|
1485
1707
|
),
|
|
1486
1708
|
),
|
|
1487
1709
|
payload
|
|
@@ -1504,15 +1726,15 @@ function VerdictBanner(props: SlotProps) {
|
|
|
1504
1726
|
'div',
|
|
1505
1727
|
{ 'data-iterate-root': '', 'data-iterate': 'verdict', className: 'iterate-verdict' },
|
|
1506
1728
|
React.createElement('span', { className: 'iterate-verdict-tag', 'data-ok': ok ? '' : undefined, 'data-warn': ok ? undefined : '' },
|
|
1507
|
-
ok ? '报告已批准' : '报告需修订'),
|
|
1729
|
+
ok ? t('报告已批准') : t('报告需修订')),
|
|
1508
1730
|
React.createElement('span', { className: 'iterate-verdict-detail' },
|
|
1509
|
-
item(verdict.totalFindings, '项发现'),
|
|
1510
|
-
item(verdict.totalRounds, '轮'),
|
|
1511
|
-
item(verdict.checksRun, '项审查'),
|
|
1731
|
+
item(verdict.totalFindings, t('项发现')),
|
|
1732
|
+
item(verdict.totalRounds, t('轮')),
|
|
1733
|
+
item(verdict.checksRun, t('项审查')),
|
|
1512
1734
|
ok
|
|
1513
|
-
? phrase('报告通过全部一致性检查')
|
|
1514
|
-
: item(verdict.reportIssues, '处报告缺陷'),
|
|
1515
|
-
phrase(verdict.converged ? '已收敛' : '未收敛'),
|
|
1735
|
+
? phrase(t('报告通过全部一致性检查'))
|
|
1736
|
+
: item(verdict.reportIssues, t('处报告缺陷')),
|
|
1737
|
+
phrase(verdict.converged ? t('已收敛') : t('未收敛')),
|
|
1516
1738
|
),
|
|
1517
1739
|
)
|
|
1518
1740
|
}
|
|
@@ -1561,7 +1783,7 @@ function ProgressCapsule() {
|
|
|
1561
1783
|
const listener = (payload: { round: number; converged: boolean }) => {
|
|
1562
1784
|
const converged = payload && payload.converged === true
|
|
1563
1785
|
const round = payload && typeof payload.round === 'number' ? payload.round : '?'
|
|
1564
|
-
setInfo({ text: converged ? `Round
|
|
1786
|
+
setInfo({ text: converged ? tf(`Round {a0} 完成 · 已收敛`, { a0: round }) : tf(`Round {a0} 完成`, { a0: round }), ok: converged })
|
|
1565
1787
|
if (timer) clearTimeout(timer)
|
|
1566
1788
|
// Converged notifications stay a bit longer.
|
|
1567
1789
|
timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400)
|
|
@@ -1584,7 +1806,17 @@ function ProgressCapsule() {
|
|
|
1584
1806
|
}
|
|
1585
1807
|
|
|
1586
1808
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
1587
|
-
function SettingsPanel(
|
|
1809
|
+
function SettingsPanel(props: SlotProps) {
|
|
1810
|
+
// Re-render on a language switch (the slot wrapper covers the tree; this
|
|
1811
|
+
// keeps the panel honest even if it is ever rendered directly).
|
|
1812
|
+
const lang = useLang()
|
|
1813
|
+
// Root-scoped slot: the host renders settings OUTSIDE the session scope, so
|
|
1814
|
+
// take the session the session-scoped slots published (a host that does pass
|
|
1815
|
+
// `props.session` wins). This is what makes the config read-back visible.
|
|
1816
|
+
// The hook is called UNCONDITIONALLY — a conditional hook would break the
|
|
1817
|
+
// panel as soon as a host started passing `props.session`.
|
|
1818
|
+
const cachedSession = useSessionSnapshot()
|
|
1819
|
+
const session = props && props.session ? props.session : cachedSession
|
|
1588
1820
|
const [enabled, setEnabled] = React.useState(themeEnabled)
|
|
1589
1821
|
const [guideCopied, setGuideCopied] = React.useState(false)
|
|
1590
1822
|
const [statusCopied, setStatusCopied] = React.useState(false)
|
|
@@ -1592,9 +1824,37 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1592
1824
|
const [showStatus, setShowStatus] = React.useState(false)
|
|
1593
1825
|
const [confirming, setConfirming] = React.useState(false)
|
|
1594
1826
|
const [clearedInfo, setClearedInfo] = React.useState<number | null>(null)
|
|
1827
|
+
// Gap #8: "pick a field → generate an iterate_config write instruction".
|
|
1828
|
+
// Defaults to validation.commands, the field the old guide omitted entirely
|
|
1829
|
+
// (and the root cause of the zero-verification P0).
|
|
1830
|
+
const [cfgField, setCfgField] = React.useState<string>(
|
|
1831
|
+
() => (CONFIG_EDIT_FIELDS.some((f) => f.key === 'validation.commands') ? 'validation.commands' : (CONFIG_EDIT_FIELDS[0]?.key ?? 'goal')),
|
|
1832
|
+
)
|
|
1833
|
+
const [cfgCopied, setCfgCopied] = React.useState(false)
|
|
1834
|
+
const [cfgFailed, setCfgFailed] = React.useState(false)
|
|
1835
|
+
const [cfgText, setCfgText] = React.useState<string | null>(null)
|
|
1836
|
+
// Gap #1 (3.7.0): the read half of the write -> read-back loop.
|
|
1837
|
+
const [cfgReadCopied, setCfgReadCopied] = React.useState(false)
|
|
1838
|
+
const [cfgReadText, setCfgReadText] = React.useState<string | null>(null)
|
|
1839
|
+
const [pullCopied, setPullCopied] = React.useState(false)
|
|
1840
|
+
const [copyError, setCopyError] = React.useState<'guide' | 'status' | 'pull' | 'cfgRead' | null>(null)
|
|
1595
1841
|
const guide = buildConfigEditGuide()
|
|
1596
1842
|
const statusGuide = buildRuntimeStatusGuide()
|
|
1597
1843
|
|
|
1844
|
+
// What the config actually says right now: latest `iterate_config` result
|
|
1845
|
+
// echoed into the session, then the value of the field being edited.
|
|
1846
|
+
const cfgMeta = configFieldByKey(cfgField)
|
|
1847
|
+
const cfgRead = scanSessionForConfigRead(session)
|
|
1848
|
+
const cfgCurrent = cfgRead ? configValueByPath(cfgRead, cfgField) : null
|
|
1849
|
+
const cfgFieldLabel = cfgMeta ? cfgMeta.label[getLang()] : cfgField
|
|
1850
|
+
|
|
1851
|
+
// Audit: don't leak flash timers across settings unmounts.
|
|
1852
|
+
const cfgTimers = React.useRef<number[]>([])
|
|
1853
|
+
React.useEffect(() => () => {
|
|
1854
|
+
for (const t of cfgTimers.current) clearTimeout(t)
|
|
1855
|
+
cfgTimers.current = []
|
|
1856
|
+
}, [])
|
|
1857
|
+
|
|
1598
1858
|
const toggleTheme = () => {
|
|
1599
1859
|
const next = !enabled
|
|
1600
1860
|
setEnabled(next)
|
|
@@ -1602,54 +1862,84 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1602
1862
|
}
|
|
1603
1863
|
|
|
1604
1864
|
/** Flash a per-slot "已复制" state for the given duration. */
|
|
1605
|
-
const flashCopied = (slot: 'guide' | 'status') => {
|
|
1606
|
-
const setter =
|
|
1865
|
+
const flashCopied = (slot: 'guide' | 'status' | 'pull' | 'cfgRead') => {
|
|
1866
|
+
const setter =
|
|
1867
|
+
slot === 'guide' ? setGuideCopied : slot === 'status' ? setStatusCopied : slot === 'pull' ? setPullCopied : setCfgReadCopied
|
|
1607
1868
|
setter(true)
|
|
1608
|
-
setTimeout(() => setter(false), 1600)
|
|
1869
|
+
cfgTimers.current.push(window.setTimeout(() => setter(false), 1600))
|
|
1609
1870
|
}
|
|
1610
1871
|
|
|
1611
|
-
/** Copy a guide/status block; flash success only when it actually copied
|
|
1612
|
-
|
|
1872
|
+
/** Copy a guide/status block; flash success only when it actually copied,
|
|
1873
|
+
* and surface a visible error when the clipboard write is refused (the old
|
|
1874
|
+
* behaviour was a silent no-op, which reads as a broken button). */
|
|
1875
|
+
const doCopy = (text: string, slot: 'guide' | 'status' | 'pull' | 'cfgRead') => {
|
|
1613
1876
|
copyText(text).then((ok) => {
|
|
1877
|
+
setCopyError(ok ? null : slot)
|
|
1614
1878
|
if (ok) flashCopied(slot)
|
|
1615
1879
|
})
|
|
1616
1880
|
}
|
|
1617
1881
|
|
|
1882
|
+
/** Gap #8: build + copy the write instruction for the selected field. */
|
|
1883
|
+
const doBuildConfigInstruction = () => {
|
|
1884
|
+
const text = buildConfigFieldInstruction(cfgField)
|
|
1885
|
+
setCfgText(text)
|
|
1886
|
+
copyText(text).then((ok) => {
|
|
1887
|
+
setCfgFailed(!ok)
|
|
1888
|
+
setCfgCopied(ok)
|
|
1889
|
+
cfgTimers.current.push(window.setTimeout(() => setCfgCopied(false), 1600))
|
|
1890
|
+
// On failure the raw text stays on screen for a manual copy.
|
|
1891
|
+
if (!ok) return
|
|
1892
|
+
})
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/** Copy the read-back instruction; a refused clipboard reveals it below. */
|
|
1896
|
+
const doCopyConfigRead = () => {
|
|
1897
|
+
const text = buildConfigReadInstruction()
|
|
1898
|
+
copyText(text).then((ok) => {
|
|
1899
|
+
setCopyError(ok ? null : 'cfgRead')
|
|
1900
|
+
if (ok) {
|
|
1901
|
+
flashCopied('cfgRead')
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1904
|
+
setCfgReadText(text)
|
|
1905
|
+
})
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1618
1908
|
/** Two-step destroy guard: first click arms, second click clears. */
|
|
1619
1909
|
const requestClear = () => {
|
|
1620
1910
|
if (confirming) {
|
|
1621
1911
|
const count = removeStorageByPrefix(TRIAGE_STORAGE_PREFIX)
|
|
1622
1912
|
setClearedInfo(count)
|
|
1623
1913
|
setConfirming(false)
|
|
1624
|
-
setTimeout(() => setClearedInfo(null), 3000)
|
|
1914
|
+
cfgTimers.current.push(window.setTimeout(() => setClearedInfo(null), 3000))
|
|
1625
1915
|
return
|
|
1626
1916
|
}
|
|
1627
1917
|
setConfirming(true)
|
|
1628
|
-
setTimeout(() => setConfirming(false), 3000)
|
|
1918
|
+
cfgTimers.current.push(window.setTimeout(() => setConfirming(false), 3000))
|
|
1629
1919
|
}
|
|
1630
1920
|
|
|
1631
1921
|
const clearButton = clearedInfo !== null
|
|
1632
|
-
? React.createElement('button', { className: 'iterate-btn', 'data-copied': '', disabled: true }, `已清除
|
|
1922
|
+
? React.createElement('button', { className: 'iterate-btn', 'data-copied': '', disabled: true }, tf(`已清除 {a0} 条`, { a0: clearedInfo }))
|
|
1633
1923
|
: React.createElement('button', {
|
|
1634
1924
|
className: 'iterate-btn',
|
|
1635
1925
|
'data-danger': confirming ? undefined : '',
|
|
1636
1926
|
'data-confirm': confirming ? '' : undefined,
|
|
1637
1927
|
onClick: requestClear,
|
|
1638
|
-
title: confirming ? '再次点击以确认清除全部判定' : '清除所有分诊判定记录(需要二次确认)',
|
|
1639
|
-
}, confirming ? '确认清除?' : '清除分诊')
|
|
1928
|
+
title: confirming ? t('再次点击以确认清除全部判定') : t('清除所有分诊判定记录(需要二次确认)'),
|
|
1929
|
+
}, confirming ? t('确认清除?') : t('清除分诊'))
|
|
1640
1930
|
|
|
1641
1931
|
const themeCard = React.createElement('div', { key: 'theme', className: 'iterate-scard' },
|
|
1642
1932
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1643
1933
|
React.createElement('div', {},
|
|
1644
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '迭代主题'),
|
|
1645
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。'),
|
|
1934
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('迭代主题')),
|
|
1935
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。')),
|
|
1646
1936
|
),
|
|
1647
1937
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1648
|
-
React.createElement('span', { className: 'iterate-chip', 'data-ok': enabled ? '' : undefined }, enabled ? '已启用' : '已关闭'),
|
|
1938
|
+
React.createElement('span', { className: 'iterate-chip', 'data-ok': enabled ? '' : undefined }, enabled ? t('已启用') : t('已关闭')),
|
|
1649
1939
|
React.createElement('button', {
|
|
1650
1940
|
role: 'switch',
|
|
1651
1941
|
'aria-checked': enabled,
|
|
1652
|
-
'aria-label': '开关 iterate 主题',
|
|
1942
|
+
'aria-label': t('开关 iterate 主题'),
|
|
1653
1943
|
className: 'iterate-switch',
|
|
1654
1944
|
'data-on': enabled ? '' : undefined,
|
|
1655
1945
|
onClick: toggleTheme,
|
|
@@ -1658,14 +1948,36 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1658
1948
|
),
|
|
1659
1949
|
)
|
|
1660
1950
|
|
|
1951
|
+
const langCard = React.createElement('div', { key: 'lang', className: 'iterate-scard' },
|
|
1952
|
+
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1953
|
+
React.createElement('div', {},
|
|
1954
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('界面语言')),
|
|
1955
|
+
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
1956
|
+
t('面板文案的显示语言;复制给模型的指令保持中文,与 SKILL.md 的工作流契约一致。'),
|
|
1957
|
+
),
|
|
1958
|
+
),
|
|
1959
|
+
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1960
|
+
React.createElement('span', { className: 'iterate-chip', 'data-ok': '' }, LANG_NAMES[lang]),
|
|
1961
|
+
React.createElement('button', {
|
|
1962
|
+
role: 'switch',
|
|
1963
|
+
'aria-checked': lang === 'en',
|
|
1964
|
+
'aria-label': t('切换界面语言'),
|
|
1965
|
+
className: 'iterate-switch',
|
|
1966
|
+
'data-on': lang === 'en' ? '' : undefined,
|
|
1967
|
+
onClick: () => setLang(lang === 'en' ? 'zh' : 'en'),
|
|
1968
|
+
}, React.createElement('span', { className: 'iterate-switch-knob' })),
|
|
1969
|
+
),
|
|
1970
|
+
),
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1661
1973
|
const dataCard = React.createElement('div', { key: 'data', className: 'iterate-scard' },
|
|
1662
1974
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1663
1975
|
React.createElement('div', {},
|
|
1664
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '分诊持久化'),
|
|
1665
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, 'y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。'),
|
|
1976
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('分诊持久化')),
|
|
1977
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。')),
|
|
1666
1978
|
),
|
|
1667
1979
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1668
|
-
React.createElement('span', { className: 'iterate-chip' }, '本地保存'),
|
|
1980
|
+
React.createElement('span', { className: 'iterate-chip' }, t('本地保存')),
|
|
1669
1981
|
clearButton,
|
|
1670
1982
|
),
|
|
1671
1983
|
),
|
|
@@ -1674,19 +1986,85 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1674
1986
|
const guideCard = React.createElement('div', { key: 'guide', className: 'iterate-scard' },
|
|
1675
1987
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1676
1988
|
React.createElement('div', {},
|
|
1677
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
|
|
1678
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 /
|
|
1989
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('配置管理')),
|
|
1990
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。')),
|
|
1679
1991
|
),
|
|
1680
1992
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1681
|
-
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': guideCopied ? '' : undefined, onClick: () => doCopy(guide, 'guide') }, guideCopied ? '已复制' : '复制指引'),
|
|
1682
|
-
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowGuide((v) => !v) }, showGuide ? '收起' : '展开'),
|
|
1993
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': guideCopied ? '' : undefined, onClick: () => doCopy(guide, 'guide') }, guideCopied ? t('已复制') : t('复制指引')),
|
|
1994
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowGuide((v) => !v) }, showGuide ? t('收起') : t('展开')),
|
|
1683
1995
|
),
|
|
1684
1996
|
),
|
|
1997
|
+
// Gap #8: wire up the previously-dead `buildConfigFieldInstruction` — pick
|
|
1998
|
+
// a field, generate the `iterate_config` write payload for it, paste it to
|
|
1999
|
+
// the model. `validation.commands` is the default selection.
|
|
2000
|
+
React.createElement('div', { className: 'iterate-guide-bar', style: { marginTop: 8, flexWrap: 'wrap' } },
|
|
2001
|
+
React.createElement('select', {
|
|
2002
|
+
className: 'iterate-filter-select',
|
|
2003
|
+
value: cfgField,
|
|
2004
|
+
'aria-label': t('选择要修改的配置字段'),
|
|
2005
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setCfgField(e.target.value),
|
|
2006
|
+
},
|
|
2007
|
+
...CONFIG_EDIT_FIELDS.map((f) =>
|
|
2008
|
+
React.createElement('option', { key: f.key, value: f.key }, getLang() === 'en' ? `${f.label.en} (${f.key})` : `${f.label.zh}(${f.key})`),
|
|
2009
|
+
),
|
|
2010
|
+
),
|
|
2011
|
+
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
2012
|
+
cfgMeta?.hint[getLang()] ?? '',
|
|
2013
|
+
),
|
|
2014
|
+
React.createElement('button', {
|
|
2015
|
+
className: 'iterate-btn', 'data-primary': '',
|
|
2016
|
+
'data-copied': cfgCopied ? '' : undefined,
|
|
2017
|
+
'data-failed': cfgFailed ? '' : undefined,
|
|
2018
|
+
onClick: doBuildConfigInstruction,
|
|
2019
|
+
title: t('生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)'),
|
|
2020
|
+
}, cfgCopied ? t('已复制') : cfgFailed ? t('复制失败') : t('生成写入指令')),
|
|
2021
|
+
cfgFailed
|
|
2022
|
+
? React.createElement('span', { className: 'iterate-settings-desc' }, t('剪贴板被拒绝——指令已展示在下方,请手动复制。'))
|
|
2023
|
+
: null,
|
|
2024
|
+
),
|
|
2025
|
+
// Gap #1 (3.7.0): close the write -> read-back loop. Three honest states:
|
|
2026
|
+
// nothing echoed yet, the value the model read back, or a read-back that
|
|
2027
|
+
// never contained this field (defaults are in play — say so, don't blank).
|
|
2028
|
+
React.createElement('div', { className: 'iterate-guide-bar', style: { marginTop: 8, flexWrap: 'wrap' } },
|
|
2029
|
+
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
2030
|
+
!cfgRead
|
|
2031
|
+
? t('尚未回读当前配置——点「回读当前配置」复制指令发给模型,回显后即可在此看到当前值。')
|
|
2032
|
+
: cfgCurrent && cfgCurrent.resolved
|
|
2033
|
+
? tf('当前值 {a0}:{a1}', { a0: cfgFieldLabel, a1: formatConfigValue(cfgCurrent.value) })
|
|
2034
|
+
: tf('回读结果中没有 {a0}(该字段可能尚未写入配置文件)。', { a0: cfgFieldLabel }),
|
|
2035
|
+
),
|
|
2036
|
+
React.createElement('button', {
|
|
2037
|
+
className: 'iterate-btn',
|
|
2038
|
+
'data-primary': '',
|
|
2039
|
+
'data-copied': cfgReadCopied ? '' : undefined,
|
|
2040
|
+
onClick: doCopyConfigRead,
|
|
2041
|
+
title: t('复制回读指令:让模型调用 iterate_config(operation:"read") 并回显结果'),
|
|
2042
|
+
}, cfgReadCopied ? t('已复制') : t('回读当前配置')),
|
|
2043
|
+
),
|
|
2044
|
+
cfgReadText
|
|
2045
|
+
? React.createElement('div', { className: 'iterate-guide' },
|
|
2046
|
+
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
2047
|
+
React.createElement('span', {}, t('回读指令(粘贴给模型)')),
|
|
2048
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgReadText, 'cfgRead') }, t('复制')),
|
|
2049
|
+
),
|
|
2050
|
+
React.createElement('div', { className: 'iterate-guide-body' }, cfgReadText),
|
|
2051
|
+
)
|
|
2052
|
+
: null,
|
|
2053
|
+
cfgText
|
|
2054
|
+
? React.createElement('div', { className: 'iterate-guide' },
|
|
2055
|
+
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
2056
|
+
React.createElement('span', {}, t('写入指令(粘贴给模型)')),
|
|
2057
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgText, 'guide') }, t('复制')),
|
|
2058
|
+
),
|
|
2059
|
+
React.createElement('div', { className: 'iterate-guide-body' }, cfgText),
|
|
2060
|
+
)
|
|
2061
|
+
: null,
|
|
1685
2062
|
showGuide
|
|
1686
2063
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1687
2064
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1688
|
-
React.createElement('span', {}, 'iterate.config.yaml 可编辑字段'),
|
|
1689
|
-
React.createElement('
|
|
2065
|
+
React.createElement('span', {}, t('iterate.config.yaml 可编辑字段')),
|
|
2066
|
+
React.createElement('span', { className: 'iterate-chip' }, t('中文(粘贴给模型的契约文案)')),
|
|
2067
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(guide, 'guide') }, t('复制')),
|
|
1690
2068
|
),
|
|
1691
2069
|
React.createElement('div', { className: 'iterate-guide-body' }, guide),
|
|
1692
2070
|
)
|
|
@@ -1696,19 +2074,27 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1696
2074
|
const statusCard = React.createElement('div', { key: 'status', className: 'iterate-scard' },
|
|
1697
2075
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1698
2076
|
React.createElement('div', {},
|
|
1699
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '状态概览'),
|
|
1700
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '查看运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。'),
|
|
2077
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('状态概览')),
|
|
2078
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('查看运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。')),
|
|
1701
2079
|
),
|
|
1702
2080
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1703
|
-
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': statusCopied ? '' : undefined, onClick: () => doCopy(statusGuide, 'status') }, statusCopied ? '已复制' : '复制指引'),
|
|
1704
|
-
|
|
2081
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': statusCopied ? '' : undefined, onClick: () => doCopy(statusGuide, 'status') }, statusCopied ? t('已复制') : t('复制指引')),
|
|
2082
|
+
// Gap #4: RUNTIME_ARTIFACTS stops being documentation-only — this
|
|
2083
|
+
// copies the instruction that makes the model echo those artifacts
|
|
2084
|
+
// into the session so the observatory panels can read them.
|
|
2085
|
+
React.createElement('button', {
|
|
2086
|
+
className: 'iterate-btn', 'data-copied': pullCopied ? '' : undefined,
|
|
2087
|
+
onClick: () => doCopy(buildDiskSnapshotInstruction(), 'pull'),
|
|
2088
|
+
}, pullCopied ? t('已复制') : t('拉取磁盘快照')),
|
|
2089
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowStatus((v) => !v) }, showStatus ? t('收起') : t('展开')),
|
|
1705
2090
|
),
|
|
1706
2091
|
),
|
|
1707
2092
|
showStatus
|
|
1708
2093
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1709
2094
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1710
|
-
React.createElement('span', {}, '运行时布局与清理'),
|
|
1711
|
-
React.createElement('
|
|
2095
|
+
React.createElement('span', {}, t('运行时布局与清理')),
|
|
2096
|
+
React.createElement('span', { className: 'iterate-chip' }, t('中文(粘贴给模型的契约文案)')),
|
|
2097
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(statusGuide, 'status') }, t('复制')),
|
|
1712
2098
|
),
|
|
1713
2099
|
React.createElement('div', { className: 'iterate-guide-body' }, statusGuide),
|
|
1714
2100
|
)
|
|
@@ -1719,18 +2105,26 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1719
2105
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1720
2106
|
React.createElement('div', {},
|
|
1721
2107
|
React.createElement('div', { className: 'iterate-settings-title' }, 'iterate'),
|
|
1722
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。'),
|
|
2108
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。')),
|
|
1723
2109
|
),
|
|
1724
2110
|
React.createElement('span', { className: 'iterate-pill' },
|
|
1725
2111
|
React.createElement('span', { className: 'iterate-pill-dot' }),
|
|
1726
|
-
'就绪',
|
|
2112
|
+
t('就绪'),
|
|
1727
2113
|
),
|
|
1728
2114
|
),
|
|
1729
2115
|
)
|
|
1730
2116
|
|
|
1731
2117
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'settings', className: 'iterate-settings' },
|
|
1732
|
-
React.createElement('div', { className: 'iterate-settings-title', style: { fontSize: 15, fontWeight: 700 } }, 'iterate 设置'),
|
|
2118
|
+
React.createElement('div', { className: 'iterate-settings-title', style: { fontSize: 15, fontWeight: 700 } }, t('iterate 设置')),
|
|
1733
2119
|
banner,
|
|
2120
|
+
copyError
|
|
2121
|
+
? React.createElement('div', { key: 'copy-err', className: 'iterate-scard', role: 'alert' },
|
|
2122
|
+
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
2123
|
+
t('剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。'),
|
|
2124
|
+
),
|
|
2125
|
+
)
|
|
2126
|
+
: null,
|
|
2127
|
+
langCard,
|
|
1734
2128
|
themeCard,
|
|
1735
2129
|
dataCard,
|
|
1736
2130
|
guideCard,
|
|
@@ -1740,36 +2134,63 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1740
2134
|
|
|
1741
2135
|
// ─── Runtime observatory panel (F1–F7) ──────────────────────────────────────
|
|
1742
2136
|
|
|
1743
|
-
/** Tab id → label for the observatory's
|
|
1744
|
-
* stream) is first so it is the default view; F-panels follow.
|
|
2137
|
+
/** Tab id → label for the observatory's eleven panels. Live (real-time
|
|
2138
|
+
* activity stream) is first so it is the default view; F-panels follow.
|
|
2139
|
+
* Gap #14: labels carry the "Fn" number used everywhere else (README,
|
|
2140
|
+
* code comments, the copy instructions), so the docs and the UI line up. */
|
|
2141
|
+
/** Visible labels per tab, per language (module constants are evaluated once,
|
|
2142
|
+
* so they carry BOTH languages and resolve at render time). */
|
|
1745
2143
|
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: '决策时间线' },
|
|
2144
|
+
{ key: 'live', label: { zh: '实时活动流', en: 'Live activity' }, hotkey: '' },
|
|
2145
|
+
{ key: 'f1', label: { zh: '审查线程', en: 'Review threads' }, hotkey: '1' },
|
|
2146
|
+
{ key: 'f2', label: { zh: '收敛趋势', en: 'Convergence trend' }, hotkey: '2' },
|
|
2147
|
+
{ key: 'f3', label: { zh: '发现定位', en: 'Finding locations' }, hotkey: '3' },
|
|
2148
|
+
{ key: 'f4', label: { zh: '修复与回滚', en: 'Fixes & rollback' }, hotkey: '4' },
|
|
2149
|
+
{ key: 'f5', label: { zh: '断点恢复', en: 'Checkpoint resume' }, hotkey: '5' },
|
|
2150
|
+
{ key: 'f6', label: { zh: '运行控制台', en: 'Run console' }, hotkey: '6' },
|
|
2151
|
+
{ key: 'f7', label: { zh: '决策时间线', en: 'Decision timeline' }, hotkey: '7' },
|
|
1754
2152
|
// v3.0: Quality Command Center tabs
|
|
1755
|
-
{ key: 'f8', label: '质量门禁' },
|
|
1756
|
-
{ key: 'f9', label: '经验银行' },
|
|
1757
|
-
{ key: 'f10', label: '防御事件' },
|
|
2153
|
+
{ key: 'f8', label: { zh: '质量门禁', en: 'Quality gate' }, hotkey: '8' },
|
|
2154
|
+
{ key: 'f9', label: { zh: '经验银行', en: 'Experience bank' }, hotkey: '9' },
|
|
2155
|
+
{ key: 'f10', label: { zh: '防御事件', en: 'Defense events' }, hotkey: '0' },
|
|
1758
2156
|
] as const
|
|
1759
2157
|
|
|
1760
|
-
/**
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2158
|
+
/** Digit → tab id. `1-9` map to F1–F9, `0` to F10, `-` back to the live feed
|
|
2159
|
+
* (the default view). Digits are used instead of F1–F10 to avoid clashing
|
|
2160
|
+
* with browser/system F-key bindings. */
|
|
2161
|
+
const OBS_HOTKEY_TABS: Record<string, (typeof OBS_TABS)[number]['key']> = {
|
|
2162
|
+
1: 'f1', 2: 'f2', 3: 'f3', 4: 'f4', 5: 'f5',
|
|
2163
|
+
6: 'f6', 7: 'f7', 8: 'f8', 9: 'f9', 0: 'f10',
|
|
2164
|
+
'-': 'live',
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/** Visible label for a tab (keeps the number prefix in sync with `OBS_TABS`). */
|
|
2168
|
+
function obsTabLabel(t: (typeof OBS_TABS)[number]): string {
|
|
2169
|
+
const label = t.label[getLang()]
|
|
2170
|
+
if (t.key === 'live') return label
|
|
2171
|
+
return `${t.key.toUpperCase()} ${label}`
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/** Hotkey hint shown in the collapsed header, e.g. "11 页 · 按 1-9/0 切 Fn、- 回实时流". */
|
|
2175
|
+
function obsHotkeyHint(): string {
|
|
2176
|
+
return getLang() === 'en'
|
|
2177
|
+
? `${String(OBS_TABS.length)} tabs · keys 1-9/0 switch Fn, - returns to the live feed`
|
|
2178
|
+
: tf(`{a0} 页 · 按 1-9/0 切 Fn、- 回实时流`, { a0: String(OBS_TABS.length) })
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/** Live subagent activity badges: type → label + dsw token color accent. */
|
|
2182
|
+
const OBS_LIVE_META: Record<string, { label: { zh: string; en: string }; color: string }> = {
|
|
2183
|
+
read: { label: { zh: '阅读', en: 'Read' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2184
|
+
fix: { label: { zh: '修复', en: 'Fix' }, color: 'var(--dsw-alias-state-success-primary)' },
|
|
2185
|
+
rollback: { label: { zh: '回滚', en: 'Rollback' }, color: 'var(--dsw-alias-state-error-primary)' },
|
|
2186
|
+
diff: { label: { zh: '差异', en: 'Diff' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2187
|
+
review: { label: { zh: '审查', en: 'Review' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2188
|
+
triage: { label: { zh: '分流', en: 'Triage' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2189
|
+
checkpoint: { label: { zh: '断点', en: 'Checkpoint' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2190
|
+
validate: { label: { zh: '校验', en: 'Validate' }, color: 'var(--dsw-alias-state-success-primary)' },
|
|
2191
|
+
log: { label: { zh: '记录', en: 'Log' }, color: 'var(--dsw-alias-label-secondary)' },
|
|
2192
|
+
prune: { label: { zh: '清理', en: 'Prune' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2193
|
+
info: { label: { zh: '信息', en: 'Info' }, color: 'var(--dsw-alias-label-secondary)' },
|
|
1773
2194
|
}
|
|
1774
2195
|
const OBS_LIVE_FALLBACK_COLOR = 'var(--dsw-alias-label-secondary)'
|
|
1775
2196
|
|
|
@@ -1820,16 +2241,13 @@ function latestDefenseEvents(session: unknown): ObsDefenseResult | null {
|
|
|
1820
2241
|
return scanSessionForDefenseEvents(session) as ObsDefenseResult | null
|
|
1821
2242
|
}
|
|
1822
2243
|
|
|
1823
|
-
/** Build a human-pasteable `iterate_fix` instruction for one finding.
|
|
2244
|
+
/** Build a human-pasteable `iterate_fix` instruction for one finding.
|
|
2245
|
+
* Gap #10: delegates to the shared builder so the payload always names the
|
|
2246
|
+
* four required `iterate_fix` params (file / content / finding / round) and
|
|
2247
|
+
* marks `content` + `round` as model-supplied instead of shipping a partial
|
|
2248
|
+
* call the model has to guess at. */
|
|
1824
2249
|
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\`\`\``
|
|
2250
|
+
return buildFixInstruction(f as unknown as Record<string, unknown>)
|
|
1833
2251
|
}
|
|
1834
2252
|
|
|
1835
2253
|
/**
|
|
@@ -1843,6 +2261,9 @@ function buildObsFixInstruction(f: ObsFinding): string {
|
|
|
1843
2261
|
*/
|
|
1844
2262
|
function ObservatoryPanel(props: SlotProps) {
|
|
1845
2263
|
const session = props && props.session ? props.session : null
|
|
2264
|
+
React.useEffect(() => {
|
|
2265
|
+
noteSession(session)
|
|
2266
|
+
}, [session])
|
|
1846
2267
|
const manifest = latestTranscript(session)
|
|
1847
2268
|
// v3.1+: command-center data scanned straight out of the session stream.
|
|
1848
2269
|
const qualityGate = latestQualityGate(session)
|
|
@@ -1880,6 +2301,40 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1880
2301
|
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1881
2302
|
React.useEffect(() => () => { if (copyTimer.current) clearTimeout(copyTimer.current) }, [])
|
|
1882
2303
|
|
|
2304
|
+
// Gap #14: digit shortcuts for tab switching. Digit keys (1-9 / 0 / -) are
|
|
2305
|
+
// used instead of F1–F10 so we never fight the browser's own F-key handling.
|
|
2306
|
+
// Guards mirror the triage panel: no modifier keys, no editable focus, and
|
|
2307
|
+
// only while the observatory is expanded AND focus is inside it — so the
|
|
2308
|
+
// keys never steal input from the composer or another panel.
|
|
2309
|
+
React.useEffect(() => {
|
|
2310
|
+
const doc = typeof document !== 'undefined' ? document : null
|
|
2311
|
+
if (!doc || !open) return
|
|
2312
|
+
const onKeyDown = (ev: KeyboardEvent) => {
|
|
2313
|
+
if (ev.metaKey || ev.ctrlKey || ev.altKey || ev.shiftKey) return
|
|
2314
|
+
const t = ev.target as HTMLElement | null
|
|
2315
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
|
|
2316
|
+
if (t && typeof t.isContentEditable === 'boolean' && t.isContentEditable) return
|
|
2317
|
+
const rootEl = doc.querySelector('[data-iterate="obs"]')
|
|
2318
|
+
if (!rootEl) return
|
|
2319
|
+
const activeEl = doc.activeElement as HTMLElement | null
|
|
2320
|
+
// Act when focus is INSIDE the observatory, or nowhere in particular
|
|
2321
|
+
// (body/null — some browsers never focus a <button> on click). Focus
|
|
2322
|
+
// parked on a non-editable element outside the panel keeps standard
|
|
2323
|
+
// behavior; focus in the composer was already excluded above.
|
|
2324
|
+
const inPanel = activeEl === rootEl ||
|
|
2325
|
+
(activeEl !== null && typeof rootEl.contains === 'function' && rootEl.contains(activeEl))
|
|
2326
|
+
const idle = activeEl === null || activeEl === doc.body
|
|
2327
|
+
if (!inPanel && !idle) return
|
|
2328
|
+
const next = OBS_HOTKEY_TABS[ev.key]
|
|
2329
|
+
if (!next) return
|
|
2330
|
+
ev.preventDefault()
|
|
2331
|
+
setTab(next)
|
|
2332
|
+
}
|
|
2333
|
+
doc.addEventListener('keydown', onKeyDown)
|
|
2334
|
+
return () => doc.removeEventListener('keydown', onKeyDown)
|
|
2335
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2336
|
+
}, [open])
|
|
2337
|
+
|
|
1883
2338
|
/** Copy an instruction, flashing "已复制" on the originating button only when the copy actually succeeded. */
|
|
1884
2339
|
const copyInstruction = (key: string, text: string) => {
|
|
1885
2340
|
if (!text) return
|
|
@@ -1891,6 +2346,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1891
2346
|
})
|
|
1892
2347
|
}
|
|
1893
2348
|
|
|
2349
|
+
// Gap #4: `.iterate/` artifacts are unreachable from the browser — this is
|
|
2350
|
+
// the one instruction that makes the model echo them into the session
|
|
2351
|
+
// stream, which is what every panel scans.
|
|
2352
|
+
const pullInstruction = buildDiskSnapshotInstruction()
|
|
2353
|
+
const pullTitle = tf(`复制拉取磁盘快照指令:让模型逐个调用 {a0} 并回显完整结果`, { a0: DISK_SNAPSHOT_SOURCES.map((s) => s.tool).join(' / ') })
|
|
2354
|
+
|
|
1894
2355
|
/** Toggle one thread; a manual toggle exits any forced all/none mode. */
|
|
1895
2356
|
const toggleThread = (key: string) => {
|
|
1896
2357
|
setThreadMode('auto')
|
|
@@ -1901,10 +2362,19 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1901
2362
|
})
|
|
1902
2363
|
}
|
|
1903
2364
|
|
|
1904
|
-
/** Export the full observatory snapshot as JSON (download, then copy fallback).
|
|
2365
|
+
/** Export the full observatory snapshot as JSON (download, then copy fallback).
|
|
2366
|
+
* Gap #11: the payload also carries the four on-disk artifacts the panels
|
|
2367
|
+
* render (quality gate, experience bank, defense events, final report) —
|
|
2368
|
+
* previously only `manifest` + `live` were exported, so a review could not
|
|
2369
|
+
* be reconstructed from the file. */
|
|
1905
2370
|
const exportObservatory = () => {
|
|
1906
2371
|
if (!manifest) return
|
|
1907
|
-
const json = serializeObservatoryExport(manifest, manifest.live || []
|
|
2372
|
+
const json = serializeObservatoryExport(manifest, manifest.live || [], {
|
|
2373
|
+
qualityGate,
|
|
2374
|
+
experienceBank,
|
|
2375
|
+
defenseEvents,
|
|
2376
|
+
report: latestReport(session),
|
|
2377
|
+
})
|
|
1908
2378
|
const filename = `iterate-observatory-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
|
1909
2379
|
const flash = () => {
|
|
1910
2380
|
setCopiedKey('export')
|
|
@@ -1937,8 +2407,16 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1937
2407
|
if (!manifest) {
|
|
1938
2408
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
1939
2409
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': '' },
|
|
1940
|
-
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
1941
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2410
|
+
React.createElement('span', { className: 'iterate-obs-title' }, t('iterate 观测台')),
|
|
2411
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`暂无运行时观测数据 · {a0}`, { a0: obsHotkeyHint() })),
|
|
2412
|
+
React.createElement('button', {
|
|
2413
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
2414
|
+
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
2415
|
+
e.stopPropagation()
|
|
2416
|
+
copyInstruction('pull-snapshot', pullInstruction)
|
|
2417
|
+
},
|
|
2418
|
+
title: t('复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充'),
|
|
2419
|
+
}, copiedKey === 'pull-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
1942
2420
|
),
|
|
1943
2421
|
)
|
|
1944
2422
|
}
|
|
@@ -1964,49 +2442,49 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1964
2442
|
const renderThreads = () => {
|
|
1965
2443
|
const rounds = manifest.rounds || []
|
|
1966
2444
|
if (rounds.length === 0) {
|
|
1967
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无审查线程')
|
|
2445
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无审查线程'))
|
|
1968
2446
|
}
|
|
1969
2447
|
return React.createElement('div', {},
|
|
1970
2448
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
1971
|
-
React.createElement('b', {}, '审查线程'),
|
|
1972
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('all'), title: '展开全部线程' }, '全部展开'),
|
|
1973
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('none'), title: '收起全部线程' }, '全部收起'),
|
|
2449
|
+
React.createElement('b', {}, t('审查线程')),
|
|
2450
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('all'), title: t('展开全部线程') }, t('全部展开')),
|
|
2451
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('none'), title: t('收起全部线程') }, t('全部收起')),
|
|
1974
2452
|
React.createElement('span', { className: 'iterate-filter-count' },
|
|
1975
|
-
|
|
2453
|
+
tf(`{a0} · {a1} 轮`, { a0: threadMode === 'all' ? t('全部展开') : threadMode === 'none' ? t('全部收起') : t('自由切换'), a1: rounds.length }),
|
|
1976
2454
|
),
|
|
1977
2455
|
),
|
|
1978
2456
|
...rounds.map((r, ri) => {
|
|
1979
2457
|
const threads = r.threads || []
|
|
1980
2458
|
const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0)
|
|
1981
2459
|
const threadBlocks = threads.length === 0
|
|
1982
|
-
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, '本轮无线程')]
|
|
1983
|
-
: threads.map((
|
|
2460
|
+
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, t('本轮无线程'))]
|
|
2461
|
+
: threads.map((th, ti) => {
|
|
1984
2462
|
const key = `${ri}-${ti}`
|
|
1985
2463
|
const expanded = isThreadExpanded(key)
|
|
1986
|
-
const dim =
|
|
1987
|
-
const tFindings =
|
|
1988
|
-
const files =
|
|
1989
|
-
const msgs =
|
|
2464
|
+
const dim = th.dimension || t('未命名维度')
|
|
2465
|
+
const tFindings = th.findings || []
|
|
2466
|
+
const files = th.readFiles || []
|
|
2467
|
+
const msgs = th.messages || []
|
|
1990
2468
|
return React.createElement('div', { key, className: 'iterate-obs-block' },
|
|
1991
|
-
React.createElement('div', { className: 'iterate-obs-block-head', 'data-click': '', onClick: () => toggleThread(key), title: expanded ? '收起线程' : '展开线程' },
|
|
2469
|
+
React.createElement('div', { className: 'iterate-obs-block-head', 'data-click': '', onClick: () => toggleThread(key), title: expanded ? t('收起线程') : t('展开线程') },
|
|
1992
2470
|
React.createElement('span', {}, expanded ? '−' : '+'),
|
|
1993
2471
|
React.createElement('b', {}, dim),
|
|
1994
|
-
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof
|
|
2472
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof th.attempt === 'number' ? th.attempt : '?'}`),
|
|
1995
2473
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${tFindings.length} findings`),
|
|
1996
2474
|
),
|
|
1997
2475
|
expanded
|
|
1998
2476
|
? React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1999
2477
|
React.createElement('div', { className: 'iterate-obs-bar' },
|
|
2000
|
-
React.createElement('b', {}, '叙述'),
|
|
2001
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2478
|
+
React.createElement('b', {}, t('叙述')),
|
|
2479
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`{a0} 条`, { a0: msgs.length })),
|
|
2002
2480
|
),
|
|
2003
2481
|
...(msgs.length === 0
|
|
2004
|
-
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, '无线索叙述')]
|
|
2482
|
+
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, t('无线索叙述'))]
|
|
2005
2483
|
: msgs.map((m, mi) => React.createElement('div', { key: mi, className: 'iterate-obs-msg' }, String(m)))),
|
|
2006
2484
|
...(files.length > 0
|
|
2007
2485
|
? [
|
|
2008
2486
|
React.createElement('div', { key: 'files-h', className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2009
|
-
React.createElement('b', {}, '读取文件'),
|
|
2487
|
+
React.createElement('b', {}, t('读取文件')),
|
|
2010
2488
|
),
|
|
2011
2489
|
...files.map((f, fi) =>
|
|
2012
2490
|
React.createElement('div', { key: `f${fi}`, className: 'iterate-obs-file' }, String(f)),
|
|
@@ -2014,10 +2492,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2014
2492
|
]
|
|
2015
2493
|
: []),
|
|
2016
2494
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2017
|
-
React.createElement('b', {}, '发现'),
|
|
2495
|
+
React.createElement('b', {}, t('发现')),
|
|
2018
2496
|
),
|
|
2019
2497
|
...(tFindings.length === 0
|
|
2020
|
-
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, '本线程无发现')]
|
|
2498
|
+
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, t('本线程无发现'))]
|
|
2021
2499
|
: tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))),
|
|
2022
2500
|
)
|
|
2023
2501
|
: null,
|
|
@@ -2026,7 +2504,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2026
2504
|
return React.createElement('div', { key: ri, className: 'iterate-obs-block' },
|
|
2027
2505
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2028
2506
|
React.createElement('b', {}, `Round ${typeof r.round === 'number' ? r.round : ri + 1}`),
|
|
2029
|
-
React.createElement('span', { className: 'iterate-obs-chip' },
|
|
2507
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, tf(`{a0} 线程`, { a0: threads.length })),
|
|
2030
2508
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `${fCount} findings`),
|
|
2031
2509
|
),
|
|
2032
2510
|
React.createElement('div', { className: 'iterate-obs-block-body' }, ...threadBlocks),
|
|
@@ -2038,14 +2516,69 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2038
2516
|
const renderTrend = () => {
|
|
2039
2517
|
const conv = manifest.convergence || []
|
|
2040
2518
|
if (conv.length === 0) {
|
|
2041
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无收敛数据')
|
|
2519
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无收敛数据'))
|
|
2042
2520
|
}
|
|
2043
2521
|
const points = conv.map((n, i) => ({ round: i + 1, count: n }))
|
|
2522
|
+
// Gap #13 (step ①): single-session cross-round comparison. One row per
|
|
2523
|
+
// round — findings raised / fixed / severity mix / validation outcome.
|
|
2524
|
+
// Cross-SESSION comparison rides on the #4 pull instruction instead (the
|
|
2525
|
+
// client has no filesystem access, so `iterate_history` aggregates have to
|
|
2526
|
+
// be echoed back into the session first).
|
|
2527
|
+
const report = latestReport(session)
|
|
2528
|
+
const comparison = buildRoundComparison(report, manifest.validations || [])
|
|
2529
|
+
const comparisonTable = comparison.length === 0
|
|
2530
|
+
? null
|
|
2531
|
+
: React.createElement('table', { className: 'iterate-obs-table' },
|
|
2532
|
+
React.createElement('thead', {},
|
|
2533
|
+
React.createElement('tr', {},
|
|
2534
|
+
React.createElement('th', {}, t('轮次')),
|
|
2535
|
+
React.createElement('th', {}, t('发现')),
|
|
2536
|
+
React.createElement('th', {}, t('已修复')),
|
|
2537
|
+
React.createElement('th', {}, t('严重度分布')),
|
|
2538
|
+
React.createElement('th', {}, t('验证')),
|
|
2539
|
+
),
|
|
2540
|
+
),
|
|
2541
|
+
React.createElement('tbody', {},
|
|
2542
|
+
...comparison.map((row) => {
|
|
2543
|
+
const sevText = Object.entries(row.severities)
|
|
2544
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
2545
|
+
.join(' · ')
|
|
2546
|
+
const anyFailed = row.validations.some((v) => v.allowed !== true)
|
|
2547
|
+
const valText = row.validations.length === 0
|
|
2548
|
+
? '—'
|
|
2549
|
+
: tf(`{a0}/{a1} 通过{a2}`, { a0: row.validations.filter((v) => v.allowed === true).length, a1: row.validations.length, a2: anyFailed ? t('(含失败)') : '' })
|
|
2550
|
+
return React.createElement('tr', { key: `cmp-${row.round}` },
|
|
2551
|
+
React.createElement('td', {}, `Round ${row.round}`),
|
|
2552
|
+
React.createElement('td', {}, String(row.findings)),
|
|
2553
|
+
React.createElement('td', {}, String(row.fixed)),
|
|
2554
|
+
React.createElement('td', {}, sevText || '—'),
|
|
2555
|
+
React.createElement('td', {
|
|
2556
|
+
style: anyFailed
|
|
2557
|
+
? { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 600 }
|
|
2558
|
+
: row.validations.length > 0
|
|
2559
|
+
? { color: 'var(--dsw-alias-state-success-primary)' }
|
|
2560
|
+
: undefined,
|
|
2561
|
+
}, valText),
|
|
2562
|
+
)
|
|
2563
|
+
}),
|
|
2564
|
+
),
|
|
2565
|
+
)
|
|
2044
2566
|
return React.createElement('div', {},
|
|
2045
2567
|
React.createElement(TrendChart, { points }),
|
|
2046
2568
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2047
|
-
|
|
2569
|
+
tf(`各轮发现数量:{a0}{a1}`, { a0: conv.join(' → '), a1: typeof manifest.round === 'number' ? tf(` · 当前 Round {a0}`, { a0: manifest.round }) : '' }),
|
|
2048
2570
|
),
|
|
2571
|
+
comparisonTable
|
|
2572
|
+
? React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2573
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2574
|
+
React.createElement('span', {}, t('跨轮对比(本会话)')),
|
|
2575
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2576
|
+
t('跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行'),
|
|
2577
|
+
),
|
|
2578
|
+
),
|
|
2579
|
+
React.createElement('div', { className: 'iterate-obs-block-body' }, comparisonTable),
|
|
2580
|
+
)
|
|
2581
|
+
: null,
|
|
2049
2582
|
)
|
|
2050
2583
|
}
|
|
2051
2584
|
|
|
@@ -2053,7 +2586,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2053
2586
|
const renderFindings = () => {
|
|
2054
2587
|
const findings = manifest.findings || []
|
|
2055
2588
|
if (findings.length === 0) {
|
|
2056
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
2589
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无发现'))
|
|
2057
2590
|
}
|
|
2058
2591
|
const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
|
|
2059
2592
|
const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
|
|
@@ -2067,26 +2600,26 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2067
2600
|
}))
|
|
2068
2601
|
}
|
|
2069
2602
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
2070
|
-
React.createElement('b', {}, '发现'),
|
|
2603
|
+
React.createElement('b', {}, t('发现')),
|
|
2071
2604
|
...opts.severities.map((s) =>
|
|
2072
2605
|
React.createElement('button', {
|
|
2073
2606
|
key: `sev-${s.value}`,
|
|
2074
2607
|
className: 'iterate-filter-chip',
|
|
2075
2608
|
'data-active': f3Filter.severities.includes(s.value) ? '' : undefined,
|
|
2076
|
-
title:
|
|
2609
|
+
title: tf(`{a0}({a1} 项)`, { a0: s.value, a1: s.count }),
|
|
2077
2610
|
onClick: () => toggleSeverity(s.value),
|
|
2078
2611
|
}, `${s.value} ${s.count}`),
|
|
2079
2612
|
),
|
|
2080
2613
|
React.createElement('select', {
|
|
2081
2614
|
className: 'iterate-filter-select',
|
|
2082
2615
|
value: f3Filter.dimensions[0] || '',
|
|
2083
|
-
'aria-label': '按维度筛选',
|
|
2616
|
+
'aria-label': t('按维度筛选'),
|
|
2084
2617
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
2085
2618
|
const v = e.target.value
|
|
2086
2619
|
setF3Filter((prev) => ({ ...prev, dimensions: v ? [v] : [] }))
|
|
2087
2620
|
},
|
|
2088
2621
|
},
|
|
2089
|
-
React.createElement('option', { value: '' }, '全部维度'),
|
|
2622
|
+
React.createElement('option', { value: '' }, t('全部维度')),
|
|
2090
2623
|
...opts.dimensions.map((d) =>
|
|
2091
2624
|
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
2092
2625
|
),
|
|
@@ -2094,8 +2627,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2094
2627
|
React.createElement('input', {
|
|
2095
2628
|
className: 'iterate-filter-search',
|
|
2096
2629
|
type: 'search',
|
|
2097
|
-
placeholder: '搜索发现…',
|
|
2098
|
-
'aria-label': '搜索发现',
|
|
2630
|
+
placeholder: t('搜索发现…'),
|
|
2631
|
+
'aria-label': t('搜索发现'),
|
|
2099
2632
|
value: f3Filter.search,
|
|
2100
2633
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
2101
2634
|
setF3Filter((prev) => ({ ...prev, search: e.target.value })),
|
|
@@ -2104,8 +2637,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2104
2637
|
? React.createElement('button', {
|
|
2105
2638
|
className: 'iterate-filter-clear',
|
|
2106
2639
|
onClick: () => setF3Filter({ severities: [], dimensions: [], search: '' }),
|
|
2107
|
-
title: '清除全部筛选',
|
|
2108
|
-
}, '清除筛选')
|
|
2640
|
+
title: t('清除全部筛选'),
|
|
2641
|
+
}, t('清除筛选'))
|
|
2109
2642
|
: null,
|
|
2110
2643
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${findings.length}`),
|
|
2111
2644
|
)
|
|
@@ -2128,7 +2661,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2128
2661
|
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
2129
2662
|
React.createElement('span', {}, String(finding.dimension || '')),
|
|
2130
2663
|
finding.acknowledged === true
|
|
2131
|
-
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
2664
|
+
? React.createElement('span', { className: 'iterate-obs-chip' }, t('已确认'))
|
|
2132
2665
|
: null,
|
|
2133
2666
|
),
|
|
2134
2667
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2137,18 +2670,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2137
2670
|
React.createElement('button', {
|
|
2138
2671
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
2139
2672
|
onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
|
|
2140
|
-
title: '复制该 finding 的建议修复文本',
|
|
2141
|
-
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
2673
|
+
title: t('复制该 finding 的建议修复文本'),
|
|
2674
|
+
}, copiedKey === `${k}-fix` ? t('已复制') : t('复制 suggested_fix')),
|
|
2142
2675
|
React.createElement('button', {
|
|
2143
2676
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-known` ? '' : undefined,
|
|
2144
2677
|
onClick: () => copyInstruction(`${k}-known`, knownInstruction),
|
|
2145
|
-
title: '复制 iterate_triage 指令文本(标记为 known_intentional)',
|
|
2146
|
-
}, copiedKey === `${k}-known` ? '已复制' : 'known_intentional'),
|
|
2678
|
+
title: t('复制 iterate_triage 指令文本(标记为 known_intentional)'),
|
|
2679
|
+
}, copiedKey === `${k}-known` ? t('已复制') : 'known_intentional'),
|
|
2147
2680
|
React.createElement('button', {
|
|
2148
2681
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `${k}-repair` ? '' : undefined,
|
|
2149
2682
|
onClick: () => copyInstruction(`${k}-repair`, fixInstruction),
|
|
2150
|
-
title: '复制 iterate_fix 指令文本',
|
|
2151
|
-
}, copiedKey === `${k}-repair` ? '已复制' : '复制修复指令'),
|
|
2683
|
+
title: t('复制 iterate_fix 指令文本'),
|
|
2684
|
+
}, copiedKey === `${k}-repair` ? t('已复制') : t('复制修复指令')),
|
|
2152
2685
|
),
|
|
2153
2686
|
),
|
|
2154
2687
|
)
|
|
@@ -2156,7 +2689,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2156
2689
|
if (filtered.length === 0) {
|
|
2157
2690
|
return React.createElement('div', {},
|
|
2158
2691
|
filterBar,
|
|
2159
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的发现'),
|
|
2692
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的发现')),
|
|
2160
2693
|
)
|
|
2161
2694
|
}
|
|
2162
2695
|
return React.createElement('div', {}, filterBar, ...findingBlocks)
|
|
@@ -2166,19 +2699,19 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2166
2699
|
const renderFixes = () => {
|
|
2167
2700
|
const fixes = manifest.fixes || []
|
|
2168
2701
|
if (fixes.length === 0) {
|
|
2169
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无修复记录')
|
|
2702
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无修复记录'))
|
|
2170
2703
|
}
|
|
2171
2704
|
return React.createElement('div', {}, ...fixes.map((f, i) => {
|
|
2172
2705
|
const k = `f4-${i}`
|
|
2173
2706
|
const id = String(f.id || `fix#${i + 1}`)
|
|
2174
|
-
const rollbackText =
|
|
2707
|
+
const rollbackText = buildRollbackInstruction(id)
|
|
2175
2708
|
const added = typeof f.linesAdded === 'number' ? f.linesAdded : 0
|
|
2176
2709
|
const removed = typeof f.linesRemoved === 'number' ? f.linesRemoved : 0
|
|
2177
2710
|
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
2178
2711
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2179
2712
|
React.createElement('b', {}, id),
|
|
2180
2713
|
React.createElement('span', { className: 'iterate-obs-file' }, String(f.file || '?')),
|
|
2181
|
-
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? '成功' : '失败'),
|
|
2714
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? t('成功') : t('失败')),
|
|
2182
2715
|
typeof f.round === 'number'
|
|
2183
2716
|
? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${f.round}`)
|
|
2184
2717
|
: null,
|
|
@@ -2191,8 +2724,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2191
2724
|
React.createElement('button', {
|
|
2192
2725
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === `${k}-rb` ? '' : undefined,
|
|
2193
2726
|
onClick: () => copyInstruction(`${k}-rb`, rollbackText),
|
|
2194
|
-
title: '复制 iterate_rollback 指令文本',
|
|
2195
|
-
}, copiedKey === `${k}-rb` ? '已复制' : '回滚'),
|
|
2727
|
+
title: t('复制 iterate_rollback 指令文本'),
|
|
2728
|
+
}, copiedKey === `${k}-rb` ? t('已复制') : t('回滚')),
|
|
2196
2729
|
),
|
|
2197
2730
|
),
|
|
2198
2731
|
)
|
|
@@ -2203,23 +2736,24 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2203
2736
|
const renderCheckpoint = () => {
|
|
2204
2737
|
const cp = manifest.checkpoint
|
|
2205
2738
|
if (!cp) {
|
|
2206
|
-
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2739
|
+
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2740
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_transcript'), t('断点'), getLang()),
|
|
2741
|
+
)
|
|
2207
2742
|
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}, null, 2)}\n\`\`\``
|
|
2743
|
+
// Gap #10: `resume` accepts ONLY `operation` — `mode` / `round` /
|
|
2744
|
+
// `maxRounds` are save-side inputs and `maxRounds: null` was an
|
|
2745
|
+
// invalid-integer field on a positive-integer schema.
|
|
2746
|
+
const resumeText = buildCheckpointResumeInstruction()
|
|
2213
2747
|
// Clearing a stale checkpoint after an interrupted run — iterate_checkpoint
|
|
2214
2748
|
// exposes `clear`, but before this button it was only reachable by typing
|
|
2215
2749
|
// the command manually (an interrupted run otherwise shows a permanent
|
|
2216
2750
|
// "可恢复" chip with no companion reset action).
|
|
2217
|
-
const clearText =
|
|
2751
|
+
const clearText = buildCheckpointClearInstruction()
|
|
2218
2752
|
const item = (label: string, value: unknown) =>
|
|
2219
2753
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
2220
2754
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2221
2755
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2222
|
-
React.createElement('b', {}, '断点'),
|
|
2756
|
+
React.createElement('b', {}, t('断点')),
|
|
2223
2757
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, String(cp.updatedAt || '')),
|
|
2224
2758
|
),
|
|
2225
2759
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2234,13 +2768,13 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2234
2768
|
React.createElement('button', {
|
|
2235
2769
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'cp-resume' ? '' : undefined,
|
|
2236
2770
|
onClick: () => copyInstruction('cp-resume', resumeText),
|
|
2237
|
-
title: '复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)',
|
|
2238
|
-
}, copiedKey === 'cp-resume' ? '已复制' : '复制恢复指令'),
|
|
2771
|
+
title: t('复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)'),
|
|
2772
|
+
}, copiedKey === 'cp-resume' ? t('已复制') : t('复制恢复指令')),
|
|
2239
2773
|
React.createElement('button', {
|
|
2240
2774
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'cp-clear' ? '' : undefined,
|
|
2241
2775
|
onClick: () => copyInstruction('cp-clear', clearText),
|
|
2242
|
-
title: '复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)',
|
|
2243
|
-
}, copiedKey === 'cp-clear' ? '已复制' : '清除断点'),
|
|
2776
|
+
title: t('复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)'),
|
|
2777
|
+
}, copiedKey === 'cp-clear' ? t('已复制') : t('清除断点')),
|
|
2244
2778
|
),
|
|
2245
2779
|
),
|
|
2246
2780
|
)
|
|
@@ -2249,40 +2783,84 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2249
2783
|
// ── F6: run console (status / approval / nudge draft) ──────────────────
|
|
2250
2784
|
const renderConsole = () => {
|
|
2251
2785
|
const approval = manifest.approval || { policy: 'ask' }
|
|
2252
|
-
const policyLabelMap: Record<string, string> = { ask: '询问用户', deny: '拒绝', allow: '直接执行' }
|
|
2786
|
+
const policyLabelMap: Record<string, string> = { ask: t('询问用户'), deny: t('拒绝'), allow: t('直接执行') }
|
|
2253
2787
|
const policyLabel = policyLabelMap[String(approval.policy || 'ask')] || String(approval.policy || 'ask')
|
|
2254
2788
|
const nudge = manifest.nudge || null
|
|
2255
2789
|
const present = Boolean(nudge && nudge.text)
|
|
2256
2790
|
const activeNudgeText = nudge && nudge.text ? nudge.text : ''
|
|
2257
|
-
const nudgeInstruction = `请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n
|
|
2791
|
+
const nudgeInstruction = tf(`请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'nudge', text: nudgeText }, null, 2) })
|
|
2258
2792
|
// Clearing the PERSISTED nudge (as opposed to the draft) requires
|
|
2259
2793
|
// text:null. Copied as a paste-able instruction, mirroring the panel's
|
|
2260
2794
|
// copy-to-command pattern — it cannot be cleared from the client directly.
|
|
2261
|
-
const activeClearInstruction = `请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n
|
|
2795
|
+
const activeClearInstruction = tf(`请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'nudge', text: null }, null, 2) })
|
|
2796
|
+
// Gap #6: per-round validation rows (command / exitCode / allowed) live on
|
|
2797
|
+
// the manifest; when the run stopped on a validation failure the failing
|
|
2798
|
+
// command is called out explicitly instead of being buried in a list.
|
|
2799
|
+
const validations = Array.isArray(manifest.validations) ? manifest.validations : []
|
|
2800
|
+
const abortedByValidation = String(manifest.stoppedReason || '') === 'aborted_by_validation'
|
|
2801
|
+
const validationRows = validations.map((v, i) => {
|
|
2802
|
+
const allowed = v.allowed === true
|
|
2803
|
+
const failed = !allowed
|
|
2804
|
+
const highlight = failed && abortedByValidation
|
|
2805
|
+
const exit = typeof v.exitCode === 'number' ? `exit ${v.exitCode}` : 'exit —'
|
|
2806
|
+
return React.createElement('div', {
|
|
2807
|
+
key: `val-${i}`,
|
|
2808
|
+
className: 'iterate-obs-row',
|
|
2809
|
+
'data-status': allowed ? 'pass' : 'fail',
|
|
2810
|
+
'data-highlight': highlight ? '' : undefined,
|
|
2811
|
+
title: v.rejectReason ? String(v.rejectReason) : undefined,
|
|
2812
|
+
},
|
|
2813
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${typeof v.round === 'number' ? v.round : '?'}`),
|
|
2814
|
+
React.createElement('code', { className: 'iterate-obs-mono' }, String(v.command || '')),
|
|
2815
|
+
React.createElement('span', { className: 'iterate-obs-chip', 'data-status': allowed ? 'pass' : 'fail' },
|
|
2816
|
+
allowed ? t('通过') : t('失败'),
|
|
2817
|
+
),
|
|
2818
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, exit),
|
|
2819
|
+
highlight ? React.createElement('span', { className: 'iterate-obs-chip', 'data-status': 'fail' }, t('导致中止')) : null,
|
|
2820
|
+
)
|
|
2821
|
+
})
|
|
2822
|
+
const validationsBlock = React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2823
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2824
|
+
React.createElement('span', {}, t('验证结果')),
|
|
2825
|
+
validations.length === 0
|
|
2826
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, t('本轮尚无验证记录'))
|
|
2827
|
+
: React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`{a0} 条命令`, { a0: validations.length })),
|
|
2828
|
+
abortedByValidation
|
|
2829
|
+
? React.createElement('span', { className: 'iterate-obs-badge', 'data-status': 'fail' }, t('因验证失败中止'))
|
|
2830
|
+
: null,
|
|
2831
|
+
),
|
|
2832
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2833
|
+
validations.length === 0
|
|
2834
|
+
? React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2835
|
+
t('尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。'),
|
|
2836
|
+
)
|
|
2837
|
+
: React.createElement(React.Fragment, null, ...validationRows),
|
|
2838
|
+
),
|
|
2839
|
+
)
|
|
2262
2840
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2263
2841
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2264
|
-
React.createElement('span', {}, '运行控制台'),
|
|
2842
|
+
React.createElement('span', {}, t('运行控制台')),
|
|
2265
2843
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2266
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2844
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`批准策略:{a0}`, { a0: policyLabel })),
|
|
2267
2845
|
),
|
|
2268
2846
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2269
2847
|
present
|
|
2270
2848
|
? React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 6 } },
|
|
2271
|
-
React.createElement('b', {}, '当前 nudge'),
|
|
2849
|
+
React.createElement('b', {}, t('当前 nudge')),
|
|
2272
2850
|
React.createElement('span', { className: 'iterate-obs-msg' }, activeNudgeText),
|
|
2273
2851
|
React.createElement('button', {
|
|
2274
2852
|
className: 'iterate-btn', 'data-copied': copiedKey === 'nudge-clear' ? '' : undefined,
|
|
2275
2853
|
onClick: () => copyInstruction('nudge-clear', activeClearInstruction),
|
|
2276
|
-
title: '复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge',
|
|
2277
|
-
}, copiedKey === 'nudge-clear' ? '已复制清除指令' : '复制清除指令'),
|
|
2854
|
+
title: t('复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge'),
|
|
2855
|
+
}, copiedKey === 'nudge-clear' ? t('已复制清除指令') : t('复制清除指令')),
|
|
2278
2856
|
)
|
|
2279
2857
|
: null,
|
|
2280
2858
|
React.createElement('textarea', {
|
|
2281
2859
|
className: 'iterate-obs-input iterate-obs-mono',
|
|
2282
2860
|
rows: 3,
|
|
2283
|
-
placeholder: '起草一条 nudge 方向指令…',
|
|
2861
|
+
placeholder: t('起草一条 nudge 方向指令…'),
|
|
2284
2862
|
value: nudgeText,
|
|
2285
|
-
'aria-label': 'nudge 草稿',
|
|
2863
|
+
'aria-label': t('nudge 草稿'),
|
|
2286
2864
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setNudgeText(e.target.value),
|
|
2287
2865
|
}),
|
|
2288
2866
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
@@ -2290,9 +2868,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2290
2868
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'nudge' ? '' : undefined,
|
|
2291
2869
|
disabled: !nudgeText,
|
|
2292
2870
|
onClick: () => copyInstruction('nudge', nudgeInstruction),
|
|
2293
|
-
title: '复制 iterate_transcript nudge 指令文本',
|
|
2294
|
-
}, copiedKey === 'nudge' ? '已复制' : '复制 nudge 指令'),
|
|
2871
|
+
title: t('复制 iterate_transcript nudge 指令文本'),
|
|
2872
|
+
}, copiedKey === 'nudge' ? t('已复制') : t('复制 nudge 指令')),
|
|
2295
2873
|
),
|
|
2874
|
+
validationsBlock,
|
|
2296
2875
|
),
|
|
2297
2876
|
)
|
|
2298
2877
|
}
|
|
@@ -2301,7 +2880,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2301
2880
|
const renderTimeline = () => {
|
|
2302
2881
|
const entries = manifest.timeline || []
|
|
2303
2882
|
if (entries.length === 0) {
|
|
2304
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无时间线条目')
|
|
2883
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无时间线条目'))
|
|
2305
2884
|
}
|
|
2306
2885
|
const types = Array.from(new Set(entries.map((t) => String(t.type || 'unknown')).filter(Boolean))).sort()
|
|
2307
2886
|
const rounds = Array.from(
|
|
@@ -2337,30 +2916,30 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2337
2916
|
)
|
|
2338
2917
|
})
|
|
2339
2918
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2340
|
-
React.createElement('b', {}, '时间线'),
|
|
2919
|
+
React.createElement('b', {}, t('时间线')),
|
|
2341
2920
|
React.createElement('select', {
|
|
2342
2921
|
className: 'iterate-filter-select',
|
|
2343
2922
|
value: timelineType,
|
|
2344
|
-
'aria-label': '按时间线类型筛选',
|
|
2923
|
+
'aria-label': t('按时间线类型筛选'),
|
|
2345
2924
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineType(e.target.value),
|
|
2346
2925
|
},
|
|
2347
|
-
React.createElement('option', { value: '' }, '全部类型'),
|
|
2926
|
+
React.createElement('option', { value: '' }, t('全部类型')),
|
|
2348
2927
|
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
2349
2928
|
),
|
|
2350
2929
|
React.createElement('select', {
|
|
2351
2930
|
className: 'iterate-filter-select',
|
|
2352
2931
|
value: timelineRound,
|
|
2353
|
-
'aria-label': '按轮次筛选',
|
|
2932
|
+
'aria-label': t('按轮次筛选'),
|
|
2354
2933
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineRound(e.target.value),
|
|
2355
2934
|
},
|
|
2356
|
-
React.createElement('option', { value: '' }, '全部轮次'),
|
|
2935
|
+
React.createElement('option', { value: '' }, t('全部轮次')),
|
|
2357
2936
|
...rounds.map((r) => React.createElement('option', { key: r, value: r }, `Round ${r}`)),
|
|
2358
2937
|
),
|
|
2359
2938
|
React.createElement('input', {
|
|
2360
2939
|
className: 'iterate-filter-search',
|
|
2361
2940
|
type: 'search',
|
|
2362
|
-
placeholder: '搜索时间线…',
|
|
2363
|
-
'aria-label': '搜索时间线',
|
|
2941
|
+
placeholder: t('搜索时间线…'),
|
|
2942
|
+
'aria-label': t('搜索时间线'),
|
|
2364
2943
|
value: timelineSearch,
|
|
2365
2944
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setTimelineSearch(e.target.value),
|
|
2366
2945
|
}),
|
|
@@ -2368,15 +2947,15 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2368
2947
|
? React.createElement('button', {
|
|
2369
2948
|
className: 'iterate-filter-clear',
|
|
2370
2949
|
onClick: clearTimelineFilter,
|
|
2371
|
-
title: '清除全部筛选',
|
|
2372
|
-
}, '清除')
|
|
2950
|
+
title: t('清除全部筛选'),
|
|
2951
|
+
}, t('清除'))
|
|
2373
2952
|
: null,
|
|
2374
2953
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${entries.length}`),
|
|
2375
2954
|
)
|
|
2376
2955
|
if (rows.length === 0) {
|
|
2377
2956
|
return React.createElement('div', {},
|
|
2378
2957
|
filterBar,
|
|
2379
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的时间线条目'),
|
|
2958
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的时间线条目')),
|
|
2380
2959
|
)
|
|
2381
2960
|
}
|
|
2382
2961
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
@@ -2387,29 +2966,40 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2387
2966
|
// session (dimension convergence, verification pass rate, overall PASS/FAIL);
|
|
2388
2967
|
// falls back to copy-able query instructions when no snapshot exists yet.
|
|
2389
2968
|
const renderQualityGate = () => {
|
|
2390
|
-
|
|
2969
|
+
// Gap #10: the query instruction now carries explicit JSON args, matching
|
|
2970
|
+
// the rest of the panel's copy-to-command payloads (the model no longer
|
|
2971
|
+
// has to infer `operation`).
|
|
2972
|
+
const gateInstruction = buildQualityGateQueryInstruction()
|
|
2391
2973
|
// Reset a stale FAIL certificate before a fresh iteration — the tool's
|
|
2392
2974
|
// `clear` operation existed since v3.5.1 but had no client entry point.
|
|
2393
|
-
const gateClearInstruction =
|
|
2975
|
+
const gateClearInstruction = buildQualityGateClearInstruction()
|
|
2394
2976
|
const gate = qualityGate
|
|
2395
2977
|
const dims = gate && gate.dimensions ? gate.dimensions : []
|
|
2396
2978
|
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
|
|
2397
2979
|
const status = gate && gate.overallStatus ? String(gate.overallStatus) : 'pending'
|
|
2398
2980
|
const statusLabel = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'PENDING'
|
|
2399
2981
|
const num = (v: number | undefined) => Number(v != null ? v : 0)
|
|
2982
|
+
const pulled = toolCalledInSession(session, 'iterate_quality_gate')
|
|
2400
2983
|
|
|
2401
2984
|
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2402
|
-
React.createElement('b', {}, '质量门禁'),
|
|
2985
|
+
React.createElement('b', {}, t('质量门禁')),
|
|
2403
2986
|
React.createElement('button', {
|
|
2404
2987
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2405
2988
|
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2406
|
-
title: '复制 iterate_quality_gate
|
|
2407
|
-
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2989
|
+
title: t('复制 iterate_quality_gate 查询指令(含 JSON 参数)'),
|
|
2990
|
+
}, copiedKey === 'qgate' ? t('已复制') : t('查询门禁')),
|
|
2408
2991
|
React.createElement('button', {
|
|
2409
2992
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
|
|
2410
2993
|
onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
|
|
2411
|
-
title: '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)',
|
|
2412
|
-
}, copiedKey === 'qgate-clear' ? '已复制' : '清除门禁'),
|
|
2994
|
+
title: t('复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)'),
|
|
2995
|
+
}, copiedKey === 'qgate-clear' ? t('已复制') : t('清除门禁')),
|
|
2996
|
+
// Gap #4: the browser cannot read `.iterate/` directly — one click
|
|
2997
|
+
// copies the pull instruction that makes the model echo it into the stream.
|
|
2998
|
+
React.createElement('button', {
|
|
2999
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot' ? '' : undefined,
|
|
3000
|
+
onClick: () => copyInstruction('disk-snapshot', pullInstruction),
|
|
3001
|
+
title: t('复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript'),
|
|
3002
|
+
}, copiedKey === 'disk-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2413
3003
|
)
|
|
2414
3004
|
|
|
2415
3005
|
if (!hasGate) {
|
|
@@ -2417,17 +3007,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2417
3007
|
headerBar,
|
|
2418
3008
|
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2419
3009
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2420
|
-
React.createElement('span', {}, '质量门禁视图'),
|
|
3010
|
+
React.createElement('span', {}, t('质量门禁视图')),
|
|
2421
3011
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2422
3012
|
),
|
|
2423
3013
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2424
|
-
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3014
|
+
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3015
|
+
// Gap #4: distinguish "we pulled it and it is empty" from "we
|
|
3016
|
+
// never asked the model to read the disk".
|
|
3017
|
+
diskEmptyStateText(pulled, t('质量门禁证书'), getLang()),
|
|
3018
|
+
),
|
|
3019
|
+
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3020
|
+
t('质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
|
|
3021
|
+
),
|
|
2425
3022
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2426
3023
|
React.createElement('button', {
|
|
2427
3024
|
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2428
3025
|
onClick: () => copyInstruction('qgate-instr', gateInstruction),
|
|
2429
|
-
title: '复制查询指令',
|
|
2430
|
-
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
3026
|
+
title: t('复制查询指令'),
|
|
3027
|
+
}, copiedKey === 'qgate-instr' ? t('已复制') : t('复制查询指令')),
|
|
3028
|
+
React.createElement('button', {
|
|
3029
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot-2' ? '' : undefined,
|
|
3030
|
+
onClick: () => copyInstruction('disk-snapshot-2', pullInstruction),
|
|
3031
|
+
title: t('复制拉取磁盘快照指令'),
|
|
3032
|
+
}, copiedKey === 'disk-snapshot-2' ? t('已复制') : t('拉取磁盘快照')),
|
|
2431
3033
|
),
|
|
2432
3034
|
),
|
|
2433
3035
|
),
|
|
@@ -2440,7 +3042,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2440
3042
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2441
3043
|
React.createElement('span', { className: 'iterate-gate', 'data-status': status }, statusLabel),
|
|
2442
3044
|
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2443
|
-
`整体评分
|
|
3045
|
+
tf(`整体评分 {a0} · 验证 {a1}/{a2} · 通过率 {a3}%`, { a0: num(gate && gate.overallScore), a1: num(gate && gate.passedChecks), a2: num(gate && gate.totalChecks), a3: num(gate && gate.verificationPassRate) }),
|
|
2444
3046
|
),
|
|
2445
3047
|
),
|
|
2446
3048
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2448,7 +3050,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2448
3050
|
? React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6 } },
|
|
2449
3051
|
...dims.map((d, i) =>
|
|
2450
3052
|
React.createElement('span', { key: `gdim-${i}`, className: 'iterate-obs-chip' },
|
|
2451
|
-
|
|
3053
|
+
tf(`{a0} · 收敛 {a1}% · 评分 {a2}`, { a0: String(d.dimension || '?'), a1: num(d.convergenceRate), a2: num(d.score) }),
|
|
2452
3054
|
),
|
|
2453
3055
|
),
|
|
2454
3056
|
)
|
|
@@ -2465,12 +3067,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2465
3067
|
style: { width: `${fill}%` },
|
|
2466
3068
|
}),
|
|
2467
3069
|
),
|
|
2468
|
-
React.createElement('span', {}, `评分
|
|
3070
|
+
React.createElement('span', {}, tf(`评分 {a0} · 发现 {a1} · 已修复 {a2}`, { a0: num(d.score), a1: num(d.findingsCount), a2: num(d.fixedCount) })),
|
|
2469
3071
|
)
|
|
2470
3072
|
}),
|
|
2471
3073
|
),
|
|
2472
3074
|
gate && gate.failReason
|
|
2473
|
-
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3075
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } }, tf(`失败原因:{a0}`, { a0: String(gate.failReason) }))
|
|
2474
3076
|
: null,
|
|
2475
3077
|
),
|
|
2476
3078
|
),
|
|
@@ -2483,8 +3085,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2483
3085
|
// (list all / search / adopt one entry).
|
|
2484
3086
|
const renderExperienceBank = () => {
|
|
2485
3087
|
const expInstruction = expSearch
|
|
2486
|
-
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n
|
|
2487
|
-
:
|
|
3088
|
+
? tf(`请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'search', query: expSearch }, null, 2) })
|
|
3089
|
+
: buildExperienceListInstruction()
|
|
2488
3090
|
|
|
2489
3091
|
const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
|
|
2490
3092
|
const query = expSearch.trim().toLowerCase()
|
|
@@ -2495,14 +3097,15 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2495
3097
|
.some((t) => String(t).toLowerCase().includes(query)),
|
|
2496
3098
|
)
|
|
2497
3099
|
: expEntries
|
|
3100
|
+
const pulled = toolCalledInSession(session, 'iterate_experience')
|
|
2498
3101
|
|
|
2499
3102
|
const resultRows = visible.map((e, i) => {
|
|
2500
|
-
const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n
|
|
3103
|
+
const adoptInstruction = tf(`请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'get', id: e.id }, null, 2) })
|
|
2501
3104
|
return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-block' },
|
|
2502
3105
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2503
3106
|
React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
|
|
2504
3107
|
React.createElement('span', {}, String(e.pattern || '?')),
|
|
2505
|
-
React.createElement('span', { className: 'iterate-exp-hit' }, `命中
|
|
3108
|
+
React.createElement('span', { className: 'iterate-exp-hit' }, tf(`命中 {a0} 次`, { a0: e.hitCount ?? 0 })),
|
|
2506
3109
|
),
|
|
2507
3110
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2508
3111
|
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
@@ -2511,8 +3114,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2511
3114
|
? React.createElement('button', {
|
|
2512
3115
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `exp-adopt-${i}` ? '' : undefined,
|
|
2513
3116
|
onClick: () => copyInstruction(`exp-adopt-${i}`, adoptInstruction),
|
|
2514
|
-
title: '复制采纳指令(查询该经验并应用到当前上下文)',
|
|
2515
|
-
}, copiedKey === `exp-adopt-${i}` ? '已复制' : '采纳')
|
|
3117
|
+
title: t('复制采纳指令(查询该经验并应用到当前上下文)'),
|
|
3118
|
+
}, copiedKey === `exp-adopt-${i}` ? t('已复制') : t('采纳'))
|
|
2516
3119
|
: null,
|
|
2517
3120
|
),
|
|
2518
3121
|
),
|
|
@@ -2521,20 +3124,25 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2521
3124
|
|
|
2522
3125
|
return React.createElement('div', {},
|
|
2523
3126
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2524
|
-
React.createElement('b', {}, '经验银行'),
|
|
2525
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内
|
|
3127
|
+
React.createElement('b', {}, t('经验银行')),
|
|
3128
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`会话内 {a0} 条`, { a0: expEntries.length })),
|
|
2526
3129
|
React.createElement('button', {
|
|
2527
3130
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2528
|
-
onClick: () => copyInstruction('exp-list',
|
|
2529
|
-
title: '
|
|
2530
|
-
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
3131
|
+
onClick: () => copyInstruction('exp-list', buildExperienceListInstruction()),
|
|
3132
|
+
title: t('复制列出经验指令(含 JSON 参数)'),
|
|
3133
|
+
}, copiedKey === 'exp-list' ? t('已复制') : t('列出经验')),
|
|
3134
|
+
React.createElement('button', {
|
|
3135
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-snapshot' ? '' : undefined,
|
|
3136
|
+
onClick: () => copyInstruction('exp-snapshot', pullInstruction),
|
|
3137
|
+
title: t('复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话'),
|
|
3138
|
+
}, copiedKey === 'exp-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2531
3139
|
),
|
|
2532
3140
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2533
3141
|
React.createElement('input', {
|
|
2534
3142
|
className: 'iterate-filter-search',
|
|
2535
3143
|
type: 'search',
|
|
2536
|
-
placeholder: '搜索经验模式…',
|
|
2537
|
-
'aria-label': '搜索经验',
|
|
3144
|
+
placeholder: t('搜索经验模式…'),
|
|
3145
|
+
'aria-label': t('搜索经验'),
|
|
2538
3146
|
value: expSearch,
|
|
2539
3147
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
|
|
2540
3148
|
}),
|
|
@@ -2542,14 +3150,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2542
3150
|
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
|
|
2543
3151
|
disabled: !expSearch,
|
|
2544
3152
|
onClick: () => copyInstruction('exp-search', expInstruction),
|
|
2545
|
-
title: '复制搜索指令',
|
|
2546
|
-
}, copiedKey === 'exp-search' ? '已复制' : '搜索'),
|
|
3153
|
+
title: t('复制搜索指令'),
|
|
3154
|
+
}, copiedKey === 'exp-search' ? t('已复制') : t('搜索')),
|
|
2547
3155
|
),
|
|
2548
3156
|
resultRows.length > 0
|
|
2549
3157
|
? React.createElement('div', {}, ...resultRows)
|
|
2550
3158
|
: expEntries.length > 0
|
|
2551
|
-
? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
|
|
2552
|
-
: React.createElement('div', {
|
|
3159
|
+
? React.createElement('div', { className: 'iterate-obs-empty' }, t('没有匹配「') + expSearch + t('」的经验'))
|
|
3160
|
+
: React.createElement('div', {},
|
|
3161
|
+
// Gap #4: separate "pulled and empty" from "never pulled".
|
|
3162
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, diskEmptyStateText(pulled, t('经验银行'), getLang())),
|
|
3163
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('也可点「列出经验」查询磁盘上的全部历史经验。')),
|
|
3164
|
+
),
|
|
2553
3165
|
)
|
|
2554
3166
|
}
|
|
2555
3167
|
|
|
@@ -2559,21 +3171,21 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2559
3171
|
// falsifications); copy buttons reach the back end for the full history.
|
|
2560
3172
|
const renderDefenseEvents = () => {
|
|
2561
3173
|
const defenseInstruction = defenseFilter
|
|
2562
|
-
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n
|
|
2563
|
-
:
|
|
3174
|
+
? tf(`请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2) })
|
|
3175
|
+
: buildDefenseEventsListInstruction()
|
|
2564
3176
|
|
|
2565
3177
|
const typeOptions = [
|
|
2566
|
-
{ value: '', label: '全部类型' },
|
|
2567
|
-
{ value: 'precondition_failed', label: '前置校验失败' },
|
|
2568
|
-
{ value: 'rollback', label: '回滚' },
|
|
2569
|
-
{ value: 'invariant_violated', label: '不变量违反' },
|
|
2570
|
-
{ value: 'assumption_falsified', label: '假设被证伪' },
|
|
3178
|
+
{ value: '', label: t('全部类型') },
|
|
3179
|
+
{ value: 'precondition_failed', label: t('前置校验失败') },
|
|
3180
|
+
{ value: 'rollback', label: t('回滚') },
|
|
3181
|
+
{ value: 'invariant_violated', label: t('不变量违反') },
|
|
3182
|
+
{ value: 'assumption_falsified', label: t('假设被证伪') },
|
|
2571
3183
|
]
|
|
2572
3184
|
const TYPE_LABEL: Record<string, string> = {
|
|
2573
|
-
precondition_failed: '前置校验失败',
|
|
2574
|
-
rollback: '回滚',
|
|
2575
|
-
invariant_violated: '不变量违反',
|
|
2576
|
-
assumption_falsified: '假设被证伪',
|
|
3185
|
+
precondition_failed: t('前置校验失败'),
|
|
3186
|
+
rollback: t('回滚'),
|
|
3187
|
+
invariant_violated: t('不变量违反'),
|
|
3188
|
+
assumption_falsified: t('假设被证伪'),
|
|
2577
3189
|
}
|
|
2578
3190
|
|
|
2579
3191
|
const counts = (defenseEvents && defenseEvents.counts) || {}
|
|
@@ -2584,23 +3196,28 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2584
3196
|
: allEvents
|
|
2585
3197
|
|
|
2586
3198
|
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2587
|
-
React.createElement('b', {}, '防御事件'),
|
|
2588
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内
|
|
3199
|
+
React.createElement('b', {}, t('防御事件')),
|
|
3200
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`会话内 {a0} 条 / 累计 {a1}`, { a0: allEvents.length, a1: totalCount })),
|
|
2589
3201
|
React.createElement('button', {
|
|
2590
3202
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
2591
|
-
onClick: () => copyInstruction('defense-list',
|
|
2592
|
-
title: '
|
|
2593
|
-
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
3203
|
+
onClick: () => copyInstruction('defense-list', buildDefenseEventsListInstruction()),
|
|
3204
|
+
title: t('复制列出防御事件指令(含 JSON 参数)'),
|
|
3205
|
+
}, copiedKey === 'defense-list' ? t('已复制') : t('列出事件')),
|
|
3206
|
+
React.createElement('button', {
|
|
3207
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-snapshot' ? '' : undefined,
|
|
3208
|
+
onClick: () => copyInstruction('defense-snapshot', pullInstruction),
|
|
3209
|
+
title: t('复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话'),
|
|
3210
|
+
}, copiedKey === 'defense-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2594
3211
|
React.createElement('button', {
|
|
2595
3212
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
|
|
2596
|
-
onClick: () => copyInstruction('defense-counts',
|
|
2597
|
-
title: '
|
|
2598
|
-
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
3213
|
+
onClick: () => copyInstruction('defense-counts', tf(`请调用 \`iterate_defense_events\` 查询事件统计:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'counts' }, null, 2) })),
|
|
3214
|
+
title: t('复制统计指令(含 JSON 参数)'),
|
|
3215
|
+
}, copiedKey === 'defense-counts' ? t('已复制') : t('统计')),
|
|
2599
3216
|
React.createElement('button', {
|
|
2600
3217
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'defense-clear' ? '' : undefined,
|
|
2601
|
-
onClick: () => copyInstruction('defense-clear', `请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n
|
|
2602
|
-
title: '复制 iterate_defense_events clear 指令文本(重置陈旧事件流)',
|
|
2603
|
-
}, copiedKey === 'defense-clear' ? '已复制' : '清除事件'),
|
|
3218
|
+
onClick: () => copyInstruction('defense-clear', tf(`请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'clear' }, null, 2) })),
|
|
3219
|
+
title: t('复制 iterate_defense_events clear 指令文本(重置陈旧事件流)'),
|
|
3220
|
+
}, copiedKey === 'defense-clear' ? t('已复制') : t('清除事件')),
|
|
2604
3221
|
)
|
|
2605
3222
|
|
|
2606
3223
|
if (allEvents.length === 0 && totalCount === 0) {
|
|
@@ -2608,17 +3225,22 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2608
3225
|
headerBar,
|
|
2609
3226
|
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2610
3227
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2611
|
-
React.createElement('span', {}, '防御事件流'),
|
|
3228
|
+
React.createElement('span', {}, t('防御事件流')),
|
|
2612
3229
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2613
3230
|
),
|
|
2614
3231
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2615
|
-
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3232
|
+
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3233
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_defense_events'), t('防御事件流'), getLang()),
|
|
3234
|
+
),
|
|
3235
|
+
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3236
|
+
t('防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
|
|
3237
|
+
),
|
|
2616
3238
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2617
3239
|
React.createElement('button', {
|
|
2618
3240
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
2619
3241
|
onClick: () => copyInstruction('defense-instr', defenseInstruction),
|
|
2620
|
-
title: '复制查询指令',
|
|
2621
|
-
}, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
|
|
3242
|
+
title: t('复制查询指令'),
|
|
3243
|
+
}, copiedKey === 'defense-instr' ? t('已复制') : t('复制查询指令')),
|
|
2622
3244
|
),
|
|
2623
3245
|
),
|
|
2624
3246
|
),
|
|
@@ -2629,7 +3251,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2629
3251
|
React.createElement('select', {
|
|
2630
3252
|
className: 'iterate-filter-select',
|
|
2631
3253
|
value: defenseFilter,
|
|
2632
|
-
'aria-label': '按事件类型筛选',
|
|
3254
|
+
'aria-label': t('按事件类型筛选'),
|
|
2633
3255
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
|
|
2634
3256
|
},
|
|
2635
3257
|
...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
|
|
@@ -2638,8 +3260,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2638
3260
|
? React.createElement('button', {
|
|
2639
3261
|
className: 'iterate-filter-clear',
|
|
2640
3262
|
onClick: () => setDefenseFilter(''),
|
|
2641
|
-
title: '清除类型筛选',
|
|
2642
|
-
}, '清除')
|
|
3263
|
+
title: t('清除类型筛选'),
|
|
3264
|
+
}, t('清除'))
|
|
2643
3265
|
: null,
|
|
2644
3266
|
)
|
|
2645
3267
|
|
|
@@ -2659,7 +3281,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2659
3281
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2660
3282
|
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
2661
3283
|
e.defense
|
|
2662
|
-
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } },
|
|
3284
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } }, tf(`防线:{a0}`, { a0: String(e.defense) }))
|
|
2663
3285
|
: null,
|
|
2664
3286
|
),
|
|
2665
3287
|
)
|
|
@@ -2678,7 +3300,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2678
3300
|
),
|
|
2679
3301
|
eventRows.length > 0
|
|
2680
3302
|
? React.createElement('div', {}, ...eventRows)
|
|
2681
|
-
: React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配该类型的防御事件'),
|
|
3303
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, t('没有匹配该类型的防御事件')),
|
|
2682
3304
|
)
|
|
2683
3305
|
}
|
|
2684
3306
|
|
|
@@ -2693,25 +3315,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2693
3315
|
const hasFindings = (manifest.findings || []).length > 0
|
|
2694
3316
|
if (hasThreads || hasFindings) {
|
|
2695
3317
|
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2696
|
-
React.createElement('div', {}, '实时活动流仅在运行期间记录,当前已完成。'),
|
|
3318
|
+
React.createElement('div', {}, t('实时活动流仅在运行期间记录,当前已完成。')),
|
|
2697
3319
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2698
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f1') }, '查看审查线程'),
|
|
2699
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f3') }, '查看发现'),
|
|
3320
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f1') }, t('查看审查线程')),
|
|
3321
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f3') }, t('查看发现')),
|
|
2700
3322
|
),
|
|
2701
3323
|
)
|
|
2702
3324
|
}
|
|
2703
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无实时活动')
|
|
3325
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无实时活动'))
|
|
2704
3326
|
}
|
|
2705
3327
|
const types = Array.from(new Set(liveEntries.map((e) => String(e.type || 'unknown')).filter(Boolean))).sort()
|
|
2706
3328
|
const filtered = filterLiveEntries(liveEntries, liveType)
|
|
2707
3329
|
const rows = filtered.map((e, i) => {
|
|
2708
3330
|
const type = String(e.type || 'unknown')
|
|
2709
|
-
|
|
3331
|
+
// Module constants carry BOTH languages; resolve at render time so a
|
|
3332
|
+
// language switch repaints the badges.
|
|
3333
|
+
const meta = OBS_LIVE_META[type]
|
|
3334
|
+
const typeLabel = meta ? meta.label[getLang()] : String(e.type || '?')
|
|
3335
|
+
const typeColor = meta ? meta.color : OBS_LIVE_FALLBACK_COLOR
|
|
2710
3336
|
const tool = String(e.tool || '')
|
|
2711
3337
|
const target = String(e.target || '')
|
|
2712
3338
|
const time = formatObsLiveTime(String(e.ts || ''))
|
|
2713
3339
|
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
2714
|
-
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color:
|
|
3340
|
+
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: typeColor } }, typeLabel),
|
|
2715
3341
|
tool
|
|
2716
3342
|
? React.createElement('span', { className: 'iterate-obs-chip' }, tool)
|
|
2717
3343
|
: null,
|
|
@@ -2720,29 +3346,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2720
3346
|
)
|
|
2721
3347
|
})
|
|
2722
3348
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2723
|
-
React.createElement('b', {}, '实时活动'),
|
|
3349
|
+
React.createElement('b', {}, t('实时活动')),
|
|
2724
3350
|
React.createElement('select', {
|
|
2725
3351
|
className: 'iterate-filter-select',
|
|
2726
3352
|
value: liveType,
|
|
2727
|
-
'aria-label': '按活动类型筛选',
|
|
3353
|
+
'aria-label': t('按活动类型筛选'),
|
|
2728
3354
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setLiveType(e.target.value),
|
|
2729
3355
|
},
|
|
2730
|
-
React.createElement('option', { value: '' }, '全部活动'),
|
|
3356
|
+
React.createElement('option', { value: '' }, t('全部活动')),
|
|
2731
3357
|
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
2732
3358
|
),
|
|
2733
3359
|
liveType
|
|
2734
3360
|
? React.createElement('button', {
|
|
2735
3361
|
className: 'iterate-filter-clear',
|
|
2736
3362
|
onClick: () => setLiveType(''),
|
|
2737
|
-
title: '清除类型筛选',
|
|
2738
|
-
}, '清除')
|
|
3363
|
+
title: t('清除类型筛选'),
|
|
3364
|
+
}, t('清除'))
|
|
2739
3365
|
: null,
|
|
2740
3366
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${liveEntries.length}`),
|
|
2741
3367
|
)
|
|
2742
3368
|
if (rows.length === 0) {
|
|
2743
3369
|
return React.createElement('div', {},
|
|
2744
3370
|
filterBar,
|
|
2745
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的实时活动'),
|
|
3371
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的实时活动')),
|
|
2746
3372
|
)
|
|
2747
3373
|
}
|
|
2748
3374
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
@@ -2768,25 +3394,30 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2768
3394
|
|
|
2769
3395
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
2770
3396
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': open ? undefined : '', onClick: () => setOpen((v) => !v) },
|
|
2771
|
-
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
3397
|
+
React.createElement('span', { className: 'iterate-obs-title' }, t('iterate 观测台')),
|
|
2772
3398
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2773
3399
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, headMeta || 'runtime'),
|
|
3400
|
+
// Gap #14: make the tab system discoverable without expanding it.
|
|
3401
|
+
React.createElement('span', {
|
|
3402
|
+
className: 'iterate-obs-head-meta',
|
|
3403
|
+
title: obsHotkeyHint(),
|
|
3404
|
+
}, obsHotkeyHint()),
|
|
2774
3405
|
React.createElement('button', {
|
|
2775
3406
|
className: 'iterate-btn', 'data-ghost': '',
|
|
2776
3407
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setOpen((v) => !v) },
|
|
2777
|
-
}, open ? '收起' : '展开'),
|
|
3408
|
+
}, open ? t('收起') : t('展开')),
|
|
2778
3409
|
),
|
|
2779
3410
|
open
|
|
2780
3411
|
? React.createElement('div', {},
|
|
2781
3412
|
copyFailText
|
|
2782
3413
|
? React.createElement('div', { className: 'iterate-obs-block' },
|
|
2783
3414
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2784
|
-
React.createElement('b', {}, '复制未成功'),
|
|
2785
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, '剪贴板写入被拒绝,请手动选中下方指令复制'),
|
|
3415
|
+
React.createElement('b', {}, t('复制未成功')),
|
|
3416
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, t('剪贴板写入被拒绝,请手动选中下方指令复制')),
|
|
2786
3417
|
React.createElement('button', {
|
|
2787
|
-
className: 'iterate-btn', 'data-ghost': '', 'aria-label': '关闭复制失败提示',
|
|
3418
|
+
className: 'iterate-btn', 'data-ghost': '', 'aria-label': t('关闭复制失败提示'),
|
|
2788
3419
|
onClick: () => setCopyFailText(null),
|
|
2789
|
-
}, '关闭'),
|
|
3420
|
+
}, t('关闭')),
|
|
2790
3421
|
),
|
|
2791
3422
|
React.createElement('div', { className: 'iterate-obs-code' }, copyFailText),
|
|
2792
3423
|
)
|
|
@@ -2798,17 +3429,28 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2798
3429
|
React.createElement('button', {
|
|
2799
3430
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'export' ? '' : undefined,
|
|
2800
3431
|
onClick: exportObservatory,
|
|
2801
|
-
title: '将观测台全部数据导出为 JSON(优先下载,失败则复制)',
|
|
2802
|
-
}, copiedKey === 'export' ? '已导出' : '导出 JSON'),
|
|
3432
|
+
title: t('将观测台全部数据导出为 JSON(优先下载,失败则复制)'),
|
|
3433
|
+
}, copiedKey === 'export' ? t('已导出') : t('导出 JSON')),
|
|
3434
|
+
// Gap #4: `.iterate/` artifacts are unreachable from the browser;
|
|
3435
|
+
// this copies the one instruction that makes the model echo them
|
|
3436
|
+
// into the session stream (which is what every panel scans).
|
|
3437
|
+
React.createElement('button', {
|
|
3438
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
3439
|
+
onClick: () => copyInstruction('pull-snapshot', pullInstruction),
|
|
3440
|
+
title: pullTitle,
|
|
3441
|
+
}, copiedKey === 'pull-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2803
3442
|
),
|
|
2804
|
-
React.createElement('div', { className: 'iterate-obs-tabs' },
|
|
3443
|
+
React.createElement('div', { className: 'iterate-obs-tabs', role: 'tablist', 'aria-label': t('观测台页签(快捷键 1-9 / 0 / -)') },
|
|
2805
3444
|
...OBS_TABS.map((t) =>
|
|
2806
3445
|
React.createElement('button', {
|
|
2807
3446
|
key: t.key,
|
|
2808
3447
|
className: 'iterate-obs-tab',
|
|
3448
|
+
role: 'tab',
|
|
3449
|
+
'aria-selected': tab === t.key,
|
|
2809
3450
|
'data-active': tab === t.key ? '' : undefined,
|
|
2810
3451
|
onClick: () => setTab(t.key),
|
|
2811
|
-
|
|
3452
|
+
title: t.hotkey ? tf(`{a0}(按 {a1} 切换)`, { a0: obsTabLabel(t), a1: t.hotkey }) : tf(`{a0}(按 - 切换)`, { a0: obsTabLabel(t) }),
|
|
3453
|
+
}, obsTabLabel(t)),
|
|
2812
3454
|
),
|
|
2813
3455
|
),
|
|
2814
3456
|
React.createElement('div', { className: 'iterate-obs-body' }, renderBody()),
|
|
@@ -2830,6 +3472,17 @@ function selectTurnTail(owner: SlotProps): { matched: boolean } | null {
|
|
|
2830
3472
|
return null
|
|
2831
3473
|
}
|
|
2832
3474
|
|
|
3475
|
+
/**
|
|
3476
|
+
* Slot components wrapped once at module scope so the wrapper identity is
|
|
3477
|
+
* stable — creating the wrapper inside the render callback would remount the
|
|
3478
|
+
* subtree (and wipe triage verdicts, expanded tabs …) on every render.
|
|
3479
|
+
*/
|
|
3480
|
+
const LangDashboard = withLang(ConvergenceDashboard)
|
|
3481
|
+
const LangObservatory = withLang(ObservatoryPanel)
|
|
3482
|
+
const LangTurnTail = withLang(TurnTailEntry)
|
|
3483
|
+
const LangProgress = withLang(ProgressCapsule)
|
|
3484
|
+
const LangSettings = withLang(SettingsPanel)
|
|
3485
|
+
|
|
2833
3486
|
/**
|
|
2834
3487
|
* Plugin client entry. Called by the dsh client runtime with its context.
|
|
2835
3488
|
*/
|
|
@@ -2839,6 +3492,15 @@ export function apply(ctx: ClientContext): void {
|
|
|
2839
3492
|
themeSvc = readTheme(ctx)
|
|
2840
3493
|
storage = createStorage()
|
|
2841
3494
|
|
|
3495
|
+
// 1b. Language: an explicit stored choice wins, then the browser locale.
|
|
3496
|
+
// Deliberately NOT persisted here (see setLang's `persist` flag) — an
|
|
3497
|
+
// auto-detected language must not shadow a choice the user never made.
|
|
3498
|
+
configureLangStorage(storage)
|
|
3499
|
+
setLang(
|
|
3500
|
+
detectLang(storage.get(LANG_STORAGE_KEY), typeof navigator !== 'undefined' ? navigator.language : ''),
|
|
3501
|
+
false,
|
|
3502
|
+
)
|
|
3503
|
+
|
|
2842
3504
|
// 2. Theme preference bootstrap.
|
|
2843
3505
|
const savedTheme = storage.get(THEME_STORAGE_KEY)
|
|
2844
3506
|
themeEnabled = savedTheme === null ? true : savedTheme === '1'
|
|
@@ -2873,7 +3535,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
2873
3535
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
2874
3536
|
slotsSvc?.register(
|
|
2875
3537
|
{ name: 'conversation.input.dock', id: 'iterate-dashboard', order: 90 },
|
|
2876
|
-
(props) => React.createElement(
|
|
3538
|
+
(props) => React.createElement(LangDashboard, props),
|
|
2877
3539
|
),
|
|
2878
3540
|
)
|
|
2879
3541
|
|
|
@@ -2882,7 +3544,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
2882
3544
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
2883
3545
|
slotsSvc?.register(
|
|
2884
3546
|
{ name: 'conversation.input.dock', id: 'iterate-observatory', order: 91 },
|
|
2885
|
-
(props) => React.createElement(
|
|
3547
|
+
(props) => React.createElement(LangObservatory, props),
|
|
2886
3548
|
),
|
|
2887
3549
|
)
|
|
2888
3550
|
|
|
@@ -2890,7 +3552,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
2890
3552
|
slotsSvc.inject('conversation.chat.turnTail', () =>
|
|
2891
3553
|
slotsSvc?.register(
|
|
2892
3554
|
{ name: 'conversation.chat.turnTail', id: 'iterate-turn-tail', select: selectTurnTail },
|
|
2893
|
-
(props) => React.createElement(
|
|
3555
|
+
(props) => React.createElement(LangTurnTail, props),
|
|
2894
3556
|
),
|
|
2895
3557
|
)
|
|
2896
3558
|
|
|
@@ -2898,7 +3560,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
2898
3560
|
slotsSvc.inject('shell.overlay', () =>
|
|
2899
3561
|
slotsSvc?.register(
|
|
2900
3562
|
{ name: 'shell.overlay', id: 'iterate-progress', order: 0 },
|
|
2901
|
-
(props) => React.createElement(
|
|
3563
|
+
(props) => React.createElement(LangProgress, props),
|
|
2902
3564
|
),
|
|
2903
3565
|
)
|
|
2904
3566
|
|
|
@@ -2906,7 +3568,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
2906
3568
|
slotsSvc.inject('settings.section', () =>
|
|
2907
3569
|
slotsSvc?.register(
|
|
2908
3570
|
{ name: 'settings.section', id: 'iterate-settings', order: 30, label: () => 'iterate' },
|
|
2909
|
-
(props) => React.createElement(
|
|
3571
|
+
(props) => React.createElement(LangSettings, props),
|
|
2910
3572
|
),
|
|
2911
3573
|
)
|
|
2912
3574
|
}
|