iterate-plugin 3.6.0 → 3.8.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 +70 -7
- package/README.zh-CN.md +50 -6
- package/dist/config-loader.js +7 -1
- package/dist/file-lock.js +110 -21
- package/dist/git-scope.js +22 -2
- package/dist/index.js +7 -0
- package/dist/live.js +2 -1
- package/dist/session-hooks.js +6 -1
- package/dist/skill-prompt.js +2 -2
- package/dist/tools/checkpoint.js +24 -17
- package/dist/tools/config.js +42 -33
- package/dist/tools/decision-log.js +13 -99
- package/dist/tools/transcript.js +74 -44
- package/dist/tools/triage.js +52 -45
- package/dist/transcript.js +2 -2
- package/dist/working-directory.js +114 -0
- package/lib/client.js +1128 -468
- package/lib/parse.js +280 -62
- package/package.json +34 -11
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +553 -344
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +7 -1
- package/src/file-lock.ts +115 -18
- package/src/git-scope.ts +21 -2
- package/src/index.ts +7 -0
- package/src/live.ts +2 -1
- package/src/session-hooks.ts +6 -1
- package/src/skill-prompt.ts +2 -2
- package/src/tools/checkpoint.ts +23 -16
- package/src/tools/config.ts +40 -31
- package/src/tools/decision-log.ts +12 -82
- package/src/tools/transcript.ts +72 -43
- package/src/tools/triage.ts +47 -40
- package/src/transcript.ts +2 -2
- package/src/types.ts +1 -1
- package/src/working-directory.ts +126 -0
package/src/client/index.ts
CHANGED
|
@@ -103,8 +103,24 @@ import {
|
|
|
103
103
|
buildConfigFieldInstruction, // #8: field → iterate_config write instruction
|
|
104
104
|
configFieldByKey, // #8: field lookup for the settings picker
|
|
105
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
|
|
106
109
|
DISK_SNAPSHOT_SOURCES, // #4/#3: read-side tools listed by the pull instruction
|
|
107
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'
|
|
108
124
|
|
|
109
125
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
110
126
|
|
|
@@ -726,6 +742,37 @@ function removeStorageByPrefix(prefix: string): number {
|
|
|
726
742
|
return removed
|
|
727
743
|
}
|
|
728
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
|
+
|
|
729
776
|
/** Copy text to the clipboard, resolving to whether it actually succeeded. */
|
|
730
777
|
function copyText(text: string): Promise<boolean> {
|
|
731
778
|
if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
@@ -782,11 +829,28 @@ function severityLabel(severity: string | undefined): string {
|
|
|
782
829
|
return SEVERITY_LABEL[coerceSeverity(severity)]
|
|
783
830
|
}
|
|
784
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
|
+
|
|
785
849
|
/** Badge text for the run's live/finished state, annotated with the stop reason. */
|
|
786
850
|
function runStatusText(live: boolean, manifest: ObsManifest | null | undefined): string {
|
|
787
|
-
if (live) return '运行中'
|
|
851
|
+
if (live) return t('运行中')
|
|
788
852
|
const reason = manifest && typeof manifest.stoppedReason === 'string' ? manifest.stoppedReason : ''
|
|
789
|
-
return reason ? stoppedReasonLabel(reason) : '已结束'
|
|
853
|
+
return reason ? stoppedReasonLabel(reason, getLang()) : t('已结束')
|
|
790
854
|
}
|
|
791
855
|
|
|
792
856
|
/** Resolve the slots service: prefer the typed `ctx.slots`, fall back to `ctx.get`. */
|
|
@@ -902,7 +966,7 @@ function TrendChart({ points }: { points: Array<{ round: number; count: number }
|
|
|
902
966
|
key: p.round,
|
|
903
967
|
className: 'iterate-trend-bar',
|
|
904
968
|
'data-hot': p.count > 0 ? '' : undefined,
|
|
905
|
-
title: `Round
|
|
969
|
+
title: tf(`Round {a0}:{a1} 项`, { a0: p.round, a1: p.count }),
|
|
906
970
|
style: { height: `${Math.max(4, Math.round((p.count / max) * 24))}px` },
|
|
907
971
|
}),
|
|
908
972
|
)
|
|
@@ -912,7 +976,7 @@ function TrendChart({ points }: { points: Array<{ round: number; count: number }
|
|
|
912
976
|
return React.createElement('div', {
|
|
913
977
|
className: 'iterate-trend',
|
|
914
978
|
role: 'img',
|
|
915
|
-
'aria-label':
|
|
979
|
+
'aria-label': tf(`各轮发现数量趋势:{a0}`, { a0: summary }),
|
|
916
980
|
}, ...bars)
|
|
917
981
|
}
|
|
918
982
|
|
|
@@ -960,12 +1024,12 @@ function StartIterationButton() {
|
|
|
960
1024
|
'data-failed': failed === key ? '' : undefined,
|
|
961
1025
|
onClick: () => copy(key, command),
|
|
962
1026
|
title,
|
|
963
|
-
}, copied === key ? '已复制' : failed === key ? '复制失败' : label)
|
|
1027
|
+
}, copied === key ? t('已复制') : failed === key ? t('复制失败') : label)
|
|
964
1028
|
return React.createElement(
|
|
965
1029
|
'span',
|
|
966
1030
|
{ className: 'iterate-dashboard-launch' },
|
|
967
|
-
btn('start-full', '完整迭代', START_INSTRUCTIONS.full, true, '复制启动指令:完整「审查 → 修复 → 验证 → 复盘」闭环(粘贴给模型即可)'),
|
|
968
|
-
btn('start-review', '仅评审', START_INSTRUCTIONS.reviewOnly, false, '复制启动指令:只审查不修改(dry-run)(粘贴给模型即可)'),
|
|
1031
|
+
btn('start-full', t('完整迭代'), START_INSTRUCTIONS.full, true, t('复制启动指令:完整「审查 → 修复 → 验证 → 复盘」闭环(粘贴给模型即可)')),
|
|
1032
|
+
btn('start-review', t('仅评审'), START_INSTRUCTIONS.reviewOnly, false, t('复制启动指令:只审查不修改(dry-run)(粘贴给模型即可)')),
|
|
969
1033
|
)
|
|
970
1034
|
}
|
|
971
1035
|
|
|
@@ -982,11 +1046,30 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
982
1046
|
// The `conversation.input.dock` slot owner share (InputZone) provides
|
|
983
1047
|
// `session` as a point-in-time snapshot — read it directly, never subscribe.
|
|
984
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])
|
|
985
1055
|
const report = latestReport(session)
|
|
986
1056
|
|
|
1057
|
+
// Mount-time suppression: opening a session that ALREADY has a report mounts
|
|
1058
|
+
// this component with that report, and an effect keyed on the report hash
|
|
1059
|
+
// fires immediately — popping a stale "Round X 完成" bubble for work that did
|
|
1060
|
+
// not just complete. Track the first key this instance sees and skip it; only
|
|
1061
|
+
// a subsequent round progression pulses.
|
|
1062
|
+
const pulseSeenRef = React.useRef<string | null>(null)
|
|
987
1063
|
React.useEffect(() => {
|
|
988
1064
|
if (!report) return
|
|
989
1065
|
const cur = getCurrentRound(report)
|
|
1066
|
+
const key = hashReport(report) + ':' + String(cur)
|
|
1067
|
+
if (pulseSeenRef.current === null) {
|
|
1068
|
+
pulseSeenRef.current = key
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
if (pulseSeenRef.current === key) return
|
|
1072
|
+
pulseSeenRef.current = key
|
|
990
1073
|
const conv = report.convergence
|
|
991
1074
|
emitRoundPulse(cur, conv?.converged === true)
|
|
992
1075
|
setPulseKey((k) => k + 1)
|
|
@@ -1003,11 +1086,11 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1003
1086
|
return React.createElement(
|
|
1004
1087
|
'div',
|
|
1005
1088
|
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'running', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1006
|
-
React.createElement('span', { className: 'iterate-round-badge', 'data-live': '' }, '运行中'),
|
|
1089
|
+
React.createElement('span', { className: 'iterate-round-badge', 'data-live': '' }, t('运行中')),
|
|
1007
1090
|
React.createElement(
|
|
1008
1091
|
'span',
|
|
1009
1092
|
{ className: 'iterate-empty-hint' },
|
|
1010
|
-
`第
|
|
1093
|
+
tf(`第 {a0} 轮进行中{a1}——首轮汇总报告落地后,这里会显示收敛进度与发现统计。`, { a0: String(runState.round), a1: runState.phase ? tf(` · 阶段「{a0}」`, { a0: runState.phase }) : '' }),
|
|
1011
1094
|
),
|
|
1012
1095
|
// No start button while a run is active.
|
|
1013
1096
|
)
|
|
@@ -1016,11 +1099,11 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1016
1099
|
return React.createElement(
|
|
1017
1100
|
'div',
|
|
1018
1101
|
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'done', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1019
|
-
React.createElement('span', { className: 'iterate-round-badge' }, '已结束'),
|
|
1102
|
+
React.createElement('span', { className: 'iterate-round-badge' }, t('已结束')),
|
|
1020
1103
|
React.createElement(
|
|
1021
1104
|
'span',
|
|
1022
1105
|
{ className: 'iterate-empty-hint' },
|
|
1023
|
-
`上次迭代在第
|
|
1106
|
+
tf(`上次迭代在第 {a0} 轮后停止{a1},但没有可展示的汇总报告。`, { a0: String(runState.round), a1: runState.stoppedReason ? parenthesize(stoppedReasonLabel(runState.stoppedReason, getLang())) : '' }),
|
|
1024
1107
|
),
|
|
1025
1108
|
React.createElement(StartIterationButton, null),
|
|
1026
1109
|
)
|
|
@@ -1033,7 +1116,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1033
1116
|
'div',
|
|
1034
1117
|
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'empty', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1035
1118
|
React.createElement('span', { className: 'iterate-round-badge' }, 'iterate'),
|
|
1036
|
-
React.createElement('span', { className: 'iterate-empty-hint' }, '运行一次评审后,这里会显示收敛进度与发现统计。'),
|
|
1119
|
+
React.createElement('span', { className: 'iterate-empty-hint' }, t('运行一次评审后,这里会显示收敛进度与发现统计。')),
|
|
1037
1120
|
React.createElement(StartIterationButton, null),
|
|
1038
1121
|
)
|
|
1039
1122
|
}
|
|
@@ -1053,15 +1136,15 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1053
1136
|
? React.createElement('span', {
|
|
1054
1137
|
className: 'iterate-chip-resume',
|
|
1055
1138
|
key: 'resume',
|
|
1056
|
-
title: '本次迭代从上一次中断的断点继续执行',
|
|
1057
|
-
}, `已中断恢复
|
|
1139
|
+
title: t('本次迭代从上一次中断的断点继续执行'),
|
|
1140
|
+
}, tf(`已中断恢复 ×{a0}`, { a0: String(resumeCount) }))
|
|
1058
1141
|
: null
|
|
1059
1142
|
const imageChip = imageCount > 0
|
|
1060
1143
|
? React.createElement('span', {
|
|
1061
1144
|
className: 'iterate-chip-images',
|
|
1062
1145
|
key: 'images',
|
|
1063
|
-
title: '会话中检测到用户附带的图片,评审将作为视觉证据参考',
|
|
1064
|
-
}, `附件图片
|
|
1146
|
+
title: t('会话中检测到用户附带的图片,评审将作为视觉证据参考'),
|
|
1147
|
+
}, tf(`附件图片 {a0}`, { a0: String(imageCount) }))
|
|
1065
1148
|
: null
|
|
1066
1149
|
|
|
1067
1150
|
// Live run-state awareness: surface the transcript's current workflow phase
|
|
@@ -1075,8 +1158,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1075
1158
|
key: 'phase',
|
|
1076
1159
|
'data-live': transcript.active === true ? '' : undefined,
|
|
1077
1160
|
title: transcript.active === true
|
|
1078
|
-
?
|
|
1079
|
-
:
|
|
1161
|
+
? tf(`当前处于「{a0}」阶段(运行中)`, { a0: phase })
|
|
1162
|
+
: tf(`最近一次执行停留在「{a0}」阶段(已结束)`, { a0: phase }),
|
|
1080
1163
|
}, `${phase} · ${runStatusText(transcript.active === true, transcript)}`)
|
|
1081
1164
|
: null
|
|
1082
1165
|
|
|
@@ -1089,7 +1172,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1089
1172
|
className: 'iterate-chip-taskmode',
|
|
1090
1173
|
key: 'taskmode',
|
|
1091
1174
|
'data-mode': taskMode,
|
|
1092
|
-
title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
|
|
1175
|
+
title: taskMode === 'code' ? t('代码模式:直接修改文件') : t('迭代模式:审查-修复循环'),
|
|
1093
1176
|
}, taskMode === 'code' ? '● code' : '● iterate')
|
|
1094
1177
|
: null
|
|
1095
1178
|
|
|
@@ -1108,7 +1191,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1108
1191
|
React.createElement(
|
|
1109
1192
|
'span',
|
|
1110
1193
|
{ key: '+more', className: 'iterate-dim-badge iterate-dim-more', title: dimNames.slice(6).join(', ') },
|
|
1111
|
-
|
|
1194
|
+
tf(`+{a0} 更多`, { a0: overflow }),
|
|
1112
1195
|
),
|
|
1113
1196
|
)
|
|
1114
1197
|
}
|
|
@@ -1123,7 +1206,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1123
1206
|
? React.createElement('span', {
|
|
1124
1207
|
className: 'iterate-metric',
|
|
1125
1208
|
key: 'fixes',
|
|
1126
|
-
title: '本轮已应用的原子修复数(正常模式)',
|
|
1209
|
+
title: t('本轮已应用的原子修复数(正常模式)'),
|
|
1127
1210
|
}, `${String(fixCount)} fixes`)
|
|
1128
1211
|
: null
|
|
1129
1212
|
|
|
@@ -1134,8 +1217,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1134
1217
|
? React.createElement('span', {
|
|
1135
1218
|
className: 'iterate-chip-resume',
|
|
1136
1219
|
key: 'converged',
|
|
1137
|
-
title: '审查已收敛:最后一轮未发现新问题',
|
|
1138
|
-
}, '✓ 已收敛')
|
|
1220
|
+
title: t('审查已收敛:最后一轮未发现新问题'),
|
|
1221
|
+
}, t('✓ 已收敛'))
|
|
1139
1222
|
: null
|
|
1140
1223
|
|
|
1141
1224
|
const sevMetric = (key: 'critical' | 'high' | 'medium' | 'low', label: string) =>
|
|
@@ -1203,14 +1286,14 @@ function StatsCard(props: SlotProps) {
|
|
|
1203
1286
|
React.createElement('td', { style: { color: SEVERITY_COLOR.low } }, String(h.low)),
|
|
1204
1287
|
),
|
|
1205
1288
|
)
|
|
1206
|
-
const trendLine = `首轮
|
|
1207
|
-
const completion = buildCompletionSummary(report)
|
|
1289
|
+
const trendLine = tf(`首轮 {a0} → 末轮 {a1} 项,降幅 {a2}%{a3}`, { a0: trend.firstRound, a1: trend.lastRound, a2: trend.reductionPercent, a3: trend.converged ? t(',已收敛') : '' })
|
|
1290
|
+
const completion = buildCompletionSummary(report, getLang())
|
|
1208
1291
|
|
|
1209
1292
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'stats', className: 'iterate-stats' },
|
|
1210
1293
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
1211
|
-
React.createElement('span', {}, 'Iterate · 收敛统计'),
|
|
1294
|
+
React.createElement('span', {}, t('Iterate · 收敛统计')),
|
|
1212
1295
|
React.createElement('span', { className: 'iterate-triage-hint' },
|
|
1213
|
-
`Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? '已收敛' : '未收敛'}`,
|
|
1296
|
+
`Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? t('已收敛') : t('未收敛')}`,
|
|
1214
1297
|
),
|
|
1215
1298
|
),
|
|
1216
1299
|
React.createElement('div', { className: 'iterate-stats-grid' }, ...rows),
|
|
@@ -1221,15 +1304,15 @@ function StatsCard(props: SlotProps) {
|
|
|
1221
1304
|
className: 'iterate-btn',
|
|
1222
1305
|
'data-primary': showHistory ? '' : undefined,
|
|
1223
1306
|
onClick: () => setShowHistory((v) => !v),
|
|
1224
|
-
}, showHistory ? '收起历史' : '历史 / 趋势'),
|
|
1307
|
+
}, showHistory ? t('收起历史') : t('历史 / 趋势')),
|
|
1225
1308
|
),
|
|
1226
1309
|
showHistory
|
|
1227
1310
|
? React.createElement('div', { className: 'iterate-history-body' },
|
|
1228
1311
|
React.createElement('table', { className: 'iterate-history-table' },
|
|
1229
1312
|
React.createElement('thead', {},
|
|
1230
1313
|
React.createElement('tr', {},
|
|
1231
|
-
React.createElement('th', {}, '轮次'),
|
|
1232
|
-
React.createElement('th', {}, '发现'),
|
|
1314
|
+
React.createElement('th', {}, t('轮次')),
|
|
1315
|
+
React.createElement('th', {}, t('发现')),
|
|
1233
1316
|
React.createElement('th', { style: { color: SEVERITY_COLOR.critical } }, 'CRIT'),
|
|
1234
1317
|
React.createElement('th', { style: { color: SEVERITY_COLOR.high } }, 'HIGH'),
|
|
1235
1318
|
React.createElement('th', { style: { color: SEVERITY_COLOR.medium } }, 'MED'),
|
|
@@ -1336,6 +1419,11 @@ function TriagePanel(props: SlotProps) {
|
|
|
1336
1419
|
}
|
|
1337
1420
|
|
|
1338
1421
|
// ── Keyboard shortcuts (y / n / a on the selected finding, ↑/↓ to move) ──
|
|
1422
|
+
// Multi-instance scoping: every turn that has a report mounts its OWN triage
|
|
1423
|
+
// panel. Keying off `document.querySelector('[data-iterate="triage"]')` would
|
|
1424
|
+
// return only the FIRST one, so y/n/a/↑/↓ silently dead whenever focus sits
|
|
1425
|
+
// in a later panel. A per-component ref points at THIS instance's root.
|
|
1426
|
+
const rootRef = React.useRef<HTMLDivElement | null>(null)
|
|
1339
1427
|
React.useEffect(() => {
|
|
1340
1428
|
const doc = typeof document !== 'undefined' ? document : null
|
|
1341
1429
|
if (!doc) return
|
|
@@ -1347,10 +1435,10 @@ function TriagePanel(props: SlotProps) {
|
|
|
1347
1435
|
const t = ev.target as HTMLElement | null
|
|
1348
1436
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
|
|
1349
1437
|
if (t && typeof t.isContentEditable === 'boolean' && t.isContentEditable) return
|
|
1350
|
-
// Only act when focus is INSIDE
|
|
1438
|
+
// Only act when focus is INSIDE THIS panel. Everything else keeps
|
|
1351
1439
|
// standard behavior (↑/↓ page scroll, typing in the composer), so we do
|
|
1352
1440
|
// not hijack document-level keys just because a report is mounted.
|
|
1353
|
-
const rootEl =
|
|
1441
|
+
const rootEl = rootRef.current
|
|
1354
1442
|
if (!rootEl) return
|
|
1355
1443
|
const activeEl = doc.activeElement as HTMLElement | null
|
|
1356
1444
|
if (!activeEl) return
|
|
@@ -1397,7 +1485,7 @@ function TriagePanel(props: SlotProps) {
|
|
|
1397
1485
|
}
|
|
1398
1486
|
|
|
1399
1487
|
const doTriggerNewRound = () => {
|
|
1400
|
-
const cmd = '请继续执行下一轮迭代审查'
|
|
1488
|
+
const cmd = t('请继续执行下一轮迭代审查')
|
|
1401
1489
|
copyWithFeedback(cmd, 'new-round')
|
|
1402
1490
|
}
|
|
1403
1491
|
|
|
@@ -1405,7 +1493,7 @@ function TriagePanel(props: SlotProps) {
|
|
|
1405
1493
|
// iterate_checkpoint has NO rollback operation (save/load/resume/clear
|
|
1406
1494
|
// only); reverting applied fixes is done via iterate_rollback by fix id,
|
|
1407
1495
|
// so the paste-able instruction must point there instead.
|
|
1408
|
-
const cmd = '请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复'
|
|
1496
|
+
const cmd = t('请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复')
|
|
1409
1497
|
copyWithFeedback(cmd, 'rollback')
|
|
1410
1498
|
}
|
|
1411
1499
|
|
|
@@ -1507,27 +1595,27 @@ function TriagePanel(props: SlotProps) {
|
|
|
1507
1595
|
),
|
|
1508
1596
|
React.createElement('div', { className: 'iterate-finding-summary' }, String(finding.summary || '')),
|
|
1509
1597
|
React.createElement('div', { className: 'iterate-finding-actions' },
|
|
1510
|
-
btn('y 修复', 'keep', '保留该 finding,进入修复'),
|
|
1511
|
-
btn('n 跳过', 'skip', '跳过该 finding'),
|
|
1512
|
-
btn('a 已知有意', 'ignore', '标记为已知有意并写入 known_intentional'),
|
|
1598
|
+
btn(t('y 修复'), 'keep', t('保留该 finding,进入修复')),
|
|
1599
|
+
btn(t('n 跳过'), 'skip', t('跳过该 finding')),
|
|
1600
|
+
btn(t('a 已知有意'), 'ignore', t('标记为已知有意并写入 known_intentional')),
|
|
1513
1601
|
),
|
|
1514
1602
|
)
|
|
1515
1603
|
})
|
|
1516
1604
|
|
|
1517
|
-
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
1605
|
+
return React.createElement('div', { ref: rootRef, 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
1518
1606
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
1519
|
-
React.createElement('span', { role: 'heading', 'aria-level': 3 }, `Iterate · Findings 分诊 (
|
|
1520
|
-
React.createElement('span', { className: 'iterate-triage-hint' }, 'y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择'),
|
|
1607
|
+
React.createElement('span', { role: 'heading', 'aria-level': 3 }, tf(`Iterate · Findings 分诊 ({a0}/{a1})`, { a0: filtered.length, a1: findings.length })),
|
|
1608
|
+
React.createElement('span', { className: 'iterate-triage-hint' }, t('y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择')),
|
|
1521
1609
|
),
|
|
1522
1610
|
React.createElement('div', { className: 'iterate-filter' },
|
|
1523
|
-
React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setSeverityFilter(e.target.value), 'aria-label': '按严重度筛选' },
|
|
1524
|
-
React.createElement('option', { value: '' }, '全部严重度'),
|
|
1611
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setSeverityFilter(e.target.value), 'aria-label': t('按严重度筛选') },
|
|
1612
|
+
React.createElement('option', { value: '' }, t('全部严重度')),
|
|
1525
1613
|
...options.severities.map((s) =>
|
|
1526
1614
|
React.createElement('option', { key: s.value, value: s.value }, `${severityLabel(s.value)} (${s.count})`),
|
|
1527
1615
|
),
|
|
1528
1616
|
),
|
|
1529
|
-
React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDimensionFilter(e.target.value), 'aria-label': '按维度筛选' },
|
|
1530
|
-
React.createElement('option', { value: '' }, '全部维度'),
|
|
1617
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDimensionFilter(e.target.value), 'aria-label': t('按维度筛选') },
|
|
1618
|
+
React.createElement('option', { value: '' }, t('全部维度')),
|
|
1531
1619
|
...options.dimensions.map((d) =>
|
|
1532
1620
|
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
1533
1621
|
),
|
|
@@ -1535,37 +1623,37 @@ function TriagePanel(props: SlotProps) {
|
|
|
1535
1623
|
React.createElement('input', {
|
|
1536
1624
|
className: 'iterate-filter-search',
|
|
1537
1625
|
type: 'search',
|
|
1538
|
-
placeholder: '搜索文件 / 摘要…',
|
|
1539
|
-
'aria-label': '搜索文件或摘要',
|
|
1626
|
+
placeholder: t('搜索文件 / 摘要…'),
|
|
1627
|
+
'aria-label': t('搜索文件或摘要'),
|
|
1540
1628
|
value: filter.search,
|
|
1541
1629
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setSearchFilter(e.target.value),
|
|
1542
1630
|
}),
|
|
1543
1631
|
React.createElement('span', { className: 'iterate-filter-count' },
|
|
1544
1632
|
isFilterActive
|
|
1545
|
-
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, `清除筛选(显示
|
|
1546
|
-
: `共
|
|
1633
|
+
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, tf(`清除筛选(显示 {a0})`, { a0: filtered.length }))
|
|
1634
|
+
: tf(`共 {a0} 项`, { a0: findings.length }),
|
|
1547
1635
|
),
|
|
1548
1636
|
),
|
|
1549
1637
|
React.createElement('div', { className: 'iterate-batch' },
|
|
1550
|
-
React.createElement('span', { className: 'iterate-batch-label' }, '批量作用于:'),
|
|
1638
|
+
React.createElement('span', { className: 'iterate-batch-label' }, t('批量作用于:')),
|
|
1551
1639
|
React.createElement('button', {
|
|
1552
1640
|
className: selectAll ? 'iterate-batch-btn iterate-batch-scope' : 'iterate-batch-btn iterate-batch-scope iterate-batch-scope-on',
|
|
1553
1641
|
onClick: () => setSelectAll(false),
|
|
1554
|
-
title: '批量按钮仅作用于当前筛选可见的 findings',
|
|
1555
|
-
}, `可见
|
|
1642
|
+
title: t('批量按钮仅作用于当前筛选可见的 findings'),
|
|
1643
|
+
}, tf(`可见 {a0}`, { a0: indices.length })),
|
|
1556
1644
|
React.createElement('button', {
|
|
1557
1645
|
className: selectAll ? 'iterate-batch-btn iterate-batch-scope iterate-batch-scope-on' : 'iterate-batch-btn iterate-batch-scope',
|
|
1558
1646
|
onClick: () => setSelectAll(true),
|
|
1559
|
-
title: '批量按钮作用于全部 findings',
|
|
1560
|
-
}, `全部
|
|
1647
|
+
title: t('批量按钮作用于全部 findings'),
|
|
1648
|
+
}, tf(`全部 {a0}`, { a0: allIndices.length })),
|
|
1561
1649
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('keep') }, 'y'),
|
|
1562
1650
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('skip') }, 'n'),
|
|
1563
1651
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('ignore') }, 'a'),
|
|
1564
|
-
React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: '把所有判定恢复为默认 y(修复)' }, '重置'),
|
|
1652
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: t('把所有判定恢复为默认 y(修复)') }, t('重置')),
|
|
1565
1653
|
),
|
|
1566
1654
|
...rows,
|
|
1567
1655
|
React.createElement('div', { className: 'iterate-triage-foot' },
|
|
1568
|
-
React.createElement('span', {}, `y
|
|
1656
|
+
React.createElement('span', {}, tf(`y {a0} · n {a1} · a {a2} · 待写回 known_intentional:{a3} 条`, { a0: counts.keep, a1: counts.skip, a2: counts.ignore, a3: ignoredCount })),
|
|
1569
1657
|
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1570
1658
|
// Gap #7: the apply instruction (safe path) is PRIMARY; the raw YAML
|
|
1571
1659
|
// is demoted to an explicit "just the snippet" escape hatch.
|
|
@@ -1575,65 +1663,65 @@ function TriagePanel(props: SlotProps) {
|
|
|
1575
1663
|
'data-failed': cmdFailed === 'apply' ? '' : undefined,
|
|
1576
1664
|
onClick: doBuildInstruction, disabled: ignoredCount === 0,
|
|
1577
1665
|
title: ignoredCount === 0
|
|
1578
|
-
? '当前没有标记为「已知有意」的 finding'
|
|
1579
|
-
: '复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)',
|
|
1580
|
-
}, cmdCopied === 'apply' ? '已复制' : cmdFailed === 'apply' ? '复制失败' :
|
|
1666
|
+
? t('当前没有标记为「已知有意」的 finding')
|
|
1667
|
+
: t('复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)'),
|
|
1668
|
+
}, cmdCopied === 'apply' ? t('已复制') : cmdFailed === 'apply' ? t('复制失败') : tf(`生成应用指令{a0}`, { a0: ignoredCount > 0 ? `(${ignoredCount})` : '' })),
|
|
1581
1669
|
React.createElement('button', {
|
|
1582
1670
|
className: 'iterate-btn',
|
|
1583
1671
|
'data-copied': copied ? '' : undefined,
|
|
1584
1672
|
'data-failed': cmdFailed === 'yaml' ? '' : undefined,
|
|
1585
1673
|
onClick: doCopyYaml, disabled: ignoredCount === 0,
|
|
1586
1674
|
title: ignoredCount === 0
|
|
1587
|
-
? '当前没有标记为「已知有意」的 finding'
|
|
1588
|
-
: '只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)',
|
|
1589
|
-
}, copied ? '已复制' : cmdFailed === 'yaml' ? '复制失败' : '仅复制 YAML 片段'),
|
|
1675
|
+
? t('当前没有标记为「已知有意」的 finding')
|
|
1676
|
+
: t('只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)'),
|
|
1677
|
+
}, copied ? t('已复制') : cmdFailed === 'yaml' ? t('复制失败') : t('仅复制 YAML 片段')),
|
|
1590
1678
|
React.createElement('button', {
|
|
1591
1679
|
className: 'iterate-btn',
|
|
1592
1680
|
'data-copied': cmdCopied === 'readback' ? '' : undefined,
|
|
1593
1681
|
'data-failed': cmdFailed === 'readback' ? '' : undefined,
|
|
1594
1682
|
onClick: doReadback,
|
|
1595
|
-
title: '复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)',
|
|
1596
|
-
}, cmdCopied === 'readback' ? '已复制' : cmdFailed === 'readback' ? '复制失败' : '回读已写入条目'),
|
|
1683
|
+
title: t('复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)'),
|
|
1684
|
+
}, cmdCopied === 'readback' ? t('已复制') : cmdFailed === 'readback' ? t('复制失败') : t('回读已写入条目')),
|
|
1597
1685
|
),
|
|
1598
1686
|
cmdCopied === 'apply'
|
|
1599
|
-
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, '已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。')
|
|
1687
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, t('已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。'))
|
|
1600
1688
|
: cmdCopied === 'readback'
|
|
1601
|
-
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, '已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。')
|
|
1689
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, t('已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。'))
|
|
1602
1690
|
: null,
|
|
1603
1691
|
),
|
|
1604
1692
|
// v3.0: Command buttons for native actions (§8)
|
|
1605
1693
|
React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1606
|
-
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
|
|
1694
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('指挥操作')),
|
|
1607
1695
|
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1608
1696
|
React.createElement('button', {
|
|
1609
1697
|
className: 'iterate-cmd', 'data-primary': '',
|
|
1610
1698
|
'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1611
1699
|
'data-failed': cmdFailed === 'approve-arch' ? '' : undefined,
|
|
1612
1700
|
onClick: doApproveArchitecturalFix,
|
|
1613
|
-
title: '批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)',
|
|
1614
|
-
}, cmdCopied === 'approve-arch' ? '已复制' : cmdFailed === 'approve-arch' ? '复制失败' : '批准架构修复'),
|
|
1701
|
+
title: t('批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)'),
|
|
1702
|
+
}, cmdCopied === 'approve-arch' ? t('已复制') : cmdFailed === 'approve-arch' ? t('复制失败') : t('批准架构修复')),
|
|
1615
1703
|
React.createElement('button', {
|
|
1616
1704
|
className: 'iterate-cmd',
|
|
1617
1705
|
'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1618
1706
|
'data-failed': cmdFailed === 'new-round' ? '' : undefined,
|
|
1619
1707
|
onClick: doTriggerNewRound,
|
|
1620
|
-
title: '触发新一轮迭代审查',
|
|
1621
|
-
}, cmdCopied === 'new-round' ? '已复制' : cmdFailed === 'new-round' ? '复制失败' : '触发新一轮'),
|
|
1708
|
+
title: t('触发新一轮迭代审查'),
|
|
1709
|
+
}, cmdCopied === 'new-round' ? t('已复制') : cmdFailed === 'new-round' ? t('复制失败') : t('触发新一轮')),
|
|
1622
1710
|
React.createElement('button', {
|
|
1623
1711
|
className: 'iterate-cmd', 'data-primary': '',
|
|
1624
1712
|
'data-copied': cmdCopied === 'assign' ? '' : undefined,
|
|
1625
1713
|
'data-failed': cmdFailed === 'assign' ? '' : undefined,
|
|
1626
1714
|
onClick: doAssignFindings,
|
|
1627
1715
|
disabled: (selectAll ? allIndices : indices).length === 0,
|
|
1628
|
-
title: selectAll ? `指派全部
|
|
1629
|
-
}, cmdCopied === 'assign' ? '已复制' : cmdFailed === 'assign' ? '复制失败' : '指派修复'),
|
|
1716
|
+
title: selectAll ? tf(`指派全部 {a0} 个 findings 修复(逐个 iterate_fix)`, { a0: allIndices.length }) : tf(`指派当前可见 {a0} 个 findings 修复(逐个 iterate_fix)`, { a0: indices.length }),
|
|
1717
|
+
}, cmdCopied === 'assign' ? t('已复制') : cmdFailed === 'assign' ? t('复制失败') : t('指派修复')),
|
|
1630
1718
|
React.createElement('button', {
|
|
1631
1719
|
className: 'iterate-cmd', 'data-danger': '',
|
|
1632
1720
|
'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1633
1721
|
'data-failed': cmdFailed === 'rollback' ? '' : undefined,
|
|
1634
1722
|
onClick: doRollbackToCheckpoint,
|
|
1635
|
-
title: '回滚到上一个检查点',
|
|
1636
|
-
}, cmdCopied === 'rollback' ? '已复制' : cmdFailed === 'rollback' ? '复制失败' : '回滚检查点'),
|
|
1723
|
+
title: t('回滚到上一个检查点'),
|
|
1724
|
+
}, cmdCopied === 'rollback' ? t('已复制') : cmdFailed === 'rollback' ? t('复制失败') : t('回滚检查点')),
|
|
1637
1725
|
),
|
|
1638
1726
|
),
|
|
1639
1727
|
payload
|
|
@@ -1656,15 +1744,15 @@ function VerdictBanner(props: SlotProps) {
|
|
|
1656
1744
|
'div',
|
|
1657
1745
|
{ 'data-iterate-root': '', 'data-iterate': 'verdict', className: 'iterate-verdict' },
|
|
1658
1746
|
React.createElement('span', { className: 'iterate-verdict-tag', 'data-ok': ok ? '' : undefined, 'data-warn': ok ? undefined : '' },
|
|
1659
|
-
ok ? '报告已批准' : '报告需修订'),
|
|
1747
|
+
ok ? t('报告已批准') : t('报告需修订')),
|
|
1660
1748
|
React.createElement('span', { className: 'iterate-verdict-detail' },
|
|
1661
|
-
item(verdict.totalFindings, '项发现'),
|
|
1662
|
-
item(verdict.totalRounds, '轮'),
|
|
1663
|
-
item(verdict.checksRun, '项审查'),
|
|
1749
|
+
item(verdict.totalFindings, t('项发现')),
|
|
1750
|
+
item(verdict.totalRounds, t('轮')),
|
|
1751
|
+
item(verdict.checksRun, t('项审查')),
|
|
1664
1752
|
ok
|
|
1665
|
-
? phrase('报告通过全部一致性检查')
|
|
1666
|
-
: item(verdict.reportIssues, '处报告缺陷'),
|
|
1667
|
-
phrase(verdict.converged ? '已收敛' : '未收敛'),
|
|
1753
|
+
? phrase(t('报告通过全部一致性检查'))
|
|
1754
|
+
: item(verdict.reportIssues, t('处报告缺陷')),
|
|
1755
|
+
phrase(verdict.converged ? t('已收敛') : t('未收敛')),
|
|
1668
1756
|
),
|
|
1669
1757
|
)
|
|
1670
1758
|
}
|
|
@@ -1713,7 +1801,7 @@ function ProgressCapsule() {
|
|
|
1713
1801
|
const listener = (payload: { round: number; converged: boolean }) => {
|
|
1714
1802
|
const converged = payload && payload.converged === true
|
|
1715
1803
|
const round = payload && typeof payload.round === 'number' ? payload.round : '?'
|
|
1716
|
-
setInfo({ text: converged ? `Round
|
|
1804
|
+
setInfo({ text: converged ? tf(`Round {a0} 完成 · 已收敛`, { a0: round }) : tf(`Round {a0} 完成`, { a0: round }), ok: converged })
|
|
1717
1805
|
if (timer) clearTimeout(timer)
|
|
1718
1806
|
// Converged notifications stay a bit longer.
|
|
1719
1807
|
timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400)
|
|
@@ -1736,7 +1824,17 @@ function ProgressCapsule() {
|
|
|
1736
1824
|
}
|
|
1737
1825
|
|
|
1738
1826
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
1739
|
-
function SettingsPanel(
|
|
1827
|
+
function SettingsPanel(props: SlotProps) {
|
|
1828
|
+
// Re-render on a language switch (the slot wrapper covers the tree; this
|
|
1829
|
+
// keeps the panel honest even if it is ever rendered directly).
|
|
1830
|
+
const lang = useLang()
|
|
1831
|
+
// Root-scoped slot: the host renders settings OUTSIDE the session scope, so
|
|
1832
|
+
// take the session the session-scoped slots published (a host that does pass
|
|
1833
|
+
// `props.session` wins). This is what makes the config read-back visible.
|
|
1834
|
+
// The hook is called UNCONDITIONALLY — a conditional hook would break the
|
|
1835
|
+
// panel as soon as a host started passing `props.session`.
|
|
1836
|
+
const cachedSession = useSessionSnapshot()
|
|
1837
|
+
const session = props && props.session ? props.session : cachedSession
|
|
1740
1838
|
const [enabled, setEnabled] = React.useState(themeEnabled)
|
|
1741
1839
|
const [guideCopied, setGuideCopied] = React.useState(false)
|
|
1742
1840
|
const [statusCopied, setStatusCopied] = React.useState(false)
|
|
@@ -1753,11 +1851,21 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1753
1851
|
const [cfgCopied, setCfgCopied] = React.useState(false)
|
|
1754
1852
|
const [cfgFailed, setCfgFailed] = React.useState(false)
|
|
1755
1853
|
const [cfgText, setCfgText] = React.useState<string | null>(null)
|
|
1854
|
+
// Gap #1 (3.7.0): the read half of the write -> read-back loop.
|
|
1855
|
+
const [cfgReadCopied, setCfgReadCopied] = React.useState(false)
|
|
1856
|
+
const [cfgReadText, setCfgReadText] = React.useState<string | null>(null)
|
|
1756
1857
|
const [pullCopied, setPullCopied] = React.useState(false)
|
|
1757
|
-
const [copyError, setCopyError] = React.useState<'guide' | 'status' | 'pull' | null>(null)
|
|
1858
|
+
const [copyError, setCopyError] = React.useState<'guide' | 'status' | 'pull' | 'cfgRead' | null>(null)
|
|
1758
1859
|
const guide = buildConfigEditGuide()
|
|
1759
1860
|
const statusGuide = buildRuntimeStatusGuide()
|
|
1760
1861
|
|
|
1862
|
+
// What the config actually says right now: latest `iterate_config` result
|
|
1863
|
+
// echoed into the session, then the value of the field being edited.
|
|
1864
|
+
const cfgMeta = configFieldByKey(cfgField)
|
|
1865
|
+
const cfgRead = scanSessionForConfigRead(session)
|
|
1866
|
+
const cfgCurrent = cfgRead ? configValueByPath(cfgRead, cfgField) : null
|
|
1867
|
+
const cfgFieldLabel = cfgMeta ? cfgMeta.label[getLang()] : cfgField
|
|
1868
|
+
|
|
1761
1869
|
// Audit: don't leak flash timers across settings unmounts.
|
|
1762
1870
|
const cfgTimers = React.useRef<number[]>([])
|
|
1763
1871
|
React.useEffect(() => () => {
|
|
@@ -1772,8 +1880,9 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1772
1880
|
}
|
|
1773
1881
|
|
|
1774
1882
|
/** Flash a per-slot "已复制" state for the given duration. */
|
|
1775
|
-
const flashCopied = (slot: 'guide' | 'status' | 'pull') => {
|
|
1776
|
-
const setter =
|
|
1883
|
+
const flashCopied = (slot: 'guide' | 'status' | 'pull' | 'cfgRead') => {
|
|
1884
|
+
const setter =
|
|
1885
|
+
slot === 'guide' ? setGuideCopied : slot === 'status' ? setStatusCopied : slot === 'pull' ? setPullCopied : setCfgReadCopied
|
|
1777
1886
|
setter(true)
|
|
1778
1887
|
cfgTimers.current.push(window.setTimeout(() => setter(false), 1600))
|
|
1779
1888
|
}
|
|
@@ -1781,7 +1890,7 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1781
1890
|
/** Copy a guide/status block; flash success only when it actually copied,
|
|
1782
1891
|
* and surface a visible error when the clipboard write is refused (the old
|
|
1783
1892
|
* behaviour was a silent no-op, which reads as a broken button). */
|
|
1784
|
-
const doCopy = (text: string, slot: 'guide' | 'status' | 'pull') => {
|
|
1893
|
+
const doCopy = (text: string, slot: 'guide' | 'status' | 'pull' | 'cfgRead') => {
|
|
1785
1894
|
copyText(text).then((ok) => {
|
|
1786
1895
|
setCopyError(ok ? null : slot)
|
|
1787
1896
|
if (ok) flashCopied(slot)
|
|
@@ -1801,6 +1910,19 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1801
1910
|
})
|
|
1802
1911
|
}
|
|
1803
1912
|
|
|
1913
|
+
/** Copy the read-back instruction; a refused clipboard reveals it below. */
|
|
1914
|
+
const doCopyConfigRead = () => {
|
|
1915
|
+
const text = buildConfigReadInstruction()
|
|
1916
|
+
copyText(text).then((ok) => {
|
|
1917
|
+
setCopyError(ok ? null : 'cfgRead')
|
|
1918
|
+
if (ok) {
|
|
1919
|
+
flashCopied('cfgRead')
|
|
1920
|
+
return
|
|
1921
|
+
}
|
|
1922
|
+
setCfgReadText(text)
|
|
1923
|
+
})
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1804
1926
|
/** Two-step destroy guard: first click arms, second click clears. */
|
|
1805
1927
|
const requestClear = () => {
|
|
1806
1928
|
if (confirming) {
|
|
@@ -1815,27 +1937,27 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1815
1937
|
}
|
|
1816
1938
|
|
|
1817
1939
|
const clearButton = clearedInfo !== null
|
|
1818
|
-
? React.createElement('button', { className: 'iterate-btn', 'data-copied': '', disabled: true }, `已清除
|
|
1940
|
+
? React.createElement('button', { className: 'iterate-btn', 'data-copied': '', disabled: true }, tf(`已清除 {a0} 条`, { a0: clearedInfo }))
|
|
1819
1941
|
: React.createElement('button', {
|
|
1820
1942
|
className: 'iterate-btn',
|
|
1821
1943
|
'data-danger': confirming ? undefined : '',
|
|
1822
1944
|
'data-confirm': confirming ? '' : undefined,
|
|
1823
1945
|
onClick: requestClear,
|
|
1824
|
-
title: confirming ? '再次点击以确认清除全部判定' : '清除所有分诊判定记录(需要二次确认)',
|
|
1825
|
-
}, confirming ? '确认清除?' : '清除分诊')
|
|
1946
|
+
title: confirming ? t('再次点击以确认清除全部判定') : t('清除所有分诊判定记录(需要二次确认)'),
|
|
1947
|
+
}, confirming ? t('确认清除?') : t('清除分诊'))
|
|
1826
1948
|
|
|
1827
1949
|
const themeCard = React.createElement('div', { key: 'theme', className: 'iterate-scard' },
|
|
1828
1950
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1829
1951
|
React.createElement('div', {},
|
|
1830
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '迭代主题'),
|
|
1831
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。'),
|
|
1952
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('迭代主题')),
|
|
1953
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。')),
|
|
1832
1954
|
),
|
|
1833
1955
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1834
|
-
React.createElement('span', { className: 'iterate-chip', 'data-ok': enabled ? '' : undefined }, enabled ? '已启用' : '已关闭'),
|
|
1956
|
+
React.createElement('span', { className: 'iterate-chip', 'data-ok': enabled ? '' : undefined }, enabled ? t('已启用') : t('已关闭')),
|
|
1835
1957
|
React.createElement('button', {
|
|
1836
1958
|
role: 'switch',
|
|
1837
1959
|
'aria-checked': enabled,
|
|
1838
|
-
'aria-label': '开关 iterate 主题',
|
|
1960
|
+
'aria-label': t('开关 iterate 主题'),
|
|
1839
1961
|
className: 'iterate-switch',
|
|
1840
1962
|
'data-on': enabled ? '' : undefined,
|
|
1841
1963
|
onClick: toggleTheme,
|
|
@@ -1844,14 +1966,36 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1844
1966
|
),
|
|
1845
1967
|
)
|
|
1846
1968
|
|
|
1969
|
+
const langCard = React.createElement('div', { key: 'lang', className: 'iterate-scard' },
|
|
1970
|
+
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1971
|
+
React.createElement('div', {},
|
|
1972
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('界面语言')),
|
|
1973
|
+
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
1974
|
+
t('面板文案的显示语言;复制给模型的指令保持中文,与 SKILL.md 的工作流契约一致。'),
|
|
1975
|
+
),
|
|
1976
|
+
),
|
|
1977
|
+
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1978
|
+
React.createElement('span', { className: 'iterate-chip', 'data-ok': '' }, LANG_NAMES[lang]),
|
|
1979
|
+
React.createElement('button', {
|
|
1980
|
+
role: 'switch',
|
|
1981
|
+
'aria-checked': lang === 'en',
|
|
1982
|
+
'aria-label': t('切换界面语言'),
|
|
1983
|
+
className: 'iterate-switch',
|
|
1984
|
+
'data-on': lang === 'en' ? '' : undefined,
|
|
1985
|
+
onClick: () => setLang(lang === 'en' ? 'zh' : 'en'),
|
|
1986
|
+
}, React.createElement('span', { className: 'iterate-switch-knob' })),
|
|
1987
|
+
),
|
|
1988
|
+
),
|
|
1989
|
+
)
|
|
1990
|
+
|
|
1847
1991
|
const dataCard = React.createElement('div', { key: 'data', className: 'iterate-scard' },
|
|
1848
1992
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1849
1993
|
React.createElement('div', {},
|
|
1850
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '分诊持久化'),
|
|
1851
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, 'y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。'),
|
|
1994
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('分诊持久化')),
|
|
1995
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。')),
|
|
1852
1996
|
),
|
|
1853
1997
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1854
|
-
React.createElement('span', { className: 'iterate-chip' }, '本地保存'),
|
|
1998
|
+
React.createElement('span', { className: 'iterate-chip' }, t('本地保存')),
|
|
1855
1999
|
clearButton,
|
|
1856
2000
|
),
|
|
1857
2001
|
),
|
|
@@ -1860,12 +2004,12 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1860
2004
|
const guideCard = React.createElement('div', { key: 'guide', className: 'iterate-scard' },
|
|
1861
2005
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1862
2006
|
React.createElement('div', {},
|
|
1863
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
|
|
1864
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。'),
|
|
2007
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('配置管理')),
|
|
2008
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。')),
|
|
1865
2009
|
),
|
|
1866
2010
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1867
|
-
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': guideCopied ? '' : undefined, onClick: () => doCopy(guide, 'guide') }, guideCopied ? '已复制' : '复制指引'),
|
|
1868
|
-
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowGuide((v) => !v) }, showGuide ? '收起' : '展开'),
|
|
2011
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': guideCopied ? '' : undefined, onClick: () => doCopy(guide, 'guide') }, guideCopied ? t('已复制') : t('复制指引')),
|
|
2012
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowGuide((v) => !v) }, showGuide ? t('收起') : t('展开')),
|
|
1869
2013
|
),
|
|
1870
2014
|
),
|
|
1871
2015
|
// Gap #8: wire up the previously-dead `buildConfigFieldInstruction` — pick
|
|
@@ -1875,32 +2019,60 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1875
2019
|
React.createElement('select', {
|
|
1876
2020
|
className: 'iterate-filter-select',
|
|
1877
2021
|
value: cfgField,
|
|
1878
|
-
'aria-label': '选择要修改的配置字段',
|
|
2022
|
+
'aria-label': t('选择要修改的配置字段'),
|
|
1879
2023
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setCfgField(e.target.value),
|
|
1880
2024
|
},
|
|
1881
2025
|
...CONFIG_EDIT_FIELDS.map((f) =>
|
|
1882
|
-
React.createElement('option', { key: f.key, value: f.key }, `${f.label}(${f.key})`),
|
|
2026
|
+
React.createElement('option', { key: f.key, value: f.key }, getLang() === 'en' ? `${f.label.en} (${f.key})` : `${f.label.zh}(${f.key})`),
|
|
1883
2027
|
),
|
|
1884
2028
|
),
|
|
1885
2029
|
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
1886
|
-
|
|
2030
|
+
cfgMeta?.hint[getLang()] ?? '',
|
|
1887
2031
|
),
|
|
1888
2032
|
React.createElement('button', {
|
|
1889
2033
|
className: 'iterate-btn', 'data-primary': '',
|
|
1890
2034
|
'data-copied': cfgCopied ? '' : undefined,
|
|
1891
2035
|
'data-failed': cfgFailed ? '' : undefined,
|
|
1892
2036
|
onClick: doBuildConfigInstruction,
|
|
1893
|
-
title: '生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)',
|
|
1894
|
-
}, cfgCopied ? '已复制' : cfgFailed ? '复制失败' : '生成写入指令'),
|
|
2037
|
+
title: t('生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)'),
|
|
2038
|
+
}, cfgCopied ? t('已复制') : cfgFailed ? t('复制失败') : t('生成写入指令')),
|
|
1895
2039
|
cfgFailed
|
|
1896
|
-
? React.createElement('span', { className: 'iterate-settings-desc' }, '剪贴板被拒绝——指令已展示在下方,请手动复制。')
|
|
2040
|
+
? React.createElement('span', { className: 'iterate-settings-desc' }, t('剪贴板被拒绝——指令已展示在下方,请手动复制。'))
|
|
1897
2041
|
: null,
|
|
1898
2042
|
),
|
|
2043
|
+
// Gap #1 (3.7.0): close the write -> read-back loop. Three honest states:
|
|
2044
|
+
// nothing echoed yet, the value the model read back, or a read-back that
|
|
2045
|
+
// never contained this field (defaults are in play — say so, don't blank).
|
|
2046
|
+
React.createElement('div', { className: 'iterate-guide-bar', style: { marginTop: 8, flexWrap: 'wrap' } },
|
|
2047
|
+
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
2048
|
+
!cfgRead
|
|
2049
|
+
? t('尚未回读当前配置——点「回读当前配置」复制指令发给模型,回显后即可在此看到当前值。')
|
|
2050
|
+
: cfgCurrent && cfgCurrent.resolved
|
|
2051
|
+
? tf('当前值 {a0}:{a1}', { a0: cfgFieldLabel, a1: formatConfigValue(cfgCurrent.value) })
|
|
2052
|
+
: tf('回读结果中没有 {a0}(该字段可能尚未写入配置文件)。', { a0: cfgFieldLabel }),
|
|
2053
|
+
),
|
|
2054
|
+
React.createElement('button', {
|
|
2055
|
+
className: 'iterate-btn',
|
|
2056
|
+
'data-primary': '',
|
|
2057
|
+
'data-copied': cfgReadCopied ? '' : undefined,
|
|
2058
|
+
onClick: doCopyConfigRead,
|
|
2059
|
+
title: t('复制回读指令:让模型调用 iterate_config(operation:"read") 并回显结果'),
|
|
2060
|
+
}, cfgReadCopied ? t('已复制') : t('回读当前配置')),
|
|
2061
|
+
),
|
|
2062
|
+
cfgReadText
|
|
2063
|
+
? React.createElement('div', { className: 'iterate-guide' },
|
|
2064
|
+
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
2065
|
+
React.createElement('span', {}, t('回读指令(粘贴给模型)')),
|
|
2066
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgReadText, 'cfgRead') }, t('复制')),
|
|
2067
|
+
),
|
|
2068
|
+
React.createElement('div', { className: 'iterate-guide-body' }, cfgReadText),
|
|
2069
|
+
)
|
|
2070
|
+
: null,
|
|
1899
2071
|
cfgText
|
|
1900
2072
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1901
2073
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1902
|
-
React.createElement('span', {}, '写入指令(粘贴给模型)'),
|
|
1903
|
-
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgText, 'guide') }, '复制'),
|
|
2074
|
+
React.createElement('span', {}, t('写入指令(粘贴给模型)')),
|
|
2075
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgText, 'guide') }, t('复制')),
|
|
1904
2076
|
),
|
|
1905
2077
|
React.createElement('div', { className: 'iterate-guide-body' }, cfgText),
|
|
1906
2078
|
)
|
|
@@ -1908,8 +2080,9 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1908
2080
|
showGuide
|
|
1909
2081
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1910
2082
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1911
|
-
React.createElement('span', {}, 'iterate.config.yaml 可编辑字段'),
|
|
1912
|
-
React.createElement('
|
|
2083
|
+
React.createElement('span', {}, t('iterate.config.yaml 可编辑字段')),
|
|
2084
|
+
React.createElement('span', { className: 'iterate-chip' }, t('中文(粘贴给模型的契约文案)')),
|
|
2085
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(guide, 'guide') }, t('复制')),
|
|
1913
2086
|
),
|
|
1914
2087
|
React.createElement('div', { className: 'iterate-guide-body' }, guide),
|
|
1915
2088
|
)
|
|
@@ -1919,26 +2092,27 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1919
2092
|
const statusCard = React.createElement('div', { key: 'status', className: 'iterate-scard' },
|
|
1920
2093
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1921
2094
|
React.createElement('div', {},
|
|
1922
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '状态概览'),
|
|
1923
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '查看运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。'),
|
|
2095
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('状态概览')),
|
|
2096
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('查看运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。')),
|
|
1924
2097
|
),
|
|
1925
2098
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1926
|
-
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': statusCopied ? '' : undefined, onClick: () => doCopy(statusGuide, 'status') }, statusCopied ? '已复制' : '复制指引'),
|
|
2099
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': statusCopied ? '' : undefined, onClick: () => doCopy(statusGuide, 'status') }, statusCopied ? t('已复制') : t('复制指引')),
|
|
1927
2100
|
// Gap #4: RUNTIME_ARTIFACTS stops being documentation-only — this
|
|
1928
2101
|
// copies the instruction that makes the model echo those artifacts
|
|
1929
2102
|
// into the session so the observatory panels can read them.
|
|
1930
2103
|
React.createElement('button', {
|
|
1931
2104
|
className: 'iterate-btn', 'data-copied': pullCopied ? '' : undefined,
|
|
1932
2105
|
onClick: () => doCopy(buildDiskSnapshotInstruction(), 'pull'),
|
|
1933
|
-
}, pullCopied ? '已复制' : '拉取磁盘快照'),
|
|
1934
|
-
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowStatus((v) => !v) }, showStatus ? '收起' : '展开'),
|
|
2106
|
+
}, pullCopied ? t('已复制') : t('拉取磁盘快照')),
|
|
2107
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowStatus((v) => !v) }, showStatus ? t('收起') : t('展开')),
|
|
1935
2108
|
),
|
|
1936
2109
|
),
|
|
1937
2110
|
showStatus
|
|
1938
2111
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1939
2112
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1940
|
-
React.createElement('span', {}, '运行时布局与清理'),
|
|
1941
|
-
React.createElement('
|
|
2113
|
+
React.createElement('span', {}, t('运行时布局与清理')),
|
|
2114
|
+
React.createElement('span', { className: 'iterate-chip' }, t('中文(粘贴给模型的契约文案)')),
|
|
2115
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(statusGuide, 'status') }, t('复制')),
|
|
1942
2116
|
),
|
|
1943
2117
|
React.createElement('div', { className: 'iterate-guide-body' }, statusGuide),
|
|
1944
2118
|
)
|
|
@@ -1949,25 +2123,26 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1949
2123
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1950
2124
|
React.createElement('div', {},
|
|
1951
2125
|
React.createElement('div', { className: 'iterate-settings-title' }, 'iterate'),
|
|
1952
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。'),
|
|
2126
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。')),
|
|
1953
2127
|
),
|
|
1954
2128
|
React.createElement('span', { className: 'iterate-pill' },
|
|
1955
2129
|
React.createElement('span', { className: 'iterate-pill-dot' }),
|
|
1956
|
-
'就绪',
|
|
2130
|
+
t('就绪'),
|
|
1957
2131
|
),
|
|
1958
2132
|
),
|
|
1959
2133
|
)
|
|
1960
2134
|
|
|
1961
2135
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'settings', className: 'iterate-settings' },
|
|
1962
|
-
React.createElement('div', { className: 'iterate-settings-title', style: { fontSize: 15, fontWeight: 700 } }, 'iterate 设置'),
|
|
2136
|
+
React.createElement('div', { className: 'iterate-settings-title', style: { fontSize: 15, fontWeight: 700 } }, t('iterate 设置')),
|
|
1963
2137
|
banner,
|
|
1964
2138
|
copyError
|
|
1965
2139
|
? React.createElement('div', { key: 'copy-err', className: 'iterate-scard', role: 'alert' },
|
|
1966
2140
|
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
1967
|
-
'剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。',
|
|
2141
|
+
t('剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。'),
|
|
1968
2142
|
),
|
|
1969
2143
|
)
|
|
1970
2144
|
: null,
|
|
2145
|
+
langCard,
|
|
1971
2146
|
themeCard,
|
|
1972
2147
|
dataCard,
|
|
1973
2148
|
guideCard,
|
|
@@ -1981,19 +2156,21 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1981
2156
|
* activity stream) is first so it is the default view; F-panels follow.
|
|
1982
2157
|
* Gap #14: labels carry the "Fn" number used everywhere else (README,
|
|
1983
2158
|
* code comments, the copy instructions), so the docs and the UI line up. */
|
|
2159
|
+
/** Visible labels per tab, per language (module constants are evaluated once,
|
|
2160
|
+
* so they carry BOTH languages and resolve at render time). */
|
|
1984
2161
|
const OBS_TABS = [
|
|
1985
|
-
{ key: 'live', label: '实时活动流', hotkey: '' },
|
|
1986
|
-
{ key: 'f1', label: '审查线程', hotkey: '1' },
|
|
1987
|
-
{ key: 'f2', label: '收敛趋势', hotkey: '2' },
|
|
1988
|
-
{ key: 'f3', label: '发现定位', hotkey: '3' },
|
|
1989
|
-
{ key: 'f4', label: '修复与回滚', hotkey: '4' },
|
|
1990
|
-
{ key: 'f5', label: '断点恢复', hotkey: '5' },
|
|
1991
|
-
{ key: 'f6', label: '运行控制台', hotkey: '6' },
|
|
1992
|
-
{ key: 'f7', label: '决策时间线', hotkey: '7' },
|
|
2162
|
+
{ key: 'live', label: { zh: '实时活动流', en: 'Live activity' }, hotkey: '' },
|
|
2163
|
+
{ key: 'f1', label: { zh: '审查线程', en: 'Review threads' }, hotkey: '1' },
|
|
2164
|
+
{ key: 'f2', label: { zh: '收敛趋势', en: 'Convergence trend' }, hotkey: '2' },
|
|
2165
|
+
{ key: 'f3', label: { zh: '发现定位', en: 'Finding locations' }, hotkey: '3' },
|
|
2166
|
+
{ key: 'f4', label: { zh: '修复与回滚', en: 'Fixes & rollback' }, hotkey: '4' },
|
|
2167
|
+
{ key: 'f5', label: { zh: '断点恢复', en: 'Checkpoint resume' }, hotkey: '5' },
|
|
2168
|
+
{ key: 'f6', label: { zh: '运行控制台', en: 'Run console' }, hotkey: '6' },
|
|
2169
|
+
{ key: 'f7', label: { zh: '决策时间线', en: 'Decision timeline' }, hotkey: '7' },
|
|
1993
2170
|
// v3.0: Quality Command Center tabs
|
|
1994
|
-
{ key: 'f8', label: '质量门禁', hotkey: '8' },
|
|
1995
|
-
{ key: 'f9', label: '经验银行', hotkey: '9' },
|
|
1996
|
-
{ key: 'f10', label: '防御事件', hotkey: '0' },
|
|
2171
|
+
{ key: 'f8', label: { zh: '质量门禁', en: 'Quality gate' }, hotkey: '8' },
|
|
2172
|
+
{ key: 'f9', label: { zh: '经验银行', en: 'Experience bank' }, hotkey: '9' },
|
|
2173
|
+
{ key: 'f10', label: { zh: '防御事件', en: 'Defense events' }, hotkey: '0' },
|
|
1997
2174
|
] as const
|
|
1998
2175
|
|
|
1999
2176
|
/** Digit → tab id. `1-9` map to F1–F9, `0` to F10, `-` back to the live feed
|
|
@@ -2007,26 +2184,31 @@ const OBS_HOTKEY_TABS: Record<string, (typeof OBS_TABS)[number]['key']> = {
|
|
|
2007
2184
|
|
|
2008
2185
|
/** Visible label for a tab (keeps the number prefix in sync with `OBS_TABS`). */
|
|
2009
2186
|
function obsTabLabel(t: (typeof OBS_TABS)[number]): string {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2187
|
+
const label = t.label[getLang()]
|
|
2188
|
+
if (t.key === 'live') return label
|
|
2189
|
+
return `${t.key.toUpperCase()} ${label}`
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/** Hotkey hint shown in the collapsed header, e.g. "11 页 · 按 1-9/0 切 Fn、- 回实时流". */
|
|
2193
|
+
function obsHotkeyHint(): string {
|
|
2194
|
+
return getLang() === 'en'
|
|
2195
|
+
? `${String(OBS_TABS.length)} tabs · keys 1-9/0 switch Fn, - returns to the live feed`
|
|
2196
|
+
: tf(`{a0} 页 · 按 1-9/0 切 Fn、- 回实时流`, { a0: String(OBS_TABS.length) })
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/** Live subagent activity badges: type → label + dsw token color accent. */
|
|
2200
|
+
const OBS_LIVE_META: Record<string, { label: { zh: string; en: string }; color: string }> = {
|
|
2201
|
+
read: { label: { zh: '阅读', en: 'Read' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2202
|
+
fix: { label: { zh: '修复', en: 'Fix' }, color: 'var(--dsw-alias-state-success-primary)' },
|
|
2203
|
+
rollback: { label: { zh: '回滚', en: 'Rollback' }, color: 'var(--dsw-alias-state-error-primary)' },
|
|
2204
|
+
diff: { label: { zh: '差异', en: 'Diff' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2205
|
+
review: { label: { zh: '审查', en: 'Review' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2206
|
+
triage: { label: { zh: '分流', en: 'Triage' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2207
|
+
checkpoint: { label: { zh: '断点', en: 'Checkpoint' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2208
|
+
validate: { label: { zh: '校验', en: 'Validate' }, color: 'var(--dsw-alias-state-success-primary)' },
|
|
2209
|
+
log: { label: { zh: '记录', en: 'Log' }, color: 'var(--dsw-alias-label-secondary)' },
|
|
2210
|
+
prune: { label: { zh: '清理', en: 'Prune' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2211
|
+
info: { label: { zh: '信息', en: 'Info' }, color: 'var(--dsw-alias-label-secondary)' },
|
|
2030
2212
|
}
|
|
2031
2213
|
const OBS_LIVE_FALLBACK_COLOR = 'var(--dsw-alias-label-secondary)'
|
|
2032
2214
|
|
|
@@ -2097,6 +2279,9 @@ function buildObsFixInstruction(f: ObsFinding): string {
|
|
|
2097
2279
|
*/
|
|
2098
2280
|
function ObservatoryPanel(props: SlotProps) {
|
|
2099
2281
|
const session = props && props.session ? props.session : null
|
|
2282
|
+
React.useEffect(() => {
|
|
2283
|
+
noteSession(session)
|
|
2284
|
+
}, [session])
|
|
2100
2285
|
const manifest = latestTranscript(session)
|
|
2101
2286
|
// v3.1+: command-center data scanned straight out of the session stream.
|
|
2102
2287
|
const qualityGate = latestQualityGate(session)
|
|
@@ -2183,7 +2368,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2183
2368
|
// the one instruction that makes the model echo them into the session
|
|
2184
2369
|
// stream, which is what every panel scans.
|
|
2185
2370
|
const pullInstruction = buildDiskSnapshotInstruction()
|
|
2186
|
-
const pullTitle = `复制拉取磁盘快照指令:让模型逐个调用
|
|
2371
|
+
const pullTitle = tf(`复制拉取磁盘快照指令:让模型逐个调用 {a0} 并回显完整结果`, { a0: DISK_SNAPSHOT_SOURCES.map((s) => s.tool).join(' / ') })
|
|
2187
2372
|
|
|
2188
2373
|
/** Toggle one thread; a manual toggle exits any forced all/none mode. */
|
|
2189
2374
|
const toggleThread = (key: string) => {
|
|
@@ -2240,16 +2425,16 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2240
2425
|
if (!manifest) {
|
|
2241
2426
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
2242
2427
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': '' },
|
|
2243
|
-
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
2244
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, `暂无运行时观测数据 ·
|
|
2428
|
+
React.createElement('span', { className: 'iterate-obs-title' }, t('iterate 观测台')),
|
|
2429
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`暂无运行时观测数据 · {a0}`, { a0: obsHotkeyHint() })),
|
|
2245
2430
|
React.createElement('button', {
|
|
2246
2431
|
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
2247
2432
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
2248
2433
|
e.stopPropagation()
|
|
2249
2434
|
copyInstruction('pull-snapshot', pullInstruction)
|
|
2250
2435
|
},
|
|
2251
|
-
title: '复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充',
|
|
2252
|
-
}, copiedKey === 'pull-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
2436
|
+
title: t('复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充'),
|
|
2437
|
+
}, copiedKey === 'pull-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2253
2438
|
),
|
|
2254
2439
|
)
|
|
2255
2440
|
}
|
|
@@ -2275,49 +2460,49 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2275
2460
|
const renderThreads = () => {
|
|
2276
2461
|
const rounds = manifest.rounds || []
|
|
2277
2462
|
if (rounds.length === 0) {
|
|
2278
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无审查线程')
|
|
2463
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无审查线程'))
|
|
2279
2464
|
}
|
|
2280
2465
|
return React.createElement('div', {},
|
|
2281
2466
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
2282
|
-
React.createElement('b', {}, '审查线程'),
|
|
2283
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('all'), title: '展开全部线程' }, '全部展开'),
|
|
2284
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('none'), title: '收起全部线程' }, '全部收起'),
|
|
2467
|
+
React.createElement('b', {}, t('审查线程')),
|
|
2468
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('all'), title: t('展开全部线程') }, t('全部展开')),
|
|
2469
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('none'), title: t('收起全部线程') }, t('全部收起')),
|
|
2285
2470
|
React.createElement('span', { className: 'iterate-filter-count' },
|
|
2286
|
-
|
|
2471
|
+
tf(`{a0} · {a1} 轮`, { a0: threadMode === 'all' ? t('全部展开') : threadMode === 'none' ? t('全部收起') : t('自由切换'), a1: rounds.length }),
|
|
2287
2472
|
),
|
|
2288
2473
|
),
|
|
2289
2474
|
...rounds.map((r, ri) => {
|
|
2290
2475
|
const threads = r.threads || []
|
|
2291
2476
|
const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0)
|
|
2292
2477
|
const threadBlocks = threads.length === 0
|
|
2293
|
-
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, '本轮无线程')]
|
|
2294
|
-
: threads.map((
|
|
2478
|
+
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, t('本轮无线程'))]
|
|
2479
|
+
: threads.map((th, ti) => {
|
|
2295
2480
|
const key = `${ri}-${ti}`
|
|
2296
2481
|
const expanded = isThreadExpanded(key)
|
|
2297
|
-
const dim =
|
|
2298
|
-
const tFindings =
|
|
2299
|
-
const files =
|
|
2300
|
-
const msgs =
|
|
2482
|
+
const dim = th.dimension || t('未命名维度')
|
|
2483
|
+
const tFindings = th.findings || []
|
|
2484
|
+
const files = th.readFiles || []
|
|
2485
|
+
const msgs = th.messages || []
|
|
2301
2486
|
return React.createElement('div', { key, className: 'iterate-obs-block' },
|
|
2302
|
-
React.createElement('div', { className: 'iterate-obs-block-head', 'data-click': '', onClick: () => toggleThread(key), title: expanded ? '收起线程' : '展开线程' },
|
|
2487
|
+
React.createElement('div', { className: 'iterate-obs-block-head', 'data-click': '', onClick: () => toggleThread(key), title: expanded ? t('收起线程') : t('展开线程') },
|
|
2303
2488
|
React.createElement('span', {}, expanded ? '−' : '+'),
|
|
2304
2489
|
React.createElement('b', {}, dim),
|
|
2305
|
-
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof
|
|
2490
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof th.attempt === 'number' ? th.attempt : '?'}`),
|
|
2306
2491
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${tFindings.length} findings`),
|
|
2307
2492
|
),
|
|
2308
2493
|
expanded
|
|
2309
2494
|
? React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2310
2495
|
React.createElement('div', { className: 'iterate-obs-bar' },
|
|
2311
|
-
React.createElement('b', {}, '叙述'),
|
|
2312
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2496
|
+
React.createElement('b', {}, t('叙述')),
|
|
2497
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`{a0} 条`, { a0: msgs.length })),
|
|
2313
2498
|
),
|
|
2314
2499
|
...(msgs.length === 0
|
|
2315
|
-
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, '无线索叙述')]
|
|
2500
|
+
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, t('无线索叙述'))]
|
|
2316
2501
|
: msgs.map((m, mi) => React.createElement('div', { key: mi, className: 'iterate-obs-msg' }, String(m)))),
|
|
2317
2502
|
...(files.length > 0
|
|
2318
2503
|
? [
|
|
2319
2504
|
React.createElement('div', { key: 'files-h', className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2320
|
-
React.createElement('b', {}, '读取文件'),
|
|
2505
|
+
React.createElement('b', {}, t('读取文件')),
|
|
2321
2506
|
),
|
|
2322
2507
|
...files.map((f, fi) =>
|
|
2323
2508
|
React.createElement('div', { key: `f${fi}`, className: 'iterate-obs-file' }, String(f)),
|
|
@@ -2325,10 +2510,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2325
2510
|
]
|
|
2326
2511
|
: []),
|
|
2327
2512
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2328
|
-
React.createElement('b', {}, '发现'),
|
|
2513
|
+
React.createElement('b', {}, t('发现')),
|
|
2329
2514
|
),
|
|
2330
2515
|
...(tFindings.length === 0
|
|
2331
|
-
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, '本线程无发现')]
|
|
2516
|
+
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, t('本线程无发现'))]
|
|
2332
2517
|
: tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))),
|
|
2333
2518
|
)
|
|
2334
2519
|
: null,
|
|
@@ -2337,7 +2522,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2337
2522
|
return React.createElement('div', { key: ri, className: 'iterate-obs-block' },
|
|
2338
2523
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2339
2524
|
React.createElement('b', {}, `Round ${typeof r.round === 'number' ? r.round : ri + 1}`),
|
|
2340
|
-
React.createElement('span', { className: 'iterate-obs-chip' },
|
|
2525
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, tf(`{a0} 线程`, { a0: threads.length })),
|
|
2341
2526
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `${fCount} findings`),
|
|
2342
2527
|
),
|
|
2343
2528
|
React.createElement('div', { className: 'iterate-obs-block-body' }, ...threadBlocks),
|
|
@@ -2349,7 +2534,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2349
2534
|
const renderTrend = () => {
|
|
2350
2535
|
const conv = manifest.convergence || []
|
|
2351
2536
|
if (conv.length === 0) {
|
|
2352
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无收敛数据')
|
|
2537
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无收敛数据'))
|
|
2353
2538
|
}
|
|
2354
2539
|
const points = conv.map((n, i) => ({ round: i + 1, count: n }))
|
|
2355
2540
|
// Gap #13 (step ①): single-session cross-round comparison. One row per
|
|
@@ -2358,17 +2543,17 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2358
2543
|
// client has no filesystem access, so `iterate_history` aggregates have to
|
|
2359
2544
|
// be echoed back into the session first).
|
|
2360
2545
|
const report = latestReport(session)
|
|
2361
|
-
const comparison = buildRoundComparison(report, manifest.validations || [])
|
|
2546
|
+
const comparison = buildRoundComparison(report, manifest.validations || [], manifest.fixes || [])
|
|
2362
2547
|
const comparisonTable = comparison.length === 0
|
|
2363
2548
|
? null
|
|
2364
2549
|
: React.createElement('table', { className: 'iterate-obs-table' },
|
|
2365
2550
|
React.createElement('thead', {},
|
|
2366
2551
|
React.createElement('tr', {},
|
|
2367
|
-
React.createElement('th', {}, '轮次'),
|
|
2368
|
-
React.createElement('th', {}, '发现'),
|
|
2369
|
-
React.createElement('th', {}, '已修复'),
|
|
2370
|
-
React.createElement('th', {}, '严重度分布'),
|
|
2371
|
-
React.createElement('th', {}, '验证'),
|
|
2552
|
+
React.createElement('th', {}, t('轮次')),
|
|
2553
|
+
React.createElement('th', {}, t('发现')),
|
|
2554
|
+
React.createElement('th', {}, t('已修复')),
|
|
2555
|
+
React.createElement('th', {}, t('严重度分布')),
|
|
2556
|
+
React.createElement('th', {}, t('验证')),
|
|
2372
2557
|
),
|
|
2373
2558
|
),
|
|
2374
2559
|
React.createElement('tbody', {},
|
|
@@ -2379,7 +2564,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2379
2564
|
const anyFailed = row.validations.some((v) => v.allowed !== true)
|
|
2380
2565
|
const valText = row.validations.length === 0
|
|
2381
2566
|
? '—'
|
|
2382
|
-
:
|
|
2567
|
+
: tf(`{a0}/{a1} 通过{a2}`, { a0: row.validations.filter((v) => v.allowed === true).length, a1: row.validations.length, a2: anyFailed ? t('(含失败)') : '' })
|
|
2383
2568
|
return React.createElement('tr', { key: `cmp-${row.round}` },
|
|
2384
2569
|
React.createElement('td', {}, `Round ${row.round}`),
|
|
2385
2570
|
React.createElement('td', {}, String(row.findings)),
|
|
@@ -2399,14 +2584,14 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2399
2584
|
return React.createElement('div', {},
|
|
2400
2585
|
React.createElement(TrendChart, { points }),
|
|
2401
2586
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2402
|
-
|
|
2587
|
+
tf(`各轮发现数量:{a0}{a1}`, { a0: conv.join(' → '), a1: typeof manifest.round === 'number' ? tf(` · 当前 Round {a0}`, { a0: manifest.round }) : '' }),
|
|
2403
2588
|
),
|
|
2404
2589
|
comparisonTable
|
|
2405
2590
|
? React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2406
2591
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2407
|
-
React.createElement('span', {}, '跨轮对比(本会话)'),
|
|
2592
|
+
React.createElement('span', {}, t('跨轮对比(本会话)')),
|
|
2408
2593
|
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2409
|
-
'跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行',
|
|
2594
|
+
t('跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行'),
|
|
2410
2595
|
),
|
|
2411
2596
|
),
|
|
2412
2597
|
React.createElement('div', { className: 'iterate-obs-block-body' }, comparisonTable),
|
|
@@ -2419,7 +2604,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2419
2604
|
const renderFindings = () => {
|
|
2420
2605
|
const findings = manifest.findings || []
|
|
2421
2606
|
if (findings.length === 0) {
|
|
2422
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
2607
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无发现'))
|
|
2423
2608
|
}
|
|
2424
2609
|
const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
|
|
2425
2610
|
const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
|
|
@@ -2433,26 +2618,26 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2433
2618
|
}))
|
|
2434
2619
|
}
|
|
2435
2620
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
2436
|
-
React.createElement('b', {}, '发现'),
|
|
2621
|
+
React.createElement('b', {}, t('发现')),
|
|
2437
2622
|
...opts.severities.map((s) =>
|
|
2438
2623
|
React.createElement('button', {
|
|
2439
2624
|
key: `sev-${s.value}`,
|
|
2440
2625
|
className: 'iterate-filter-chip',
|
|
2441
2626
|
'data-active': f3Filter.severities.includes(s.value) ? '' : undefined,
|
|
2442
|
-
title:
|
|
2627
|
+
title: tf(`{a0}({a1} 项)`, { a0: s.value, a1: s.count }),
|
|
2443
2628
|
onClick: () => toggleSeverity(s.value),
|
|
2444
2629
|
}, `${s.value} ${s.count}`),
|
|
2445
2630
|
),
|
|
2446
2631
|
React.createElement('select', {
|
|
2447
2632
|
className: 'iterate-filter-select',
|
|
2448
2633
|
value: f3Filter.dimensions[0] || '',
|
|
2449
|
-
'aria-label': '按维度筛选',
|
|
2634
|
+
'aria-label': t('按维度筛选'),
|
|
2450
2635
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
2451
2636
|
const v = e.target.value
|
|
2452
2637
|
setF3Filter((prev) => ({ ...prev, dimensions: v ? [v] : [] }))
|
|
2453
2638
|
},
|
|
2454
2639
|
},
|
|
2455
|
-
React.createElement('option', { value: '' }, '全部维度'),
|
|
2640
|
+
React.createElement('option', { value: '' }, t('全部维度')),
|
|
2456
2641
|
...opts.dimensions.map((d) =>
|
|
2457
2642
|
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
2458
2643
|
),
|
|
@@ -2460,8 +2645,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2460
2645
|
React.createElement('input', {
|
|
2461
2646
|
className: 'iterate-filter-search',
|
|
2462
2647
|
type: 'search',
|
|
2463
|
-
placeholder: '搜索发现…',
|
|
2464
|
-
'aria-label': '搜索发现',
|
|
2648
|
+
placeholder: t('搜索发现…'),
|
|
2649
|
+
'aria-label': t('搜索发现'),
|
|
2465
2650
|
value: f3Filter.search,
|
|
2466
2651
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
2467
2652
|
setF3Filter((prev) => ({ ...prev, search: e.target.value })),
|
|
@@ -2470,8 +2655,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2470
2655
|
? React.createElement('button', {
|
|
2471
2656
|
className: 'iterate-filter-clear',
|
|
2472
2657
|
onClick: () => setF3Filter({ severities: [], dimensions: [], search: '' }),
|
|
2473
|
-
title: '清除全部筛选',
|
|
2474
|
-
}, '清除筛选')
|
|
2658
|
+
title: t('清除全部筛选'),
|
|
2659
|
+
}, t('清除筛选'))
|
|
2475
2660
|
: null,
|
|
2476
2661
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${findings.length}`),
|
|
2477
2662
|
)
|
|
@@ -2494,7 +2679,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2494
2679
|
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
2495
2680
|
React.createElement('span', {}, String(finding.dimension || '')),
|
|
2496
2681
|
finding.acknowledged === true
|
|
2497
|
-
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
2682
|
+
? React.createElement('span', { className: 'iterate-obs-chip' }, t('已确认'))
|
|
2498
2683
|
: null,
|
|
2499
2684
|
),
|
|
2500
2685
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2503,18 +2688,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2503
2688
|
React.createElement('button', {
|
|
2504
2689
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
2505
2690
|
onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
|
|
2506
|
-
title: '复制该 finding 的建议修复文本',
|
|
2507
|
-
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
2691
|
+
title: t('复制该 finding 的建议修复文本'),
|
|
2692
|
+
}, copiedKey === `${k}-fix` ? t('已复制') : t('复制 suggested_fix')),
|
|
2508
2693
|
React.createElement('button', {
|
|
2509
2694
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-known` ? '' : undefined,
|
|
2510
2695
|
onClick: () => copyInstruction(`${k}-known`, knownInstruction),
|
|
2511
|
-
title: '复制 iterate_triage 指令文本(标记为 known_intentional)',
|
|
2512
|
-
}, copiedKey === `${k}-known` ? '已复制' : 'known_intentional'),
|
|
2696
|
+
title: t('复制 iterate_triage 指令文本(标记为 known_intentional)'),
|
|
2697
|
+
}, copiedKey === `${k}-known` ? t('已复制') : 'known_intentional'),
|
|
2513
2698
|
React.createElement('button', {
|
|
2514
2699
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `${k}-repair` ? '' : undefined,
|
|
2515
2700
|
onClick: () => copyInstruction(`${k}-repair`, fixInstruction),
|
|
2516
|
-
title: '复制 iterate_fix 指令文本',
|
|
2517
|
-
}, copiedKey === `${k}-repair` ? '已复制' : '复制修复指令'),
|
|
2701
|
+
title: t('复制 iterate_fix 指令文本'),
|
|
2702
|
+
}, copiedKey === `${k}-repair` ? t('已复制') : t('复制修复指令')),
|
|
2518
2703
|
),
|
|
2519
2704
|
),
|
|
2520
2705
|
)
|
|
@@ -2522,7 +2707,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2522
2707
|
if (filtered.length === 0) {
|
|
2523
2708
|
return React.createElement('div', {},
|
|
2524
2709
|
filterBar,
|
|
2525
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的发现'),
|
|
2710
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的发现')),
|
|
2526
2711
|
)
|
|
2527
2712
|
}
|
|
2528
2713
|
return React.createElement('div', {}, filterBar, ...findingBlocks)
|
|
@@ -2532,7 +2717,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2532
2717
|
const renderFixes = () => {
|
|
2533
2718
|
const fixes = manifest.fixes || []
|
|
2534
2719
|
if (fixes.length === 0) {
|
|
2535
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无修复记录')
|
|
2720
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无修复记录'))
|
|
2536
2721
|
}
|
|
2537
2722
|
return React.createElement('div', {}, ...fixes.map((f, i) => {
|
|
2538
2723
|
const k = `f4-${i}`
|
|
@@ -2544,7 +2729,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2544
2729
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2545
2730
|
React.createElement('b', {}, id),
|
|
2546
2731
|
React.createElement('span', { className: 'iterate-obs-file' }, String(f.file || '?')),
|
|
2547
|
-
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? '成功' : '失败'),
|
|
2732
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? t('成功') : t('失败')),
|
|
2548
2733
|
typeof f.round === 'number'
|
|
2549
2734
|
? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${f.round}`)
|
|
2550
2735
|
: null,
|
|
@@ -2557,8 +2742,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2557
2742
|
React.createElement('button', {
|
|
2558
2743
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === `${k}-rb` ? '' : undefined,
|
|
2559
2744
|
onClick: () => copyInstruction(`${k}-rb`, rollbackText),
|
|
2560
|
-
title: '复制 iterate_rollback 指令文本',
|
|
2561
|
-
}, copiedKey === `${k}-rb` ? '已复制' : '回滚'),
|
|
2745
|
+
title: t('复制 iterate_rollback 指令文本'),
|
|
2746
|
+
}, copiedKey === `${k}-rb` ? t('已复制') : t('回滚')),
|
|
2562
2747
|
),
|
|
2563
2748
|
),
|
|
2564
2749
|
)
|
|
@@ -2570,7 +2755,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2570
2755
|
const cp = manifest.checkpoint
|
|
2571
2756
|
if (!cp) {
|
|
2572
2757
|
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2573
|
-
diskEmptyStateText(toolCalledInSession(session, 'iterate_transcript'), '断点'),
|
|
2758
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_transcript'), t('断点'), getLang()),
|
|
2574
2759
|
)
|
|
2575
2760
|
}
|
|
2576
2761
|
// Gap #10: `resume` accepts ONLY `operation` — `mode` / `round` /
|
|
@@ -2586,7 +2771,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2586
2771
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
2587
2772
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2588
2773
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2589
|
-
React.createElement('b', {}, '断点'),
|
|
2774
|
+
React.createElement('b', {}, t('断点')),
|
|
2590
2775
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, String(cp.updatedAt || '')),
|
|
2591
2776
|
),
|
|
2592
2777
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2601,13 +2786,13 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2601
2786
|
React.createElement('button', {
|
|
2602
2787
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'cp-resume' ? '' : undefined,
|
|
2603
2788
|
onClick: () => copyInstruction('cp-resume', resumeText),
|
|
2604
|
-
title: '复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)',
|
|
2605
|
-
}, copiedKey === 'cp-resume' ? '已复制' : '复制恢复指令'),
|
|
2789
|
+
title: t('复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)'),
|
|
2790
|
+
}, copiedKey === 'cp-resume' ? t('已复制') : t('复制恢复指令')),
|
|
2606
2791
|
React.createElement('button', {
|
|
2607
2792
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'cp-clear' ? '' : undefined,
|
|
2608
2793
|
onClick: () => copyInstruction('cp-clear', clearText),
|
|
2609
|
-
title: '复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)',
|
|
2610
|
-
}, copiedKey === 'cp-clear' ? '已复制' : '清除断点'),
|
|
2794
|
+
title: t('复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)'),
|
|
2795
|
+
}, copiedKey === 'cp-clear' ? t('已复制') : t('清除断点')),
|
|
2611
2796
|
),
|
|
2612
2797
|
),
|
|
2613
2798
|
)
|
|
@@ -2616,16 +2801,16 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2616
2801
|
// ── F6: run console (status / approval / nudge draft) ──────────────────
|
|
2617
2802
|
const renderConsole = () => {
|
|
2618
2803
|
const approval = manifest.approval || { policy: 'ask' }
|
|
2619
|
-
const policyLabelMap: Record<string, string> = { ask: '询问用户', deny: '拒绝', allow: '直接执行' }
|
|
2804
|
+
const policyLabelMap: Record<string, string> = { ask: t('询问用户'), deny: t('拒绝'), allow: t('直接执行') }
|
|
2620
2805
|
const policyLabel = policyLabelMap[String(approval.policy || 'ask')] || String(approval.policy || 'ask')
|
|
2621
2806
|
const nudge = manifest.nudge || null
|
|
2622
2807
|
const present = Boolean(nudge && nudge.text)
|
|
2623
2808
|
const activeNudgeText = nudge && nudge.text ? nudge.text : ''
|
|
2624
|
-
const nudgeInstruction = `请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n
|
|
2809
|
+
const nudgeInstruction = tf(`请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'nudge', text: nudgeText }, null, 2) })
|
|
2625
2810
|
// Clearing the PERSISTED nudge (as opposed to the draft) requires
|
|
2626
2811
|
// text:null. Copied as a paste-able instruction, mirroring the panel's
|
|
2627
2812
|
// copy-to-command pattern — it cannot be cleared from the client directly.
|
|
2628
|
-
const activeClearInstruction = `请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n
|
|
2813
|
+
const activeClearInstruction = tf(`请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'nudge', text: null }, null, 2) })
|
|
2629
2814
|
// Gap #6: per-round validation rows (command / exitCode / allowed) live on
|
|
2630
2815
|
// the manifest; when the run stopped on a validation failure the failing
|
|
2631
2816
|
// command is called out explicitly instead of being buried in a list.
|
|
@@ -2646,54 +2831,54 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2646
2831
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${typeof v.round === 'number' ? v.round : '?'}`),
|
|
2647
2832
|
React.createElement('code', { className: 'iterate-obs-mono' }, String(v.command || '')),
|
|
2648
2833
|
React.createElement('span', { className: 'iterate-obs-chip', 'data-status': allowed ? 'pass' : 'fail' },
|
|
2649
|
-
allowed ? '通过' : '失败',
|
|
2834
|
+
allowed ? t('通过') : t('失败'),
|
|
2650
2835
|
),
|
|
2651
2836
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, exit),
|
|
2652
|
-
highlight ? React.createElement('span', { className: 'iterate-obs-chip', 'data-status': 'fail' }, '导致中止') : null,
|
|
2837
|
+
highlight ? React.createElement('span', { className: 'iterate-obs-chip', 'data-status': 'fail' }, t('导致中止')) : null,
|
|
2653
2838
|
)
|
|
2654
2839
|
})
|
|
2655
2840
|
const validationsBlock = React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2656
2841
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2657
|
-
React.createElement('span', {}, '验证结果'),
|
|
2842
|
+
React.createElement('span', {}, t('验证结果')),
|
|
2658
2843
|
validations.length === 0
|
|
2659
|
-
? React.createElement('span', { className: 'iterate-obs-head-meta' }, '本轮尚无验证记录')
|
|
2660
|
-
: React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2844
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, t('本轮尚无验证记录'))
|
|
2845
|
+
: React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`{a0} 条命令`, { a0: validations.length })),
|
|
2661
2846
|
abortedByValidation
|
|
2662
|
-
? React.createElement('span', { className: 'iterate-obs-badge', 'data-status': 'fail' }, '因验证失败中止')
|
|
2847
|
+
? React.createElement('span', { className: 'iterate-obs-badge', 'data-status': 'fail' }, t('因验证失败中止'))
|
|
2663
2848
|
: null,
|
|
2664
2849
|
),
|
|
2665
2850
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2666
2851
|
validations.length === 0
|
|
2667
2852
|
? React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2668
|
-
'尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。',
|
|
2853
|
+
t('尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。'),
|
|
2669
2854
|
)
|
|
2670
2855
|
: React.createElement(React.Fragment, null, ...validationRows),
|
|
2671
2856
|
),
|
|
2672
2857
|
)
|
|
2673
2858
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2674
2859
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2675
|
-
React.createElement('span', {}, '运行控制台'),
|
|
2860
|
+
React.createElement('span', {}, t('运行控制台')),
|
|
2676
2861
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2677
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2862
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`批准策略:{a0}`, { a0: policyLabel })),
|
|
2678
2863
|
),
|
|
2679
2864
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2680
2865
|
present
|
|
2681
2866
|
? React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 6 } },
|
|
2682
|
-
React.createElement('b', {}, '当前 nudge'),
|
|
2867
|
+
React.createElement('b', {}, t('当前 nudge')),
|
|
2683
2868
|
React.createElement('span', { className: 'iterate-obs-msg' }, activeNudgeText),
|
|
2684
2869
|
React.createElement('button', {
|
|
2685
2870
|
className: 'iterate-btn', 'data-copied': copiedKey === 'nudge-clear' ? '' : undefined,
|
|
2686
2871
|
onClick: () => copyInstruction('nudge-clear', activeClearInstruction),
|
|
2687
|
-
title: '复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge',
|
|
2688
|
-
}, copiedKey === 'nudge-clear' ? '已复制清除指令' : '复制清除指令'),
|
|
2872
|
+
title: t('复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge'),
|
|
2873
|
+
}, copiedKey === 'nudge-clear' ? t('已复制清除指令') : t('复制清除指令')),
|
|
2689
2874
|
)
|
|
2690
2875
|
: null,
|
|
2691
2876
|
React.createElement('textarea', {
|
|
2692
2877
|
className: 'iterate-obs-input iterate-obs-mono',
|
|
2693
2878
|
rows: 3,
|
|
2694
|
-
placeholder: '起草一条 nudge 方向指令…',
|
|
2879
|
+
placeholder: t('起草一条 nudge 方向指令…'),
|
|
2695
2880
|
value: nudgeText,
|
|
2696
|
-
'aria-label': 'nudge 草稿',
|
|
2881
|
+
'aria-label': t('nudge 草稿'),
|
|
2697
2882
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setNudgeText(e.target.value),
|
|
2698
2883
|
}),
|
|
2699
2884
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
@@ -2701,8 +2886,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2701
2886
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'nudge' ? '' : undefined,
|
|
2702
2887
|
disabled: !nudgeText,
|
|
2703
2888
|
onClick: () => copyInstruction('nudge', nudgeInstruction),
|
|
2704
|
-
title: '复制 iterate_transcript nudge 指令文本',
|
|
2705
|
-
}, copiedKey === 'nudge' ? '已复制' : '复制 nudge 指令'),
|
|
2889
|
+
title: t('复制 iterate_transcript nudge 指令文本'),
|
|
2890
|
+
}, copiedKey === 'nudge' ? t('已复制') : t('复制 nudge 指令')),
|
|
2706
2891
|
),
|
|
2707
2892
|
validationsBlock,
|
|
2708
2893
|
),
|
|
@@ -2713,7 +2898,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2713
2898
|
const renderTimeline = () => {
|
|
2714
2899
|
const entries = manifest.timeline || []
|
|
2715
2900
|
if (entries.length === 0) {
|
|
2716
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无时间线条目')
|
|
2901
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无时间线条目'))
|
|
2717
2902
|
}
|
|
2718
2903
|
const types = Array.from(new Set(entries.map((t) => String(t.type || 'unknown')).filter(Boolean))).sort()
|
|
2719
2904
|
const rounds = Array.from(
|
|
@@ -2749,30 +2934,30 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2749
2934
|
)
|
|
2750
2935
|
})
|
|
2751
2936
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2752
|
-
React.createElement('b', {}, '时间线'),
|
|
2937
|
+
React.createElement('b', {}, t('时间线')),
|
|
2753
2938
|
React.createElement('select', {
|
|
2754
2939
|
className: 'iterate-filter-select',
|
|
2755
2940
|
value: timelineType,
|
|
2756
|
-
'aria-label': '按时间线类型筛选',
|
|
2941
|
+
'aria-label': t('按时间线类型筛选'),
|
|
2757
2942
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineType(e.target.value),
|
|
2758
2943
|
},
|
|
2759
|
-
React.createElement('option', { value: '' }, '全部类型'),
|
|
2944
|
+
React.createElement('option', { value: '' }, t('全部类型')),
|
|
2760
2945
|
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
2761
2946
|
),
|
|
2762
2947
|
React.createElement('select', {
|
|
2763
2948
|
className: 'iterate-filter-select',
|
|
2764
2949
|
value: timelineRound,
|
|
2765
|
-
'aria-label': '按轮次筛选',
|
|
2950
|
+
'aria-label': t('按轮次筛选'),
|
|
2766
2951
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineRound(e.target.value),
|
|
2767
2952
|
},
|
|
2768
|
-
React.createElement('option', { value: '' }, '全部轮次'),
|
|
2953
|
+
React.createElement('option', { value: '' }, t('全部轮次')),
|
|
2769
2954
|
...rounds.map((r) => React.createElement('option', { key: r, value: r }, `Round ${r}`)),
|
|
2770
2955
|
),
|
|
2771
2956
|
React.createElement('input', {
|
|
2772
2957
|
className: 'iterate-filter-search',
|
|
2773
2958
|
type: 'search',
|
|
2774
|
-
placeholder: '搜索时间线…',
|
|
2775
|
-
'aria-label': '搜索时间线',
|
|
2959
|
+
placeholder: t('搜索时间线…'),
|
|
2960
|
+
'aria-label': t('搜索时间线'),
|
|
2776
2961
|
value: timelineSearch,
|
|
2777
2962
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setTimelineSearch(e.target.value),
|
|
2778
2963
|
}),
|
|
@@ -2780,15 +2965,15 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2780
2965
|
? React.createElement('button', {
|
|
2781
2966
|
className: 'iterate-filter-clear',
|
|
2782
2967
|
onClick: clearTimelineFilter,
|
|
2783
|
-
title: '清除全部筛选',
|
|
2784
|
-
}, '清除')
|
|
2968
|
+
title: t('清除全部筛选'),
|
|
2969
|
+
}, t('清除'))
|
|
2785
2970
|
: null,
|
|
2786
2971
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${entries.length}`),
|
|
2787
2972
|
)
|
|
2788
2973
|
if (rows.length === 0) {
|
|
2789
2974
|
return React.createElement('div', {},
|
|
2790
2975
|
filterBar,
|
|
2791
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的时间线条目'),
|
|
2976
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的时间线条目')),
|
|
2792
2977
|
)
|
|
2793
2978
|
}
|
|
2794
2979
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
@@ -2815,24 +3000,24 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2815
3000
|
const pulled = toolCalledInSession(session, 'iterate_quality_gate')
|
|
2816
3001
|
|
|
2817
3002
|
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2818
|
-
React.createElement('b', {}, '质量门禁'),
|
|
3003
|
+
React.createElement('b', {}, t('质量门禁')),
|
|
2819
3004
|
React.createElement('button', {
|
|
2820
3005
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2821
3006
|
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2822
|
-
title: '复制 iterate_quality_gate 查询指令(含 JSON 参数)',
|
|
2823
|
-
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
3007
|
+
title: t('复制 iterate_quality_gate 查询指令(含 JSON 参数)'),
|
|
3008
|
+
}, copiedKey === 'qgate' ? t('已复制') : t('查询门禁')),
|
|
2824
3009
|
React.createElement('button', {
|
|
2825
3010
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
|
|
2826
3011
|
onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
|
|
2827
|
-
title: '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)',
|
|
2828
|
-
}, copiedKey === 'qgate-clear' ? '已复制' : '清除门禁'),
|
|
3012
|
+
title: t('复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)'),
|
|
3013
|
+
}, copiedKey === 'qgate-clear' ? t('已复制') : t('清除门禁')),
|
|
2829
3014
|
// Gap #4: the browser cannot read `.iterate/` directly — one click
|
|
2830
3015
|
// copies the pull instruction that makes the model echo it into the stream.
|
|
2831
3016
|
React.createElement('button', {
|
|
2832
3017
|
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot' ? '' : undefined,
|
|
2833
3018
|
onClick: () => copyInstruction('disk-snapshot', pullInstruction),
|
|
2834
|
-
title: '复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript',
|
|
2835
|
-
}, copiedKey === 'disk-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3019
|
+
title: t('复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript'),
|
|
3020
|
+
}, copiedKey === 'disk-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2836
3021
|
)
|
|
2837
3022
|
|
|
2838
3023
|
if (!hasGate) {
|
|
@@ -2840,29 +3025,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2840
3025
|
headerBar,
|
|
2841
3026
|
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2842
3027
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2843
|
-
React.createElement('span', {}, '质量门禁视图'),
|
|
3028
|
+
React.createElement('span', {}, t('质量门禁视图')),
|
|
2844
3029
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2845
3030
|
),
|
|
2846
3031
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2847
3032
|
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
2848
3033
|
// Gap #4: distinguish "we pulled it and it is empty" from "we
|
|
2849
3034
|
// never asked the model to read the disk".
|
|
2850
|
-
diskEmptyStateText(pulled, '质量门禁证书'),
|
|
3035
|
+
diskEmptyStateText(pulled, t('质量门禁证书'), getLang()),
|
|
2851
3036
|
),
|
|
2852
3037
|
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
2853
|
-
'质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。',
|
|
3038
|
+
t('质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
|
|
2854
3039
|
),
|
|
2855
3040
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2856
3041
|
React.createElement('button', {
|
|
2857
3042
|
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2858
3043
|
onClick: () => copyInstruction('qgate-instr', gateInstruction),
|
|
2859
|
-
title: '复制查询指令',
|
|
2860
|
-
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
3044
|
+
title: t('复制查询指令'),
|
|
3045
|
+
}, copiedKey === 'qgate-instr' ? t('已复制') : t('复制查询指令')),
|
|
2861
3046
|
React.createElement('button', {
|
|
2862
3047
|
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot-2' ? '' : undefined,
|
|
2863
3048
|
onClick: () => copyInstruction('disk-snapshot-2', pullInstruction),
|
|
2864
|
-
title: '复制拉取磁盘快照指令',
|
|
2865
|
-
}, copiedKey === 'disk-snapshot-2' ? '已复制' : '拉取磁盘快照'),
|
|
3049
|
+
title: t('复制拉取磁盘快照指令'),
|
|
3050
|
+
}, copiedKey === 'disk-snapshot-2' ? t('已复制') : t('拉取磁盘快照')),
|
|
2866
3051
|
),
|
|
2867
3052
|
),
|
|
2868
3053
|
),
|
|
@@ -2875,7 +3060,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2875
3060
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2876
3061
|
React.createElement('span', { className: 'iterate-gate', 'data-status': status }, statusLabel),
|
|
2877
3062
|
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2878
|
-
`整体评分
|
|
3063
|
+
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) }),
|
|
2879
3064
|
),
|
|
2880
3065
|
),
|
|
2881
3066
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2883,7 +3068,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2883
3068
|
? React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6 } },
|
|
2884
3069
|
...dims.map((d, i) =>
|
|
2885
3070
|
React.createElement('span', { key: `gdim-${i}`, className: 'iterate-obs-chip' },
|
|
2886
|
-
|
|
3071
|
+
tf(`{a0} · 收敛 {a1}% · 评分 {a2}`, { a0: String(d.dimension || '?'), a1: num(d.convergenceRate), a2: num(d.score) }),
|
|
2887
3072
|
),
|
|
2888
3073
|
),
|
|
2889
3074
|
)
|
|
@@ -2900,12 +3085,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2900
3085
|
style: { width: `${fill}%` },
|
|
2901
3086
|
}),
|
|
2902
3087
|
),
|
|
2903
|
-
React.createElement('span', {}, `评分
|
|
3088
|
+
React.createElement('span', {}, tf(`评分 {a0} · 发现 {a1} · 已修复 {a2}`, { a0: num(d.score), a1: num(d.findingsCount), a2: num(d.fixedCount) })),
|
|
2904
3089
|
)
|
|
2905
3090
|
}),
|
|
2906
3091
|
),
|
|
2907
3092
|
gate && gate.failReason
|
|
2908
|
-
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3093
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } }, tf(`失败原因:{a0}`, { a0: String(gate.failReason) }))
|
|
2909
3094
|
: null,
|
|
2910
3095
|
),
|
|
2911
3096
|
),
|
|
@@ -2918,7 +3103,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2918
3103
|
// (list all / search / adopt one entry).
|
|
2919
3104
|
const renderExperienceBank = () => {
|
|
2920
3105
|
const expInstruction = expSearch
|
|
2921
|
-
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n
|
|
3106
|
+
? tf(`请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'search', query: expSearch }, null, 2) })
|
|
2922
3107
|
: buildExperienceListInstruction()
|
|
2923
3108
|
|
|
2924
3109
|
const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
|
|
@@ -2933,12 +3118,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2933
3118
|
const pulled = toolCalledInSession(session, 'iterate_experience')
|
|
2934
3119
|
|
|
2935
3120
|
const resultRows = visible.map((e, i) => {
|
|
2936
|
-
const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n
|
|
3121
|
+
const adoptInstruction = tf(`请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'get', id: e.id }, null, 2) })
|
|
2937
3122
|
return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-block' },
|
|
2938
3123
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2939
3124
|
React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
|
|
2940
3125
|
React.createElement('span', {}, String(e.pattern || '?')),
|
|
2941
|
-
React.createElement('span', { className: 'iterate-exp-hit' }, `命中
|
|
3126
|
+
React.createElement('span', { className: 'iterate-exp-hit' }, tf(`命中 {a0} 次`, { a0: e.hitCount ?? 0 })),
|
|
2942
3127
|
),
|
|
2943
3128
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2944
3129
|
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
@@ -2947,8 +3132,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2947
3132
|
? React.createElement('button', {
|
|
2948
3133
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `exp-adopt-${i}` ? '' : undefined,
|
|
2949
3134
|
onClick: () => copyInstruction(`exp-adopt-${i}`, adoptInstruction),
|
|
2950
|
-
title: '复制采纳指令(查询该经验并应用到当前上下文)',
|
|
2951
|
-
}, copiedKey === `exp-adopt-${i}` ? '已复制' : '采纳')
|
|
3135
|
+
title: t('复制采纳指令(查询该经验并应用到当前上下文)'),
|
|
3136
|
+
}, copiedKey === `exp-adopt-${i}` ? t('已复制') : t('采纳'))
|
|
2952
3137
|
: null,
|
|
2953
3138
|
),
|
|
2954
3139
|
),
|
|
@@ -2957,25 +3142,25 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2957
3142
|
|
|
2958
3143
|
return React.createElement('div', {},
|
|
2959
3144
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2960
|
-
React.createElement('b', {}, '经验银行'),
|
|
2961
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内
|
|
3145
|
+
React.createElement('b', {}, t('经验银行')),
|
|
3146
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`会话内 {a0} 条`, { a0: expEntries.length })),
|
|
2962
3147
|
React.createElement('button', {
|
|
2963
3148
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2964
3149
|
onClick: () => copyInstruction('exp-list', buildExperienceListInstruction()),
|
|
2965
|
-
title: '复制列出经验指令(含 JSON 参数)',
|
|
2966
|
-
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
3150
|
+
title: t('复制列出经验指令(含 JSON 参数)'),
|
|
3151
|
+
}, copiedKey === 'exp-list' ? t('已复制') : t('列出经验')),
|
|
2967
3152
|
React.createElement('button', {
|
|
2968
3153
|
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-snapshot' ? '' : undefined,
|
|
2969
3154
|
onClick: () => copyInstruction('exp-snapshot', pullInstruction),
|
|
2970
|
-
title: '复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话',
|
|
2971
|
-
}, copiedKey === 'exp-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3155
|
+
title: t('复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话'),
|
|
3156
|
+
}, copiedKey === 'exp-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2972
3157
|
),
|
|
2973
3158
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2974
3159
|
React.createElement('input', {
|
|
2975
3160
|
className: 'iterate-filter-search',
|
|
2976
3161
|
type: 'search',
|
|
2977
|
-
placeholder: '搜索经验模式…',
|
|
2978
|
-
'aria-label': '搜索经验',
|
|
3162
|
+
placeholder: t('搜索经验模式…'),
|
|
3163
|
+
'aria-label': t('搜索经验'),
|
|
2979
3164
|
value: expSearch,
|
|
2980
3165
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
|
|
2981
3166
|
}),
|
|
@@ -2983,17 +3168,17 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2983
3168
|
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
|
|
2984
3169
|
disabled: !expSearch,
|
|
2985
3170
|
onClick: () => copyInstruction('exp-search', expInstruction),
|
|
2986
|
-
title: '复制搜索指令',
|
|
2987
|
-
}, copiedKey === 'exp-search' ? '已复制' : '搜索'),
|
|
3171
|
+
title: t('复制搜索指令'),
|
|
3172
|
+
}, copiedKey === 'exp-search' ? t('已复制') : t('搜索')),
|
|
2988
3173
|
),
|
|
2989
3174
|
resultRows.length > 0
|
|
2990
3175
|
? React.createElement('div', {}, ...resultRows)
|
|
2991
3176
|
: expEntries.length > 0
|
|
2992
|
-
? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
|
|
3177
|
+
? React.createElement('div', { className: 'iterate-obs-empty' }, t('没有匹配「') + expSearch + t('」的经验'))
|
|
2993
3178
|
: React.createElement('div', {},
|
|
2994
3179
|
// Gap #4: separate "pulled and empty" from "never pulled".
|
|
2995
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, diskEmptyStateText(pulled, '经验银行')),
|
|
2996
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '也可点「列出经验」查询磁盘上的全部历史经验。'),
|
|
3180
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, diskEmptyStateText(pulled, t('经验银行'), getLang())),
|
|
3181
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('也可点「列出经验」查询磁盘上的全部历史经验。')),
|
|
2997
3182
|
),
|
|
2998
3183
|
)
|
|
2999
3184
|
}
|
|
@@ -3004,21 +3189,21 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3004
3189
|
// falsifications); copy buttons reach the back end for the full history.
|
|
3005
3190
|
const renderDefenseEvents = () => {
|
|
3006
3191
|
const defenseInstruction = defenseFilter
|
|
3007
|
-
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n
|
|
3192
|
+
? tf(`请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2) })
|
|
3008
3193
|
: buildDefenseEventsListInstruction()
|
|
3009
3194
|
|
|
3010
3195
|
const typeOptions = [
|
|
3011
|
-
{ value: '', label: '全部类型' },
|
|
3012
|
-
{ value: 'precondition_failed', label: '前置校验失败' },
|
|
3013
|
-
{ value: 'rollback', label: '回滚' },
|
|
3014
|
-
{ value: 'invariant_violated', label: '不变量违反' },
|
|
3015
|
-
{ value: 'assumption_falsified', label: '假设被证伪' },
|
|
3196
|
+
{ value: '', label: t('全部类型') },
|
|
3197
|
+
{ value: 'precondition_failed', label: t('前置校验失败') },
|
|
3198
|
+
{ value: 'rollback', label: t('回滚') },
|
|
3199
|
+
{ value: 'invariant_violated', label: t('不变量违反') },
|
|
3200
|
+
{ value: 'assumption_falsified', label: t('假设被证伪') },
|
|
3016
3201
|
]
|
|
3017
3202
|
const TYPE_LABEL: Record<string, string> = {
|
|
3018
|
-
precondition_failed: '前置校验失败',
|
|
3019
|
-
rollback: '回滚',
|
|
3020
|
-
invariant_violated: '不变量违反',
|
|
3021
|
-
assumption_falsified: '假设被证伪',
|
|
3203
|
+
precondition_failed: t('前置校验失败'),
|
|
3204
|
+
rollback: t('回滚'),
|
|
3205
|
+
invariant_violated: t('不变量违反'),
|
|
3206
|
+
assumption_falsified: t('假设被证伪'),
|
|
3022
3207
|
}
|
|
3023
3208
|
|
|
3024
3209
|
const counts = (defenseEvents && defenseEvents.counts) || {}
|
|
@@ -3029,28 +3214,28 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3029
3214
|
: allEvents
|
|
3030
3215
|
|
|
3031
3216
|
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
3032
|
-
React.createElement('b', {}, '防御事件'),
|
|
3033
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内
|
|
3217
|
+
React.createElement('b', {}, t('防御事件')),
|
|
3218
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`会话内 {a0} 条 / 累计 {a1}`, { a0: allEvents.length, a1: totalCount })),
|
|
3034
3219
|
React.createElement('button', {
|
|
3035
3220
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
3036
3221
|
onClick: () => copyInstruction('defense-list', buildDefenseEventsListInstruction()),
|
|
3037
|
-
title: '复制列出防御事件指令(含 JSON 参数)',
|
|
3038
|
-
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
3222
|
+
title: t('复制列出防御事件指令(含 JSON 参数)'),
|
|
3223
|
+
}, copiedKey === 'defense-list' ? t('已复制') : t('列出事件')),
|
|
3039
3224
|
React.createElement('button', {
|
|
3040
3225
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-snapshot' ? '' : undefined,
|
|
3041
3226
|
onClick: () => copyInstruction('defense-snapshot', pullInstruction),
|
|
3042
|
-
title: '复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话',
|
|
3043
|
-
}, copiedKey === 'defense-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3227
|
+
title: t('复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话'),
|
|
3228
|
+
}, copiedKey === 'defense-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
3044
3229
|
React.createElement('button', {
|
|
3045
3230
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
|
|
3046
|
-
onClick: () => copyInstruction('defense-counts', `请调用 \`iterate_defense_events\` 查询事件统计:\n\n\`\`\`json\n
|
|
3047
|
-
title: '复制统计指令(含 JSON 参数)',
|
|
3048
|
-
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
3231
|
+
onClick: () => copyInstruction('defense-counts', tf(`请调用 \`iterate_defense_events\` 查询事件统计:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'counts' }, null, 2) })),
|
|
3232
|
+
title: t('复制统计指令(含 JSON 参数)'),
|
|
3233
|
+
}, copiedKey === 'defense-counts' ? t('已复制') : t('统计')),
|
|
3049
3234
|
React.createElement('button', {
|
|
3050
3235
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'defense-clear' ? '' : undefined,
|
|
3051
|
-
onClick: () => copyInstruction('defense-clear', `请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n
|
|
3052
|
-
title: '复制 iterate_defense_events clear 指令文本(重置陈旧事件流)',
|
|
3053
|
-
}, copiedKey === 'defense-clear' ? '已复制' : '清除事件'),
|
|
3236
|
+
onClick: () => copyInstruction('defense-clear', tf(`请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'clear' }, null, 2) })),
|
|
3237
|
+
title: t('复制 iterate_defense_events clear 指令文本(重置陈旧事件流)'),
|
|
3238
|
+
}, copiedKey === 'defense-clear' ? t('已复制') : t('清除事件')),
|
|
3054
3239
|
)
|
|
3055
3240
|
|
|
3056
3241
|
if (allEvents.length === 0 && totalCount === 0) {
|
|
@@ -3058,22 +3243,22 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3058
3243
|
headerBar,
|
|
3059
3244
|
React.createElement('div', { className: 'iterate-obs-block' },
|
|
3060
3245
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3061
|
-
React.createElement('span', {}, '防御事件流'),
|
|
3246
|
+
React.createElement('span', {}, t('防御事件流')),
|
|
3062
3247
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
3063
3248
|
),
|
|
3064
3249
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3065
3250
|
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3066
|
-
diskEmptyStateText(toolCalledInSession(session, 'iterate_defense_events'), '防御事件流'),
|
|
3251
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_defense_events'), t('防御事件流'), getLang()),
|
|
3067
3252
|
),
|
|
3068
3253
|
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3069
|
-
'防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。',
|
|
3254
|
+
t('防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
|
|
3070
3255
|
),
|
|
3071
3256
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
3072
3257
|
React.createElement('button', {
|
|
3073
3258
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
3074
3259
|
onClick: () => copyInstruction('defense-instr', defenseInstruction),
|
|
3075
|
-
title: '复制查询指令',
|
|
3076
|
-
}, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
|
|
3260
|
+
title: t('复制查询指令'),
|
|
3261
|
+
}, copiedKey === 'defense-instr' ? t('已复制') : t('复制查询指令')),
|
|
3077
3262
|
),
|
|
3078
3263
|
),
|
|
3079
3264
|
),
|
|
@@ -3084,7 +3269,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3084
3269
|
React.createElement('select', {
|
|
3085
3270
|
className: 'iterate-filter-select',
|
|
3086
3271
|
value: defenseFilter,
|
|
3087
|
-
'aria-label': '按事件类型筛选',
|
|
3272
|
+
'aria-label': t('按事件类型筛选'),
|
|
3088
3273
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
|
|
3089
3274
|
},
|
|
3090
3275
|
...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
|
|
@@ -3093,8 +3278,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3093
3278
|
? React.createElement('button', {
|
|
3094
3279
|
className: 'iterate-filter-clear',
|
|
3095
3280
|
onClick: () => setDefenseFilter(''),
|
|
3096
|
-
title: '清除类型筛选',
|
|
3097
|
-
}, '清除')
|
|
3281
|
+
title: t('清除类型筛选'),
|
|
3282
|
+
}, t('清除'))
|
|
3098
3283
|
: null,
|
|
3099
3284
|
)
|
|
3100
3285
|
|
|
@@ -3114,7 +3299,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3114
3299
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3115
3300
|
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
3116
3301
|
e.defense
|
|
3117
|
-
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } },
|
|
3302
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } }, tf(`防线:{a0}`, { a0: String(e.defense) }))
|
|
3118
3303
|
: null,
|
|
3119
3304
|
),
|
|
3120
3305
|
)
|
|
@@ -3133,7 +3318,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3133
3318
|
),
|
|
3134
3319
|
eventRows.length > 0
|
|
3135
3320
|
? React.createElement('div', {}, ...eventRows)
|
|
3136
|
-
: React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配该类型的防御事件'),
|
|
3321
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, t('没有匹配该类型的防御事件')),
|
|
3137
3322
|
)
|
|
3138
3323
|
}
|
|
3139
3324
|
|
|
@@ -3148,25 +3333,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3148
3333
|
const hasFindings = (manifest.findings || []).length > 0
|
|
3149
3334
|
if (hasThreads || hasFindings) {
|
|
3150
3335
|
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
3151
|
-
React.createElement('div', {}, '实时活动流仅在运行期间记录,当前已完成。'),
|
|
3336
|
+
React.createElement('div', {}, t('实时活动流仅在运行期间记录,当前已完成。')),
|
|
3152
3337
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
3153
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f1') }, '查看审查线程'),
|
|
3154
|
-
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f3') }, '查看发现'),
|
|
3338
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f1') }, t('查看审查线程')),
|
|
3339
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f3') }, t('查看发现')),
|
|
3155
3340
|
),
|
|
3156
3341
|
)
|
|
3157
3342
|
}
|
|
3158
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无实时活动')
|
|
3343
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无实时活动'))
|
|
3159
3344
|
}
|
|
3160
3345
|
const types = Array.from(new Set(liveEntries.map((e) => String(e.type || 'unknown')).filter(Boolean))).sort()
|
|
3161
3346
|
const filtered = filterLiveEntries(liveEntries, liveType)
|
|
3162
3347
|
const rows = filtered.map((e, i) => {
|
|
3163
3348
|
const type = String(e.type || 'unknown')
|
|
3164
|
-
|
|
3349
|
+
// Module constants carry BOTH languages; resolve at render time so a
|
|
3350
|
+
// language switch repaints the badges.
|
|
3351
|
+
const meta = OBS_LIVE_META[type]
|
|
3352
|
+
const typeLabel = meta ? meta.label[getLang()] : String(e.type || '?')
|
|
3353
|
+
const typeColor = meta ? meta.color : OBS_LIVE_FALLBACK_COLOR
|
|
3165
3354
|
const tool = String(e.tool || '')
|
|
3166
3355
|
const target = String(e.target || '')
|
|
3167
3356
|
const time = formatObsLiveTime(String(e.ts || ''))
|
|
3168
3357
|
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
3169
|
-
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color:
|
|
3358
|
+
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: typeColor } }, typeLabel),
|
|
3170
3359
|
tool
|
|
3171
3360
|
? React.createElement('span', { className: 'iterate-obs-chip' }, tool)
|
|
3172
3361
|
: null,
|
|
@@ -3175,29 +3364,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3175
3364
|
)
|
|
3176
3365
|
})
|
|
3177
3366
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
3178
|
-
React.createElement('b', {}, '实时活动'),
|
|
3367
|
+
React.createElement('b', {}, t('实时活动')),
|
|
3179
3368
|
React.createElement('select', {
|
|
3180
3369
|
className: 'iterate-filter-select',
|
|
3181
3370
|
value: liveType,
|
|
3182
|
-
'aria-label': '按活动类型筛选',
|
|
3371
|
+
'aria-label': t('按活动类型筛选'),
|
|
3183
3372
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setLiveType(e.target.value),
|
|
3184
3373
|
},
|
|
3185
|
-
React.createElement('option', { value: '' }, '全部活动'),
|
|
3374
|
+
React.createElement('option', { value: '' }, t('全部活动')),
|
|
3186
3375
|
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
3187
3376
|
),
|
|
3188
3377
|
liveType
|
|
3189
3378
|
? React.createElement('button', {
|
|
3190
3379
|
className: 'iterate-filter-clear',
|
|
3191
3380
|
onClick: () => setLiveType(''),
|
|
3192
|
-
title: '清除类型筛选',
|
|
3193
|
-
}, '清除')
|
|
3381
|
+
title: t('清除类型筛选'),
|
|
3382
|
+
}, t('清除'))
|
|
3194
3383
|
: null,
|
|
3195
3384
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${liveEntries.length}`),
|
|
3196
3385
|
)
|
|
3197
3386
|
if (rows.length === 0) {
|
|
3198
3387
|
return React.createElement('div', {},
|
|
3199
3388
|
filterBar,
|
|
3200
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的实时活动'),
|
|
3389
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的实时活动')),
|
|
3201
3390
|
)
|
|
3202
3391
|
}
|
|
3203
3392
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
@@ -3223,30 +3412,30 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3223
3412
|
|
|
3224
3413
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
3225
3414
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': open ? undefined : '', onClick: () => setOpen((v) => !v) },
|
|
3226
|
-
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
3415
|
+
React.createElement('span', { className: 'iterate-obs-title' }, t('iterate 观测台')),
|
|
3227
3416
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
3228
3417
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, headMeta || 'runtime'),
|
|
3229
3418
|
// Gap #14: make the tab system discoverable without expanding it.
|
|
3230
3419
|
React.createElement('span', {
|
|
3231
3420
|
className: 'iterate-obs-head-meta',
|
|
3232
|
-
title:
|
|
3233
|
-
},
|
|
3421
|
+
title: obsHotkeyHint(),
|
|
3422
|
+
}, obsHotkeyHint()),
|
|
3234
3423
|
React.createElement('button', {
|
|
3235
3424
|
className: 'iterate-btn', 'data-ghost': '',
|
|
3236
3425
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setOpen((v) => !v) },
|
|
3237
|
-
}, open ? '收起' : '展开'),
|
|
3426
|
+
}, open ? t('收起') : t('展开')),
|
|
3238
3427
|
),
|
|
3239
3428
|
open
|
|
3240
3429
|
? React.createElement('div', {},
|
|
3241
3430
|
copyFailText
|
|
3242
3431
|
? React.createElement('div', { className: 'iterate-obs-block' },
|
|
3243
3432
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3244
|
-
React.createElement('b', {}, '复制未成功'),
|
|
3245
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, '剪贴板写入被拒绝,请手动选中下方指令复制'),
|
|
3433
|
+
React.createElement('b', {}, t('复制未成功')),
|
|
3434
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, t('剪贴板写入被拒绝,请手动选中下方指令复制')),
|
|
3246
3435
|
React.createElement('button', {
|
|
3247
|
-
className: 'iterate-btn', 'data-ghost': '', 'aria-label': '关闭复制失败提示',
|
|
3436
|
+
className: 'iterate-btn', 'data-ghost': '', 'aria-label': t('关闭复制失败提示'),
|
|
3248
3437
|
onClick: () => setCopyFailText(null),
|
|
3249
|
-
}, '关闭'),
|
|
3438
|
+
}, t('关闭')),
|
|
3250
3439
|
),
|
|
3251
3440
|
React.createElement('div', { className: 'iterate-obs-code' }, copyFailText),
|
|
3252
3441
|
)
|
|
@@ -3258,8 +3447,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3258
3447
|
React.createElement('button', {
|
|
3259
3448
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'export' ? '' : undefined,
|
|
3260
3449
|
onClick: exportObservatory,
|
|
3261
|
-
title: '将观测台全部数据导出为 JSON(优先下载,失败则复制)',
|
|
3262
|
-
}, copiedKey === 'export' ? '已导出' : '导出 JSON'),
|
|
3450
|
+
title: t('将观测台全部数据导出为 JSON(优先下载,失败则复制)'),
|
|
3451
|
+
}, copiedKey === 'export' ? t('已导出') : t('导出 JSON')),
|
|
3263
3452
|
// Gap #4: `.iterate/` artifacts are unreachable from the browser;
|
|
3264
3453
|
// this copies the one instruction that makes the model echo them
|
|
3265
3454
|
// into the session stream (which is what every panel scans).
|
|
@@ -3267,9 +3456,9 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3267
3456
|
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
3268
3457
|
onClick: () => copyInstruction('pull-snapshot', pullInstruction),
|
|
3269
3458
|
title: pullTitle,
|
|
3270
|
-
}, copiedKey === 'pull-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3459
|
+
}, copiedKey === 'pull-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
3271
3460
|
),
|
|
3272
|
-
React.createElement('div', { className: 'iterate-obs-tabs', role: 'tablist', 'aria-label': '观测台页签(快捷键 1-9 / 0 / -)' },
|
|
3461
|
+
React.createElement('div', { className: 'iterate-obs-tabs', role: 'tablist', 'aria-label': t('观测台页签(快捷键 1-9 / 0 / -)') },
|
|
3273
3462
|
...OBS_TABS.map((t) =>
|
|
3274
3463
|
React.createElement('button', {
|
|
3275
3464
|
key: t.key,
|
|
@@ -3278,7 +3467,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3278
3467
|
'aria-selected': tab === t.key,
|
|
3279
3468
|
'data-active': tab === t.key ? '' : undefined,
|
|
3280
3469
|
onClick: () => setTab(t.key),
|
|
3281
|
-
title: t.hotkey ?
|
|
3470
|
+
title: t.hotkey ? tf(`{a0}(按 {a1} 切换)`, { a0: obsTabLabel(t), a1: t.hotkey }) : tf(`{a0}(按 - 切换)`, { a0: obsTabLabel(t) }),
|
|
3282
3471
|
}, obsTabLabel(t)),
|
|
3283
3472
|
),
|
|
3284
3473
|
),
|
|
@@ -3301,6 +3490,17 @@ function selectTurnTail(owner: SlotProps): { matched: boolean } | null {
|
|
|
3301
3490
|
return null
|
|
3302
3491
|
}
|
|
3303
3492
|
|
|
3493
|
+
/**
|
|
3494
|
+
* Slot components wrapped once at module scope so the wrapper identity is
|
|
3495
|
+
* stable — creating the wrapper inside the render callback would remount the
|
|
3496
|
+
* subtree (and wipe triage verdicts, expanded tabs …) on every render.
|
|
3497
|
+
*/
|
|
3498
|
+
const LangDashboard = withLang(ConvergenceDashboard)
|
|
3499
|
+
const LangObservatory = withLang(ObservatoryPanel)
|
|
3500
|
+
const LangTurnTail = withLang(TurnTailEntry)
|
|
3501
|
+
const LangProgress = withLang(ProgressCapsule)
|
|
3502
|
+
const LangSettings = withLang(SettingsPanel)
|
|
3503
|
+
|
|
3304
3504
|
/**
|
|
3305
3505
|
* Plugin client entry. Called by the dsh client runtime with its context.
|
|
3306
3506
|
*/
|
|
@@ -3310,6 +3510,15 @@ export function apply(ctx: ClientContext): void {
|
|
|
3310
3510
|
themeSvc = readTheme(ctx)
|
|
3311
3511
|
storage = createStorage()
|
|
3312
3512
|
|
|
3513
|
+
// 1b. Language: an explicit stored choice wins, then the browser locale.
|
|
3514
|
+
// Deliberately NOT persisted here (see setLang's `persist` flag) — an
|
|
3515
|
+
// auto-detected language must not shadow a choice the user never made.
|
|
3516
|
+
configureLangStorage(storage)
|
|
3517
|
+
setLang(
|
|
3518
|
+
detectLang(storage.get(LANG_STORAGE_KEY), typeof navigator !== 'undefined' ? navigator.language : ''),
|
|
3519
|
+
false,
|
|
3520
|
+
)
|
|
3521
|
+
|
|
3313
3522
|
// 2. Theme preference bootstrap.
|
|
3314
3523
|
const savedTheme = storage.get(THEME_STORAGE_KEY)
|
|
3315
3524
|
themeEnabled = savedTheme === null ? true : savedTheme === '1'
|
|
@@ -3344,7 +3553,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3344
3553
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
3345
3554
|
slotsSvc?.register(
|
|
3346
3555
|
{ name: 'conversation.input.dock', id: 'iterate-dashboard', order: 90 },
|
|
3347
|
-
(props) => React.createElement(
|
|
3556
|
+
(props) => React.createElement(LangDashboard, props),
|
|
3348
3557
|
),
|
|
3349
3558
|
)
|
|
3350
3559
|
|
|
@@ -3353,7 +3562,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3353
3562
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
3354
3563
|
slotsSvc?.register(
|
|
3355
3564
|
{ name: 'conversation.input.dock', id: 'iterate-observatory', order: 91 },
|
|
3356
|
-
(props) => React.createElement(
|
|
3565
|
+
(props) => React.createElement(LangObservatory, props),
|
|
3357
3566
|
),
|
|
3358
3567
|
)
|
|
3359
3568
|
|
|
@@ -3361,7 +3570,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3361
3570
|
slotsSvc.inject('conversation.chat.turnTail', () =>
|
|
3362
3571
|
slotsSvc?.register(
|
|
3363
3572
|
{ name: 'conversation.chat.turnTail', id: 'iterate-turn-tail', select: selectTurnTail },
|
|
3364
|
-
(props) => React.createElement(
|
|
3573
|
+
(props) => React.createElement(LangTurnTail, props),
|
|
3365
3574
|
),
|
|
3366
3575
|
)
|
|
3367
3576
|
|
|
@@ -3369,7 +3578,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3369
3578
|
slotsSvc.inject('shell.overlay', () =>
|
|
3370
3579
|
slotsSvc?.register(
|
|
3371
3580
|
{ name: 'shell.overlay', id: 'iterate-progress', order: 0 },
|
|
3372
|
-
(props) => React.createElement(
|
|
3581
|
+
(props) => React.createElement(LangProgress, props),
|
|
3373
3582
|
),
|
|
3374
3583
|
)
|
|
3375
3584
|
|
|
@@ -3377,7 +3586,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3377
3586
|
slotsSvc.inject('settings.section', () =>
|
|
3378
3587
|
slotsSvc?.register(
|
|
3379
3588
|
{ name: 'settings.section', id: 'iterate-settings', order: 30, label: () => 'iterate' },
|
|
3380
|
-
(props) => React.createElement(
|
|
3589
|
+
(props) => React.createElement(LangSettings, props),
|
|
3381
3590
|
),
|
|
3382
3591
|
)
|
|
3383
3592
|
}
|