iterate-plugin 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/README.zh-CN.md +29 -5
- package/dist/config-loader.js +7 -1
- package/dist/file-lock.js +40 -12
- package/dist/index.js +7 -0
- package/dist/live.js +2 -1
- package/dist/session-hooks.js +6 -1
- package/dist/tools/decision-log.js +4 -10
- package/dist/working-directory.js +114 -0
- package/lib/client.js +1097 -461
- package/lib/parse.js +253 -58
- package/package.json +16 -11
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +531 -340
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +7 -1
- package/src/file-lock.ts +36 -9
- package/src/index.ts +7 -0
- package/src/live.ts +2 -1
- package/src/session-hooks.ts +6 -1
- package/src/tools/decision-log.ts +4 -6
- 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,6 +1046,12 @@ 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
|
|
|
987
1057
|
React.useEffect(() => {
|
|
@@ -1003,11 +1073,11 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1003
1073
|
return React.createElement(
|
|
1004
1074
|
'div',
|
|
1005
1075
|
{ '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': '' }, '运行中'),
|
|
1076
|
+
React.createElement('span', { className: 'iterate-round-badge', 'data-live': '' }, t('运行中')),
|
|
1007
1077
|
React.createElement(
|
|
1008
1078
|
'span',
|
|
1009
1079
|
{ className: 'iterate-empty-hint' },
|
|
1010
|
-
`第
|
|
1080
|
+
tf(`第 {a0} 轮进行中{a1}——首轮汇总报告落地后,这里会显示收敛进度与发现统计。`, { a0: String(runState.round), a1: runState.phase ? tf(` · 阶段「{a0}」`, { a0: runState.phase }) : '' }),
|
|
1011
1081
|
),
|
|
1012
1082
|
// No start button while a run is active.
|
|
1013
1083
|
)
|
|
@@ -1016,11 +1086,11 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1016
1086
|
return React.createElement(
|
|
1017
1087
|
'div',
|
|
1018
1088
|
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'done', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1019
|
-
React.createElement('span', { className: 'iterate-round-badge' }, '已结束'),
|
|
1089
|
+
React.createElement('span', { className: 'iterate-round-badge' }, t('已结束')),
|
|
1020
1090
|
React.createElement(
|
|
1021
1091
|
'span',
|
|
1022
1092
|
{ className: 'iterate-empty-hint' },
|
|
1023
|
-
`上次迭代在第
|
|
1093
|
+
tf(`上次迭代在第 {a0} 轮后停止{a1},但没有可展示的汇总报告。`, { a0: String(runState.round), a1: runState.stoppedReason ? parenthesize(stoppedReasonLabel(runState.stoppedReason, getLang())) : '' }),
|
|
1024
1094
|
),
|
|
1025
1095
|
React.createElement(StartIterationButton, null),
|
|
1026
1096
|
)
|
|
@@ -1033,7 +1103,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1033
1103
|
'div',
|
|
1034
1104
|
{ 'data-iterate-root': '', 'data-iterate': 'dashboard', 'data-run-state': 'empty', className: 'iterate-dashboard iterate-dashboard-empty' },
|
|
1035
1105
|
React.createElement('span', { className: 'iterate-round-badge' }, 'iterate'),
|
|
1036
|
-
React.createElement('span', { className: 'iterate-empty-hint' }, '运行一次评审后,这里会显示收敛进度与发现统计。'),
|
|
1106
|
+
React.createElement('span', { className: 'iterate-empty-hint' }, t('运行一次评审后,这里会显示收敛进度与发现统计。')),
|
|
1037
1107
|
React.createElement(StartIterationButton, null),
|
|
1038
1108
|
)
|
|
1039
1109
|
}
|
|
@@ -1053,15 +1123,15 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1053
1123
|
? React.createElement('span', {
|
|
1054
1124
|
className: 'iterate-chip-resume',
|
|
1055
1125
|
key: 'resume',
|
|
1056
|
-
title: '本次迭代从上一次中断的断点继续执行',
|
|
1057
|
-
}, `已中断恢复
|
|
1126
|
+
title: t('本次迭代从上一次中断的断点继续执行'),
|
|
1127
|
+
}, tf(`已中断恢复 ×{a0}`, { a0: String(resumeCount) }))
|
|
1058
1128
|
: null
|
|
1059
1129
|
const imageChip = imageCount > 0
|
|
1060
1130
|
? React.createElement('span', {
|
|
1061
1131
|
className: 'iterate-chip-images',
|
|
1062
1132
|
key: 'images',
|
|
1063
|
-
title: '会话中检测到用户附带的图片,评审将作为视觉证据参考',
|
|
1064
|
-
}, `附件图片
|
|
1133
|
+
title: t('会话中检测到用户附带的图片,评审将作为视觉证据参考'),
|
|
1134
|
+
}, tf(`附件图片 {a0}`, { a0: String(imageCount) }))
|
|
1065
1135
|
: null
|
|
1066
1136
|
|
|
1067
1137
|
// Live run-state awareness: surface the transcript's current workflow phase
|
|
@@ -1075,8 +1145,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1075
1145
|
key: 'phase',
|
|
1076
1146
|
'data-live': transcript.active === true ? '' : undefined,
|
|
1077
1147
|
title: transcript.active === true
|
|
1078
|
-
?
|
|
1079
|
-
:
|
|
1148
|
+
? tf(`当前处于「{a0}」阶段(运行中)`, { a0: phase })
|
|
1149
|
+
: tf(`最近一次执行停留在「{a0}」阶段(已结束)`, { a0: phase }),
|
|
1080
1150
|
}, `${phase} · ${runStatusText(transcript.active === true, transcript)}`)
|
|
1081
1151
|
: null
|
|
1082
1152
|
|
|
@@ -1089,7 +1159,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1089
1159
|
className: 'iterate-chip-taskmode',
|
|
1090
1160
|
key: 'taskmode',
|
|
1091
1161
|
'data-mode': taskMode,
|
|
1092
|
-
title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
|
|
1162
|
+
title: taskMode === 'code' ? t('代码模式:直接修改文件') : t('迭代模式:审查-修复循环'),
|
|
1093
1163
|
}, taskMode === 'code' ? '● code' : '● iterate')
|
|
1094
1164
|
: null
|
|
1095
1165
|
|
|
@@ -1108,7 +1178,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1108
1178
|
React.createElement(
|
|
1109
1179
|
'span',
|
|
1110
1180
|
{ key: '+more', className: 'iterate-dim-badge iterate-dim-more', title: dimNames.slice(6).join(', ') },
|
|
1111
|
-
|
|
1181
|
+
tf(`+{a0} 更多`, { a0: overflow }),
|
|
1112
1182
|
),
|
|
1113
1183
|
)
|
|
1114
1184
|
}
|
|
@@ -1123,7 +1193,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1123
1193
|
? React.createElement('span', {
|
|
1124
1194
|
className: 'iterate-metric',
|
|
1125
1195
|
key: 'fixes',
|
|
1126
|
-
title: '本轮已应用的原子修复数(正常模式)',
|
|
1196
|
+
title: t('本轮已应用的原子修复数(正常模式)'),
|
|
1127
1197
|
}, `${String(fixCount)} fixes`)
|
|
1128
1198
|
: null
|
|
1129
1199
|
|
|
@@ -1134,8 +1204,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1134
1204
|
? React.createElement('span', {
|
|
1135
1205
|
className: 'iterate-chip-resume',
|
|
1136
1206
|
key: 'converged',
|
|
1137
|
-
title: '审查已收敛:最后一轮未发现新问题',
|
|
1138
|
-
}, '✓ 已收敛')
|
|
1207
|
+
title: t('审查已收敛:最后一轮未发现新问题'),
|
|
1208
|
+
}, t('✓ 已收敛'))
|
|
1139
1209
|
: null
|
|
1140
1210
|
|
|
1141
1211
|
const sevMetric = (key: 'critical' | 'high' | 'medium' | 'low', label: string) =>
|
|
@@ -1203,14 +1273,14 @@ function StatsCard(props: SlotProps) {
|
|
|
1203
1273
|
React.createElement('td', { style: { color: SEVERITY_COLOR.low } }, String(h.low)),
|
|
1204
1274
|
),
|
|
1205
1275
|
)
|
|
1206
|
-
const trendLine = `首轮
|
|
1207
|
-
const completion = buildCompletionSummary(report)
|
|
1276
|
+
const trendLine = tf(`首轮 {a0} → 末轮 {a1} 项,降幅 {a2}%{a3}`, { a0: trend.firstRound, a1: trend.lastRound, a2: trend.reductionPercent, a3: trend.converged ? t(',已收敛') : '' })
|
|
1277
|
+
const completion = buildCompletionSummary(report, getLang())
|
|
1208
1278
|
|
|
1209
1279
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'stats', className: 'iterate-stats' },
|
|
1210
1280
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
1211
|
-
React.createElement('span', {}, 'Iterate · 收敛统计'),
|
|
1281
|
+
React.createElement('span', {}, t('Iterate · 收敛统计')),
|
|
1212
1282
|
React.createElement('span', { className: 'iterate-triage-hint' },
|
|
1213
|
-
`Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? '已收敛' : '未收敛'}`,
|
|
1283
|
+
`Round ${getCurrentRound(report)}/${getTotalRounds(report)} · ${total} findings · ${report.convergence && report.convergence.converged ? t('已收敛') : t('未收敛')}`,
|
|
1214
1284
|
),
|
|
1215
1285
|
),
|
|
1216
1286
|
React.createElement('div', { className: 'iterate-stats-grid' }, ...rows),
|
|
@@ -1221,15 +1291,15 @@ function StatsCard(props: SlotProps) {
|
|
|
1221
1291
|
className: 'iterate-btn',
|
|
1222
1292
|
'data-primary': showHistory ? '' : undefined,
|
|
1223
1293
|
onClick: () => setShowHistory((v) => !v),
|
|
1224
|
-
}, showHistory ? '收起历史' : '历史 / 趋势'),
|
|
1294
|
+
}, showHistory ? t('收起历史') : t('历史 / 趋势')),
|
|
1225
1295
|
),
|
|
1226
1296
|
showHistory
|
|
1227
1297
|
? React.createElement('div', { className: 'iterate-history-body' },
|
|
1228
1298
|
React.createElement('table', { className: 'iterate-history-table' },
|
|
1229
1299
|
React.createElement('thead', {},
|
|
1230
1300
|
React.createElement('tr', {},
|
|
1231
|
-
React.createElement('th', {}, '轮次'),
|
|
1232
|
-
React.createElement('th', {}, '发现'),
|
|
1301
|
+
React.createElement('th', {}, t('轮次')),
|
|
1302
|
+
React.createElement('th', {}, t('发现')),
|
|
1233
1303
|
React.createElement('th', { style: { color: SEVERITY_COLOR.critical } }, 'CRIT'),
|
|
1234
1304
|
React.createElement('th', { style: { color: SEVERITY_COLOR.high } }, 'HIGH'),
|
|
1235
1305
|
React.createElement('th', { style: { color: SEVERITY_COLOR.medium } }, 'MED'),
|
|
@@ -1397,7 +1467,7 @@ function TriagePanel(props: SlotProps) {
|
|
|
1397
1467
|
}
|
|
1398
1468
|
|
|
1399
1469
|
const doTriggerNewRound = () => {
|
|
1400
|
-
const cmd = '请继续执行下一轮迭代审查'
|
|
1470
|
+
const cmd = t('请继续执行下一轮迭代审查')
|
|
1401
1471
|
copyWithFeedback(cmd, 'new-round')
|
|
1402
1472
|
}
|
|
1403
1473
|
|
|
@@ -1405,7 +1475,7 @@ function TriagePanel(props: SlotProps) {
|
|
|
1405
1475
|
// iterate_checkpoint has NO rollback operation (save/load/resume/clear
|
|
1406
1476
|
// only); reverting applied fixes is done via iterate_rollback by fix id,
|
|
1407
1477
|
// so the paste-able instruction must point there instead.
|
|
1408
|
-
const cmd = '请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复'
|
|
1478
|
+
const cmd = t('请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复')
|
|
1409
1479
|
copyWithFeedback(cmd, 'rollback')
|
|
1410
1480
|
}
|
|
1411
1481
|
|
|
@@ -1507,27 +1577,27 @@ function TriagePanel(props: SlotProps) {
|
|
|
1507
1577
|
),
|
|
1508
1578
|
React.createElement('div', { className: 'iterate-finding-summary' }, String(finding.summary || '')),
|
|
1509
1579
|
React.createElement('div', { className: 'iterate-finding-actions' },
|
|
1510
|
-
btn('y 修复', 'keep', '保留该 finding,进入修复'),
|
|
1511
|
-
btn('n 跳过', 'skip', '跳过该 finding'),
|
|
1512
|
-
btn('a 已知有意', 'ignore', '标记为已知有意并写入 known_intentional'),
|
|
1580
|
+
btn(t('y 修复'), 'keep', t('保留该 finding,进入修复')),
|
|
1581
|
+
btn(t('n 跳过'), 'skip', t('跳过该 finding')),
|
|
1582
|
+
btn(t('a 已知有意'), 'ignore', t('标记为已知有意并写入 known_intentional')),
|
|
1513
1583
|
),
|
|
1514
1584
|
)
|
|
1515
1585
|
})
|
|
1516
1586
|
|
|
1517
1587
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
1518
1588
|
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=已知有意 · ↑/↓ 选择'),
|
|
1589
|
+
React.createElement('span', { role: 'heading', 'aria-level': 3 }, tf(`Iterate · Findings 分诊 ({a0}/{a1})`, { a0: filtered.length, a1: findings.length })),
|
|
1590
|
+
React.createElement('span', { className: 'iterate-triage-hint' }, t('y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择')),
|
|
1521
1591
|
),
|
|
1522
1592
|
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: '' }, '全部严重度'),
|
|
1593
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.severities[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setSeverityFilter(e.target.value), 'aria-label': t('按严重度筛选') },
|
|
1594
|
+
React.createElement('option', { value: '' }, t('全部严重度')),
|
|
1525
1595
|
...options.severities.map((s) =>
|
|
1526
1596
|
React.createElement('option', { key: s.value, value: s.value }, `${severityLabel(s.value)} (${s.count})`),
|
|
1527
1597
|
),
|
|
1528
1598
|
),
|
|
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: '' }, '全部维度'),
|
|
1599
|
+
React.createElement('select', { className: 'iterate-filter-select', value: filter.dimensions[0] || '', onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDimensionFilter(e.target.value), 'aria-label': t('按维度筛选') },
|
|
1600
|
+
React.createElement('option', { value: '' }, t('全部维度')),
|
|
1531
1601
|
...options.dimensions.map((d) =>
|
|
1532
1602
|
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
1533
1603
|
),
|
|
@@ -1535,37 +1605,37 @@ function TriagePanel(props: SlotProps) {
|
|
|
1535
1605
|
React.createElement('input', {
|
|
1536
1606
|
className: 'iterate-filter-search',
|
|
1537
1607
|
type: 'search',
|
|
1538
|
-
placeholder: '搜索文件 / 摘要…',
|
|
1539
|
-
'aria-label': '搜索文件或摘要',
|
|
1608
|
+
placeholder: t('搜索文件 / 摘要…'),
|
|
1609
|
+
'aria-label': t('搜索文件或摘要'),
|
|
1540
1610
|
value: filter.search,
|
|
1541
1611
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setSearchFilter(e.target.value),
|
|
1542
1612
|
}),
|
|
1543
1613
|
React.createElement('span', { className: 'iterate-filter-count' },
|
|
1544
1614
|
isFilterActive
|
|
1545
|
-
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, `清除筛选(显示
|
|
1546
|
-
: `共
|
|
1615
|
+
? React.createElement('button', { className: 'iterate-batch-btn', onClick: clearFilter }, tf(`清除筛选(显示 {a0})`, { a0: filtered.length }))
|
|
1616
|
+
: tf(`共 {a0} 项`, { a0: findings.length }),
|
|
1547
1617
|
),
|
|
1548
1618
|
),
|
|
1549
1619
|
React.createElement('div', { className: 'iterate-batch' },
|
|
1550
|
-
React.createElement('span', { className: 'iterate-batch-label' }, '批量作用于:'),
|
|
1620
|
+
React.createElement('span', { className: 'iterate-batch-label' }, t('批量作用于:')),
|
|
1551
1621
|
React.createElement('button', {
|
|
1552
1622
|
className: selectAll ? 'iterate-batch-btn iterate-batch-scope' : 'iterate-batch-btn iterate-batch-scope iterate-batch-scope-on',
|
|
1553
1623
|
onClick: () => setSelectAll(false),
|
|
1554
|
-
title: '批量按钮仅作用于当前筛选可见的 findings',
|
|
1555
|
-
}, `可见
|
|
1624
|
+
title: t('批量按钮仅作用于当前筛选可见的 findings'),
|
|
1625
|
+
}, tf(`可见 {a0}`, { a0: indices.length })),
|
|
1556
1626
|
React.createElement('button', {
|
|
1557
1627
|
className: selectAll ? 'iterate-batch-btn iterate-batch-scope iterate-batch-scope-on' : 'iterate-batch-btn iterate-batch-scope',
|
|
1558
1628
|
onClick: () => setSelectAll(true),
|
|
1559
|
-
title: '批量按钮作用于全部 findings',
|
|
1560
|
-
}, `全部
|
|
1629
|
+
title: t('批量按钮作用于全部 findings'),
|
|
1630
|
+
}, tf(`全部 {a0}`, { a0: allIndices.length })),
|
|
1561
1631
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('keep') }, 'y'),
|
|
1562
1632
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('skip') }, 'n'),
|
|
1563
1633
|
React.createElement('button', { className: 'iterate-batch-btn', onClick: () => applyBatch('ignore') }, 'a'),
|
|
1564
|
-
React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: '把所有判定恢复为默认 y(修复)' }, '重置'),
|
|
1634
|
+
React.createElement('button', { className: 'iterate-batch-btn', onClick: doResetVerdicts, title: t('把所有判定恢复为默认 y(修复)') }, t('重置')),
|
|
1565
1635
|
),
|
|
1566
1636
|
...rows,
|
|
1567
1637
|
React.createElement('div', { className: 'iterate-triage-foot' },
|
|
1568
|
-
React.createElement('span', {}, `y
|
|
1638
|
+
React.createElement('span', {}, tf(`y {a0} · n {a1} · a {a2} · 待写回 known_intentional:{a3} 条`, { a0: counts.keep, a1: counts.skip, a2: counts.ignore, a3: ignoredCount })),
|
|
1569
1639
|
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1570
1640
|
// Gap #7: the apply instruction (safe path) is PRIMARY; the raw YAML
|
|
1571
1641
|
// is demoted to an explicit "just the snippet" escape hatch.
|
|
@@ -1575,65 +1645,65 @@ function TriagePanel(props: SlotProps) {
|
|
|
1575
1645
|
'data-failed': cmdFailed === 'apply' ? '' : undefined,
|
|
1576
1646
|
onClick: doBuildInstruction, disabled: ignoredCount === 0,
|
|
1577
1647
|
title: ignoredCount === 0
|
|
1578
|
-
? '当前没有标记为「已知有意」的 finding'
|
|
1579
|
-
: '复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)',
|
|
1580
|
-
}, cmdCopied === 'apply' ? '已复制' : cmdFailed === 'apply' ? '复制失败' :
|
|
1648
|
+
? t('当前没有标记为「已知有意」的 finding')
|
|
1649
|
+
: t('复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)'),
|
|
1650
|
+
}, cmdCopied === 'apply' ? t('已复制') : cmdFailed === 'apply' ? t('复制失败') : tf(`生成应用指令{a0}`, { a0: ignoredCount > 0 ? `(${ignoredCount})` : '' })),
|
|
1581
1651
|
React.createElement('button', {
|
|
1582
1652
|
className: 'iterate-btn',
|
|
1583
1653
|
'data-copied': copied ? '' : undefined,
|
|
1584
1654
|
'data-failed': cmdFailed === 'yaml' ? '' : undefined,
|
|
1585
1655
|
onClick: doCopyYaml, disabled: ignoredCount === 0,
|
|
1586
1656
|
title: ignoredCount === 0
|
|
1587
|
-
? '当前没有标记为「已知有意」的 finding'
|
|
1588
|
-
: '只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)',
|
|
1589
|
-
}, copied ? '已复制' : cmdFailed === 'yaml' ? '复制失败' : '仅复制 YAML 片段'),
|
|
1657
|
+
? t('当前没有标记为「已知有意」的 finding')
|
|
1658
|
+
: t('只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)'),
|
|
1659
|
+
}, copied ? t('已复制') : cmdFailed === 'yaml' ? t('复制失败') : t('仅复制 YAML 片段')),
|
|
1590
1660
|
React.createElement('button', {
|
|
1591
1661
|
className: 'iterate-btn',
|
|
1592
1662
|
'data-copied': cmdCopied === 'readback' ? '' : undefined,
|
|
1593
1663
|
'data-failed': cmdFailed === 'readback' ? '' : undefined,
|
|
1594
1664
|
onClick: doReadback,
|
|
1595
|
-
title: '复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)',
|
|
1596
|
-
}, cmdCopied === 'readback' ? '已复制' : cmdFailed === 'readback' ? '复制失败' : '回读已写入条目'),
|
|
1665
|
+
title: t('复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)'),
|
|
1666
|
+
}, cmdCopied === 'readback' ? t('已复制') : cmdFailed === 'readback' ? t('复制失败') : t('回读已写入条目')),
|
|
1597
1667
|
),
|
|
1598
1668
|
cmdCopied === 'apply'
|
|
1599
|
-
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, '已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。')
|
|
1669
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, t('已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。'))
|
|
1600
1670
|
: cmdCopied === 'readback'
|
|
1601
|
-
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, '已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。')
|
|
1671
|
+
? React.createElement('span', { className: 'iterate-triage-hint', role: 'status' }, t('已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。'))
|
|
1602
1672
|
: null,
|
|
1603
1673
|
),
|
|
1604
1674
|
// v3.0: Command buttons for native actions (§8)
|
|
1605
1675
|
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)' } }, '指挥操作'),
|
|
1676
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('指挥操作')),
|
|
1607
1677
|
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1608
1678
|
React.createElement('button', {
|
|
1609
1679
|
className: 'iterate-cmd', 'data-primary': '',
|
|
1610
1680
|
'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1611
1681
|
'data-failed': cmdFailed === 'approve-arch' ? '' : undefined,
|
|
1612
1682
|
onClick: doApproveArchitecturalFix,
|
|
1613
|
-
title: '批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)',
|
|
1614
|
-
}, cmdCopied === 'approve-arch' ? '已复制' : cmdFailed === 'approve-arch' ? '复制失败' : '批准架构修复'),
|
|
1683
|
+
title: t('批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)'),
|
|
1684
|
+
}, cmdCopied === 'approve-arch' ? t('已复制') : cmdFailed === 'approve-arch' ? t('复制失败') : t('批准架构修复')),
|
|
1615
1685
|
React.createElement('button', {
|
|
1616
1686
|
className: 'iterate-cmd',
|
|
1617
1687
|
'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1618
1688
|
'data-failed': cmdFailed === 'new-round' ? '' : undefined,
|
|
1619
1689
|
onClick: doTriggerNewRound,
|
|
1620
|
-
title: '触发新一轮迭代审查',
|
|
1621
|
-
}, cmdCopied === 'new-round' ? '已复制' : cmdFailed === 'new-round' ? '复制失败' : '触发新一轮'),
|
|
1690
|
+
title: t('触发新一轮迭代审查'),
|
|
1691
|
+
}, cmdCopied === 'new-round' ? t('已复制') : cmdFailed === 'new-round' ? t('复制失败') : t('触发新一轮')),
|
|
1622
1692
|
React.createElement('button', {
|
|
1623
1693
|
className: 'iterate-cmd', 'data-primary': '',
|
|
1624
1694
|
'data-copied': cmdCopied === 'assign' ? '' : undefined,
|
|
1625
1695
|
'data-failed': cmdFailed === 'assign' ? '' : undefined,
|
|
1626
1696
|
onClick: doAssignFindings,
|
|
1627
1697
|
disabled: (selectAll ? allIndices : indices).length === 0,
|
|
1628
|
-
title: selectAll ? `指派全部
|
|
1629
|
-
}, cmdCopied === 'assign' ? '已复制' : cmdFailed === 'assign' ? '复制失败' : '指派修复'),
|
|
1698
|
+
title: selectAll ? tf(`指派全部 {a0} 个 findings 修复(逐个 iterate_fix)`, { a0: allIndices.length }) : tf(`指派当前可见 {a0} 个 findings 修复(逐个 iterate_fix)`, { a0: indices.length }),
|
|
1699
|
+
}, cmdCopied === 'assign' ? t('已复制') : cmdFailed === 'assign' ? t('复制失败') : t('指派修复')),
|
|
1630
1700
|
React.createElement('button', {
|
|
1631
1701
|
className: 'iterate-cmd', 'data-danger': '',
|
|
1632
1702
|
'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1633
1703
|
'data-failed': cmdFailed === 'rollback' ? '' : undefined,
|
|
1634
1704
|
onClick: doRollbackToCheckpoint,
|
|
1635
|
-
title: '回滚到上一个检查点',
|
|
1636
|
-
}, cmdCopied === 'rollback' ? '已复制' : cmdFailed === 'rollback' ? '复制失败' : '回滚检查点'),
|
|
1705
|
+
title: t('回滚到上一个检查点'),
|
|
1706
|
+
}, cmdCopied === 'rollback' ? t('已复制') : cmdFailed === 'rollback' ? t('复制失败') : t('回滚检查点')),
|
|
1637
1707
|
),
|
|
1638
1708
|
),
|
|
1639
1709
|
payload
|
|
@@ -1656,15 +1726,15 @@ function VerdictBanner(props: SlotProps) {
|
|
|
1656
1726
|
'div',
|
|
1657
1727
|
{ 'data-iterate-root': '', 'data-iterate': 'verdict', className: 'iterate-verdict' },
|
|
1658
1728
|
React.createElement('span', { className: 'iterate-verdict-tag', 'data-ok': ok ? '' : undefined, 'data-warn': ok ? undefined : '' },
|
|
1659
|
-
ok ? '报告已批准' : '报告需修订'),
|
|
1729
|
+
ok ? t('报告已批准') : t('报告需修订')),
|
|
1660
1730
|
React.createElement('span', { className: 'iterate-verdict-detail' },
|
|
1661
|
-
item(verdict.totalFindings, '项发现'),
|
|
1662
|
-
item(verdict.totalRounds, '轮'),
|
|
1663
|
-
item(verdict.checksRun, '项审查'),
|
|
1731
|
+
item(verdict.totalFindings, t('项发现')),
|
|
1732
|
+
item(verdict.totalRounds, t('轮')),
|
|
1733
|
+
item(verdict.checksRun, t('项审查')),
|
|
1664
1734
|
ok
|
|
1665
|
-
? phrase('报告通过全部一致性检查')
|
|
1666
|
-
: item(verdict.reportIssues, '处报告缺陷'),
|
|
1667
|
-
phrase(verdict.converged ? '已收敛' : '未收敛'),
|
|
1735
|
+
? phrase(t('报告通过全部一致性检查'))
|
|
1736
|
+
: item(verdict.reportIssues, t('处报告缺陷')),
|
|
1737
|
+
phrase(verdict.converged ? t('已收敛') : t('未收敛')),
|
|
1668
1738
|
),
|
|
1669
1739
|
)
|
|
1670
1740
|
}
|
|
@@ -1713,7 +1783,7 @@ function ProgressCapsule() {
|
|
|
1713
1783
|
const listener = (payload: { round: number; converged: boolean }) => {
|
|
1714
1784
|
const converged = payload && payload.converged === true
|
|
1715
1785
|
const round = payload && typeof payload.round === 'number' ? payload.round : '?'
|
|
1716
|
-
setInfo({ text: converged ? `Round
|
|
1786
|
+
setInfo({ text: converged ? tf(`Round {a0} 完成 · 已收敛`, { a0: round }) : tf(`Round {a0} 完成`, { a0: round }), ok: converged })
|
|
1717
1787
|
if (timer) clearTimeout(timer)
|
|
1718
1788
|
// Converged notifications stay a bit longer.
|
|
1719
1789
|
timer = setTimeout(() => setInfo(null), converged ? 3600 : 2400)
|
|
@@ -1736,7 +1806,17 @@ function ProgressCapsule() {
|
|
|
1736
1806
|
}
|
|
1737
1807
|
|
|
1738
1808
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
1739
|
-
function SettingsPanel(
|
|
1809
|
+
function SettingsPanel(props: SlotProps) {
|
|
1810
|
+
// Re-render on a language switch (the slot wrapper covers the tree; this
|
|
1811
|
+
// keeps the panel honest even if it is ever rendered directly).
|
|
1812
|
+
const lang = useLang()
|
|
1813
|
+
// Root-scoped slot: the host renders settings OUTSIDE the session scope, so
|
|
1814
|
+
// take the session the session-scoped slots published (a host that does pass
|
|
1815
|
+
// `props.session` wins). This is what makes the config read-back visible.
|
|
1816
|
+
// The hook is called UNCONDITIONALLY — a conditional hook would break the
|
|
1817
|
+
// panel as soon as a host started passing `props.session`.
|
|
1818
|
+
const cachedSession = useSessionSnapshot()
|
|
1819
|
+
const session = props && props.session ? props.session : cachedSession
|
|
1740
1820
|
const [enabled, setEnabled] = React.useState(themeEnabled)
|
|
1741
1821
|
const [guideCopied, setGuideCopied] = React.useState(false)
|
|
1742
1822
|
const [statusCopied, setStatusCopied] = React.useState(false)
|
|
@@ -1753,11 +1833,21 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1753
1833
|
const [cfgCopied, setCfgCopied] = React.useState(false)
|
|
1754
1834
|
const [cfgFailed, setCfgFailed] = React.useState(false)
|
|
1755
1835
|
const [cfgText, setCfgText] = React.useState<string | null>(null)
|
|
1836
|
+
// Gap #1 (3.7.0): the read half of the write -> read-back loop.
|
|
1837
|
+
const [cfgReadCopied, setCfgReadCopied] = React.useState(false)
|
|
1838
|
+
const [cfgReadText, setCfgReadText] = React.useState<string | null>(null)
|
|
1756
1839
|
const [pullCopied, setPullCopied] = React.useState(false)
|
|
1757
|
-
const [copyError, setCopyError] = React.useState<'guide' | 'status' | 'pull' | null>(null)
|
|
1840
|
+
const [copyError, setCopyError] = React.useState<'guide' | 'status' | 'pull' | 'cfgRead' | null>(null)
|
|
1758
1841
|
const guide = buildConfigEditGuide()
|
|
1759
1842
|
const statusGuide = buildRuntimeStatusGuide()
|
|
1760
1843
|
|
|
1844
|
+
// What the config actually says right now: latest `iterate_config` result
|
|
1845
|
+
// echoed into the session, then the value of the field being edited.
|
|
1846
|
+
const cfgMeta = configFieldByKey(cfgField)
|
|
1847
|
+
const cfgRead = scanSessionForConfigRead(session)
|
|
1848
|
+
const cfgCurrent = cfgRead ? configValueByPath(cfgRead, cfgField) : null
|
|
1849
|
+
const cfgFieldLabel = cfgMeta ? cfgMeta.label[getLang()] : cfgField
|
|
1850
|
+
|
|
1761
1851
|
// Audit: don't leak flash timers across settings unmounts.
|
|
1762
1852
|
const cfgTimers = React.useRef<number[]>([])
|
|
1763
1853
|
React.useEffect(() => () => {
|
|
@@ -1772,8 +1862,9 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1772
1862
|
}
|
|
1773
1863
|
|
|
1774
1864
|
/** Flash a per-slot "已复制" state for the given duration. */
|
|
1775
|
-
const flashCopied = (slot: 'guide' | 'status' | 'pull') => {
|
|
1776
|
-
const setter =
|
|
1865
|
+
const flashCopied = (slot: 'guide' | 'status' | 'pull' | 'cfgRead') => {
|
|
1866
|
+
const setter =
|
|
1867
|
+
slot === 'guide' ? setGuideCopied : slot === 'status' ? setStatusCopied : slot === 'pull' ? setPullCopied : setCfgReadCopied
|
|
1777
1868
|
setter(true)
|
|
1778
1869
|
cfgTimers.current.push(window.setTimeout(() => setter(false), 1600))
|
|
1779
1870
|
}
|
|
@@ -1781,7 +1872,7 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1781
1872
|
/** Copy a guide/status block; flash success only when it actually copied,
|
|
1782
1873
|
* and surface a visible error when the clipboard write is refused (the old
|
|
1783
1874
|
* behaviour was a silent no-op, which reads as a broken button). */
|
|
1784
|
-
const doCopy = (text: string, slot: 'guide' | 'status' | 'pull') => {
|
|
1875
|
+
const doCopy = (text: string, slot: 'guide' | 'status' | 'pull' | 'cfgRead') => {
|
|
1785
1876
|
copyText(text).then((ok) => {
|
|
1786
1877
|
setCopyError(ok ? null : slot)
|
|
1787
1878
|
if (ok) flashCopied(slot)
|
|
@@ -1801,6 +1892,19 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1801
1892
|
})
|
|
1802
1893
|
}
|
|
1803
1894
|
|
|
1895
|
+
/** Copy the read-back instruction; a refused clipboard reveals it below. */
|
|
1896
|
+
const doCopyConfigRead = () => {
|
|
1897
|
+
const text = buildConfigReadInstruction()
|
|
1898
|
+
copyText(text).then((ok) => {
|
|
1899
|
+
setCopyError(ok ? null : 'cfgRead')
|
|
1900
|
+
if (ok) {
|
|
1901
|
+
flashCopied('cfgRead')
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1904
|
+
setCfgReadText(text)
|
|
1905
|
+
})
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1804
1908
|
/** Two-step destroy guard: first click arms, second click clears. */
|
|
1805
1909
|
const requestClear = () => {
|
|
1806
1910
|
if (confirming) {
|
|
@@ -1815,27 +1919,27 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1815
1919
|
}
|
|
1816
1920
|
|
|
1817
1921
|
const clearButton = clearedInfo !== null
|
|
1818
|
-
? React.createElement('button', { className: 'iterate-btn', 'data-copied': '', disabled: true }, `已清除
|
|
1922
|
+
? React.createElement('button', { className: 'iterate-btn', 'data-copied': '', disabled: true }, tf(`已清除 {a0} 条`, { a0: clearedInfo }))
|
|
1819
1923
|
: React.createElement('button', {
|
|
1820
1924
|
className: 'iterate-btn',
|
|
1821
1925
|
'data-danger': confirming ? undefined : '',
|
|
1822
1926
|
'data-confirm': confirming ? '' : undefined,
|
|
1823
1927
|
onClick: requestClear,
|
|
1824
|
-
title: confirming ? '再次点击以确认清除全部判定' : '清除所有分诊判定记录(需要二次确认)',
|
|
1825
|
-
}, confirming ? '确认清除?' : '清除分诊')
|
|
1928
|
+
title: confirming ? t('再次点击以确认清除全部判定') : t('清除所有分诊判定记录(需要二次确认)'),
|
|
1929
|
+
}, confirming ? t('确认清除?') : t('清除分诊'))
|
|
1826
1930
|
|
|
1827
1931
|
const themeCard = React.createElement('div', { key: 'theme', className: 'iterate-scard' },
|
|
1828
1932
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1829
1933
|
React.createElement('div', {},
|
|
1830
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '迭代主题'),
|
|
1831
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。'),
|
|
1934
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('迭代主题')),
|
|
1935
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。')),
|
|
1832
1936
|
),
|
|
1833
1937
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1834
|
-
React.createElement('span', { className: 'iterate-chip', 'data-ok': enabled ? '' : undefined }, enabled ? '已启用' : '已关闭'),
|
|
1938
|
+
React.createElement('span', { className: 'iterate-chip', 'data-ok': enabled ? '' : undefined }, enabled ? t('已启用') : t('已关闭')),
|
|
1835
1939
|
React.createElement('button', {
|
|
1836
1940
|
role: 'switch',
|
|
1837
1941
|
'aria-checked': enabled,
|
|
1838
|
-
'aria-label': '开关 iterate 主题',
|
|
1942
|
+
'aria-label': t('开关 iterate 主题'),
|
|
1839
1943
|
className: 'iterate-switch',
|
|
1840
1944
|
'data-on': enabled ? '' : undefined,
|
|
1841
1945
|
onClick: toggleTheme,
|
|
@@ -1844,14 +1948,36 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1844
1948
|
),
|
|
1845
1949
|
)
|
|
1846
1950
|
|
|
1951
|
+
const langCard = React.createElement('div', { key: 'lang', className: 'iterate-scard' },
|
|
1952
|
+
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1953
|
+
React.createElement('div', {},
|
|
1954
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('界面语言')),
|
|
1955
|
+
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
1956
|
+
t('面板文案的显示语言;复制给模型的指令保持中文,与 SKILL.md 的工作流契约一致。'),
|
|
1957
|
+
),
|
|
1958
|
+
),
|
|
1959
|
+
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1960
|
+
React.createElement('span', { className: 'iterate-chip', 'data-ok': '' }, LANG_NAMES[lang]),
|
|
1961
|
+
React.createElement('button', {
|
|
1962
|
+
role: 'switch',
|
|
1963
|
+
'aria-checked': lang === 'en',
|
|
1964
|
+
'aria-label': t('切换界面语言'),
|
|
1965
|
+
className: 'iterate-switch',
|
|
1966
|
+
'data-on': lang === 'en' ? '' : undefined,
|
|
1967
|
+
onClick: () => setLang(lang === 'en' ? 'zh' : 'en'),
|
|
1968
|
+
}, React.createElement('span', { className: 'iterate-switch-knob' })),
|
|
1969
|
+
),
|
|
1970
|
+
),
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1847
1973
|
const dataCard = React.createElement('div', { key: 'data', className: 'iterate-scard' },
|
|
1848
1974
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1849
1975
|
React.createElement('div', {},
|
|
1850
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '分诊持久化'),
|
|
1851
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, 'y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。'),
|
|
1976
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('分诊持久化')),
|
|
1977
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。')),
|
|
1852
1978
|
),
|
|
1853
1979
|
React.createElement('div', { className: 'iterate-scard-actions' },
|
|
1854
|
-
React.createElement('span', { className: 'iterate-chip' }, '本地保存'),
|
|
1980
|
+
React.createElement('span', { className: 'iterate-chip' }, t('本地保存')),
|
|
1855
1981
|
clearButton,
|
|
1856
1982
|
),
|
|
1857
1983
|
),
|
|
@@ -1860,12 +1986,12 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1860
1986
|
const guideCard = React.createElement('div', { key: 'guide', className: 'iterate-scard' },
|
|
1861
1987
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1862
1988
|
React.createElement('div', {},
|
|
1863
|
-
React.createElement('div', { className: 'iterate-settings-title' }, '配置管理'),
|
|
1864
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。'),
|
|
1989
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('配置管理')),
|
|
1990
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。')),
|
|
1865
1991
|
),
|
|
1866
1992
|
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 ? '收起' : '展开'),
|
|
1993
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': guideCopied ? '' : undefined, onClick: () => doCopy(guide, 'guide') }, guideCopied ? t('已复制') : t('复制指引')),
|
|
1994
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowGuide((v) => !v) }, showGuide ? t('收起') : t('展开')),
|
|
1869
1995
|
),
|
|
1870
1996
|
),
|
|
1871
1997
|
// Gap #8: wire up the previously-dead `buildConfigFieldInstruction` — pick
|
|
@@ -1875,32 +2001,60 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1875
2001
|
React.createElement('select', {
|
|
1876
2002
|
className: 'iterate-filter-select',
|
|
1877
2003
|
value: cfgField,
|
|
1878
|
-
'aria-label': '选择要修改的配置字段',
|
|
2004
|
+
'aria-label': t('选择要修改的配置字段'),
|
|
1879
2005
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setCfgField(e.target.value),
|
|
1880
2006
|
},
|
|
1881
2007
|
...CONFIG_EDIT_FIELDS.map((f) =>
|
|
1882
|
-
React.createElement('option', { key: f.key, value: f.key }, `${f.label}(${f.key})`),
|
|
2008
|
+
React.createElement('option', { key: f.key, value: f.key }, getLang() === 'en' ? `${f.label.en} (${f.key})` : `${f.label.zh}(${f.key})`),
|
|
1883
2009
|
),
|
|
1884
2010
|
),
|
|
1885
2011
|
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
1886
|
-
|
|
2012
|
+
cfgMeta?.hint[getLang()] ?? '',
|
|
1887
2013
|
),
|
|
1888
2014
|
React.createElement('button', {
|
|
1889
2015
|
className: 'iterate-btn', 'data-primary': '',
|
|
1890
2016
|
'data-copied': cfgCopied ? '' : undefined,
|
|
1891
2017
|
'data-failed': cfgFailed ? '' : undefined,
|
|
1892
2018
|
onClick: doBuildConfigInstruction,
|
|
1893
|
-
title: '生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)',
|
|
1894
|
-
}, cfgCopied ? '已复制' : cfgFailed ? '复制失败' : '生成写入指令'),
|
|
2019
|
+
title: t('生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)'),
|
|
2020
|
+
}, cfgCopied ? t('已复制') : cfgFailed ? t('复制失败') : t('生成写入指令')),
|
|
1895
2021
|
cfgFailed
|
|
1896
|
-
? React.createElement('span', { className: 'iterate-settings-desc' }, '剪贴板被拒绝——指令已展示在下方,请手动复制。')
|
|
2022
|
+
? React.createElement('span', { className: 'iterate-settings-desc' }, t('剪贴板被拒绝——指令已展示在下方,请手动复制。'))
|
|
1897
2023
|
: null,
|
|
1898
2024
|
),
|
|
2025
|
+
// Gap #1 (3.7.0): close the write -> read-back loop. Three honest states:
|
|
2026
|
+
// nothing echoed yet, the value the model read back, or a read-back that
|
|
2027
|
+
// never contained this field (defaults are in play — say so, don't blank).
|
|
2028
|
+
React.createElement('div', { className: 'iterate-guide-bar', style: { marginTop: 8, flexWrap: 'wrap' } },
|
|
2029
|
+
React.createElement('span', { className: 'iterate-settings-desc' },
|
|
2030
|
+
!cfgRead
|
|
2031
|
+
? t('尚未回读当前配置——点「回读当前配置」复制指令发给模型,回显后即可在此看到当前值。')
|
|
2032
|
+
: cfgCurrent && cfgCurrent.resolved
|
|
2033
|
+
? tf('当前值 {a0}:{a1}', { a0: cfgFieldLabel, a1: formatConfigValue(cfgCurrent.value) })
|
|
2034
|
+
: tf('回读结果中没有 {a0}(该字段可能尚未写入配置文件)。', { a0: cfgFieldLabel }),
|
|
2035
|
+
),
|
|
2036
|
+
React.createElement('button', {
|
|
2037
|
+
className: 'iterate-btn',
|
|
2038
|
+
'data-primary': '',
|
|
2039
|
+
'data-copied': cfgReadCopied ? '' : undefined,
|
|
2040
|
+
onClick: doCopyConfigRead,
|
|
2041
|
+
title: t('复制回读指令:让模型调用 iterate_config(operation:"read") 并回显结果'),
|
|
2042
|
+
}, cfgReadCopied ? t('已复制') : t('回读当前配置')),
|
|
2043
|
+
),
|
|
2044
|
+
cfgReadText
|
|
2045
|
+
? React.createElement('div', { className: 'iterate-guide' },
|
|
2046
|
+
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
2047
|
+
React.createElement('span', {}, t('回读指令(粘贴给模型)')),
|
|
2048
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgReadText, 'cfgRead') }, t('复制')),
|
|
2049
|
+
),
|
|
2050
|
+
React.createElement('div', { className: 'iterate-guide-body' }, cfgReadText),
|
|
2051
|
+
)
|
|
2052
|
+
: null,
|
|
1899
2053
|
cfgText
|
|
1900
2054
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1901
2055
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1902
|
-
React.createElement('span', {}, '写入指令(粘贴给模型)'),
|
|
1903
|
-
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgText, 'guide') }, '复制'),
|
|
2056
|
+
React.createElement('span', {}, t('写入指令(粘贴给模型)')),
|
|
2057
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(cfgText, 'guide') }, t('复制')),
|
|
1904
2058
|
),
|
|
1905
2059
|
React.createElement('div', { className: 'iterate-guide-body' }, cfgText),
|
|
1906
2060
|
)
|
|
@@ -1908,8 +2062,9 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1908
2062
|
showGuide
|
|
1909
2063
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1910
2064
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1911
|
-
React.createElement('span', {}, 'iterate.config.yaml 可编辑字段'),
|
|
1912
|
-
React.createElement('
|
|
2065
|
+
React.createElement('span', {}, t('iterate.config.yaml 可编辑字段')),
|
|
2066
|
+
React.createElement('span', { className: 'iterate-chip' }, t('中文(粘贴给模型的契约文案)')),
|
|
2067
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(guide, 'guide') }, t('复制')),
|
|
1913
2068
|
),
|
|
1914
2069
|
React.createElement('div', { className: 'iterate-guide-body' }, guide),
|
|
1915
2070
|
)
|
|
@@ -1919,26 +2074,27 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1919
2074
|
const statusCard = React.createElement('div', { key: 'status', className: 'iterate-scard' },
|
|
1920
2075
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1921
2076
|
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 工具用于查看和管理。'),
|
|
2077
|
+
React.createElement('div', { className: 'iterate-settings-title' }, t('状态概览')),
|
|
2078
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('查看运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。')),
|
|
1924
2079
|
),
|
|
1925
2080
|
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 ? '已复制' : '复制指引'),
|
|
2081
|
+
React.createElement('button', { className: 'iterate-btn', 'data-primary': '', 'data-copied': statusCopied ? '' : undefined, onClick: () => doCopy(statusGuide, 'status') }, statusCopied ? t('已复制') : t('复制指引')),
|
|
1927
2082
|
// Gap #4: RUNTIME_ARTIFACTS stops being documentation-only — this
|
|
1928
2083
|
// copies the instruction that makes the model echo those artifacts
|
|
1929
2084
|
// into the session so the observatory panels can read them.
|
|
1930
2085
|
React.createElement('button', {
|
|
1931
2086
|
className: 'iterate-btn', 'data-copied': pullCopied ? '' : undefined,
|
|
1932
2087
|
onClick: () => doCopy(buildDiskSnapshotInstruction(), 'pull'),
|
|
1933
|
-
}, pullCopied ? '已复制' : '拉取磁盘快照'),
|
|
1934
|
-
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowStatus((v) => !v) }, showStatus ? '收起' : '展开'),
|
|
2088
|
+
}, pullCopied ? t('已复制') : t('拉取磁盘快照')),
|
|
2089
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => setShowStatus((v) => !v) }, showStatus ? t('收起') : t('展开')),
|
|
1935
2090
|
),
|
|
1936
2091
|
),
|
|
1937
2092
|
showStatus
|
|
1938
2093
|
? React.createElement('div', { className: 'iterate-guide' },
|
|
1939
2094
|
React.createElement('div', { className: 'iterate-guide-bar' },
|
|
1940
|
-
React.createElement('span', {}, '运行时布局与清理'),
|
|
1941
|
-
React.createElement('
|
|
2095
|
+
React.createElement('span', {}, t('运行时布局与清理')),
|
|
2096
|
+
React.createElement('span', { className: 'iterate-chip' }, t('中文(粘贴给模型的契约文案)')),
|
|
2097
|
+
React.createElement('button', { className: 'iterate-btn', 'data-ghost': '', onClick: () => doCopy(statusGuide, 'status') }, t('复制')),
|
|
1942
2098
|
),
|
|
1943
2099
|
React.createElement('div', { className: 'iterate-guide-body' }, statusGuide),
|
|
1944
2100
|
)
|
|
@@ -1949,25 +2105,26 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1949
2105
|
React.createElement('div', { className: 'iterate-scard-head' },
|
|
1950
2106
|
React.createElement('div', {},
|
|
1951
2107
|
React.createElement('div', { className: 'iterate-settings-title' }, 'iterate'),
|
|
1952
|
-
React.createElement('div', { className: 'iterate-settings-desc' }, '为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。'),
|
|
2108
|
+
React.createElement('div', { className: 'iterate-settings-desc' }, t('为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。')),
|
|
1953
2109
|
),
|
|
1954
2110
|
React.createElement('span', { className: 'iterate-pill' },
|
|
1955
2111
|
React.createElement('span', { className: 'iterate-pill-dot' }),
|
|
1956
|
-
'就绪',
|
|
2112
|
+
t('就绪'),
|
|
1957
2113
|
),
|
|
1958
2114
|
),
|
|
1959
2115
|
)
|
|
1960
2116
|
|
|
1961
2117
|
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 设置'),
|
|
2118
|
+
React.createElement('div', { className: 'iterate-settings-title', style: { fontSize: 15, fontWeight: 700 } }, t('iterate 设置')),
|
|
1963
2119
|
banner,
|
|
1964
2120
|
copyError
|
|
1965
2121
|
? React.createElement('div', { key: 'copy-err', className: 'iterate-scard', role: 'alert' },
|
|
1966
2122
|
React.createElement('div', { className: 'iterate-settings-desc' },
|
|
1967
|
-
'剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。',
|
|
2123
|
+
t('剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。'),
|
|
1968
2124
|
),
|
|
1969
2125
|
)
|
|
1970
2126
|
: null,
|
|
2127
|
+
langCard,
|
|
1971
2128
|
themeCard,
|
|
1972
2129
|
dataCard,
|
|
1973
2130
|
guideCard,
|
|
@@ -1981,19 +2138,21 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1981
2138
|
* activity stream) is first so it is the default view; F-panels follow.
|
|
1982
2139
|
* Gap #14: labels carry the "Fn" number used everywhere else (README,
|
|
1983
2140
|
* code comments, the copy instructions), so the docs and the UI line up. */
|
|
2141
|
+
/** Visible labels per tab, per language (module constants are evaluated once,
|
|
2142
|
+
* so they carry BOTH languages and resolve at render time). */
|
|
1984
2143
|
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' },
|
|
2144
|
+
{ key: 'live', label: { zh: '实时活动流', en: 'Live activity' }, hotkey: '' },
|
|
2145
|
+
{ key: 'f1', label: { zh: '审查线程', en: 'Review threads' }, hotkey: '1' },
|
|
2146
|
+
{ key: 'f2', label: { zh: '收敛趋势', en: 'Convergence trend' }, hotkey: '2' },
|
|
2147
|
+
{ key: 'f3', label: { zh: '发现定位', en: 'Finding locations' }, hotkey: '3' },
|
|
2148
|
+
{ key: 'f4', label: { zh: '修复与回滚', en: 'Fixes & rollback' }, hotkey: '4' },
|
|
2149
|
+
{ key: 'f5', label: { zh: '断点恢复', en: 'Checkpoint resume' }, hotkey: '5' },
|
|
2150
|
+
{ key: 'f6', label: { zh: '运行控制台', en: 'Run console' }, hotkey: '6' },
|
|
2151
|
+
{ key: 'f7', label: { zh: '决策时间线', en: 'Decision timeline' }, hotkey: '7' },
|
|
1993
2152
|
// v3.0: Quality Command Center tabs
|
|
1994
|
-
{ key: 'f8', label: '质量门禁', hotkey: '8' },
|
|
1995
|
-
{ key: 'f9', label: '经验银行', hotkey: '9' },
|
|
1996
|
-
{ key: 'f10', label: '防御事件', hotkey: '0' },
|
|
2153
|
+
{ key: 'f8', label: { zh: '质量门禁', en: 'Quality gate' }, hotkey: '8' },
|
|
2154
|
+
{ key: 'f9', label: { zh: '经验银行', en: 'Experience bank' }, hotkey: '9' },
|
|
2155
|
+
{ key: 'f10', label: { zh: '防御事件', en: 'Defense events' }, hotkey: '0' },
|
|
1997
2156
|
] as const
|
|
1998
2157
|
|
|
1999
2158
|
/** Digit → tab id. `1-9` map to F1–F9, `0` to F10, `-` back to the live feed
|
|
@@ -2007,26 +2166,31 @@ const OBS_HOTKEY_TABS: Record<string, (typeof OBS_TABS)[number]['key']> = {
|
|
|
2007
2166
|
|
|
2008
2167
|
/** Visible label for a tab (keeps the number prefix in sync with `OBS_TABS`). */
|
|
2009
2168
|
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
|
-
|
|
2169
|
+
const label = t.label[getLang()]
|
|
2170
|
+
if (t.key === 'live') return label
|
|
2171
|
+
return `${t.key.toUpperCase()} ${label}`
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/** Hotkey hint shown in the collapsed header, e.g. "11 页 · 按 1-9/0 切 Fn、- 回实时流". */
|
|
2175
|
+
function obsHotkeyHint(): string {
|
|
2176
|
+
return getLang() === 'en'
|
|
2177
|
+
? `${String(OBS_TABS.length)} tabs · keys 1-9/0 switch Fn, - returns to the live feed`
|
|
2178
|
+
: tf(`{a0} 页 · 按 1-9/0 切 Fn、- 回实时流`, { a0: String(OBS_TABS.length) })
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/** Live subagent activity badges: type → label + dsw token color accent. */
|
|
2182
|
+
const OBS_LIVE_META: Record<string, { label: { zh: string; en: string }; color: string }> = {
|
|
2183
|
+
read: { label: { zh: '阅读', en: 'Read' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2184
|
+
fix: { label: { zh: '修复', en: 'Fix' }, color: 'var(--dsw-alias-state-success-primary)' },
|
|
2185
|
+
rollback: { label: { zh: '回滚', en: 'Rollback' }, color: 'var(--dsw-alias-state-error-primary)' },
|
|
2186
|
+
diff: { label: { zh: '差异', en: 'Diff' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2187
|
+
review: { label: { zh: '审查', en: 'Review' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2188
|
+
triage: { label: { zh: '分流', en: 'Triage' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2189
|
+
checkpoint: { label: { zh: '断点', en: 'Checkpoint' }, color: 'var(--dsw-alias-brand-primary)' },
|
|
2190
|
+
validate: { label: { zh: '校验', en: 'Validate' }, color: 'var(--dsw-alias-state-success-primary)' },
|
|
2191
|
+
log: { label: { zh: '记录', en: 'Log' }, color: 'var(--dsw-alias-label-secondary)' },
|
|
2192
|
+
prune: { label: { zh: '清理', en: 'Prune' }, color: 'var(--dsw-alias-state-warn-primary)' },
|
|
2193
|
+
info: { label: { zh: '信息', en: 'Info' }, color: 'var(--dsw-alias-label-secondary)' },
|
|
2030
2194
|
}
|
|
2031
2195
|
const OBS_LIVE_FALLBACK_COLOR = 'var(--dsw-alias-label-secondary)'
|
|
2032
2196
|
|
|
@@ -2097,6 +2261,9 @@ function buildObsFixInstruction(f: ObsFinding): string {
|
|
|
2097
2261
|
*/
|
|
2098
2262
|
function ObservatoryPanel(props: SlotProps) {
|
|
2099
2263
|
const session = props && props.session ? props.session : null
|
|
2264
|
+
React.useEffect(() => {
|
|
2265
|
+
noteSession(session)
|
|
2266
|
+
}, [session])
|
|
2100
2267
|
const manifest = latestTranscript(session)
|
|
2101
2268
|
// v3.1+: command-center data scanned straight out of the session stream.
|
|
2102
2269
|
const qualityGate = latestQualityGate(session)
|
|
@@ -2183,7 +2350,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2183
2350
|
// the one instruction that makes the model echo them into the session
|
|
2184
2351
|
// stream, which is what every panel scans.
|
|
2185
2352
|
const pullInstruction = buildDiskSnapshotInstruction()
|
|
2186
|
-
const pullTitle = `复制拉取磁盘快照指令:让模型逐个调用
|
|
2353
|
+
const pullTitle = tf(`复制拉取磁盘快照指令:让模型逐个调用 {a0} 并回显完整结果`, { a0: DISK_SNAPSHOT_SOURCES.map((s) => s.tool).join(' / ') })
|
|
2187
2354
|
|
|
2188
2355
|
/** Toggle one thread; a manual toggle exits any forced all/none mode. */
|
|
2189
2356
|
const toggleThread = (key: string) => {
|
|
@@ -2240,16 +2407,16 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2240
2407
|
if (!manifest) {
|
|
2241
2408
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
2242
2409
|
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' }, `暂无运行时观测数据 ·
|
|
2410
|
+
React.createElement('span', { className: 'iterate-obs-title' }, t('iterate 观测台')),
|
|
2411
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`暂无运行时观测数据 · {a0}`, { a0: obsHotkeyHint() })),
|
|
2245
2412
|
React.createElement('button', {
|
|
2246
2413
|
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
2247
2414
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
2248
2415
|
e.stopPropagation()
|
|
2249
2416
|
copyInstruction('pull-snapshot', pullInstruction)
|
|
2250
2417
|
},
|
|
2251
|
-
title: '复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充',
|
|
2252
|
-
}, copiedKey === 'pull-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
2418
|
+
title: t('复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充'),
|
|
2419
|
+
}, copiedKey === 'pull-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2253
2420
|
),
|
|
2254
2421
|
)
|
|
2255
2422
|
}
|
|
@@ -2275,49 +2442,49 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2275
2442
|
const renderThreads = () => {
|
|
2276
2443
|
const rounds = manifest.rounds || []
|
|
2277
2444
|
if (rounds.length === 0) {
|
|
2278
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无审查线程')
|
|
2445
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无审查线程'))
|
|
2279
2446
|
}
|
|
2280
2447
|
return React.createElement('div', {},
|
|
2281
2448
|
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: '收起全部线程' }, '全部收起'),
|
|
2449
|
+
React.createElement('b', {}, t('审查线程')),
|
|
2450
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('all'), title: t('展开全部线程') }, t('全部展开')),
|
|
2451
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setAllThreads('none'), title: t('收起全部线程') }, t('全部收起')),
|
|
2285
2452
|
React.createElement('span', { className: 'iterate-filter-count' },
|
|
2286
|
-
|
|
2453
|
+
tf(`{a0} · {a1} 轮`, { a0: threadMode === 'all' ? t('全部展开') : threadMode === 'none' ? t('全部收起') : t('自由切换'), a1: rounds.length }),
|
|
2287
2454
|
),
|
|
2288
2455
|
),
|
|
2289
2456
|
...rounds.map((r, ri) => {
|
|
2290
2457
|
const threads = r.threads || []
|
|
2291
2458
|
const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0)
|
|
2292
2459
|
const threadBlocks = threads.length === 0
|
|
2293
|
-
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, '本轮无线程')]
|
|
2294
|
-
: threads.map((
|
|
2460
|
+
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, t('本轮无线程'))]
|
|
2461
|
+
: threads.map((th, ti) => {
|
|
2295
2462
|
const key = `${ri}-${ti}`
|
|
2296
2463
|
const expanded = isThreadExpanded(key)
|
|
2297
|
-
const dim =
|
|
2298
|
-
const tFindings =
|
|
2299
|
-
const files =
|
|
2300
|
-
const msgs =
|
|
2464
|
+
const dim = th.dimension || t('未命名维度')
|
|
2465
|
+
const tFindings = th.findings || []
|
|
2466
|
+
const files = th.readFiles || []
|
|
2467
|
+
const msgs = th.messages || []
|
|
2301
2468
|
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 ? '收起线程' : '展开线程' },
|
|
2469
|
+
React.createElement('div', { className: 'iterate-obs-block-head', 'data-click': '', onClick: () => toggleThread(key), title: expanded ? t('收起线程') : t('展开线程') },
|
|
2303
2470
|
React.createElement('span', {}, expanded ? '−' : '+'),
|
|
2304
2471
|
React.createElement('b', {}, dim),
|
|
2305
|
-
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof
|
|
2472
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof th.attempt === 'number' ? th.attempt : '?'}`),
|
|
2306
2473
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${tFindings.length} findings`),
|
|
2307
2474
|
),
|
|
2308
2475
|
expanded
|
|
2309
2476
|
? React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2310
2477
|
React.createElement('div', { className: 'iterate-obs-bar' },
|
|
2311
|
-
React.createElement('b', {}, '叙述'),
|
|
2312
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2478
|
+
React.createElement('b', {}, t('叙述')),
|
|
2479
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`{a0} 条`, { a0: msgs.length })),
|
|
2313
2480
|
),
|
|
2314
2481
|
...(msgs.length === 0
|
|
2315
|
-
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, '无线索叙述')]
|
|
2482
|
+
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, t('无线索叙述'))]
|
|
2316
2483
|
: msgs.map((m, mi) => React.createElement('div', { key: mi, className: 'iterate-obs-msg' }, String(m)))),
|
|
2317
2484
|
...(files.length > 0
|
|
2318
2485
|
? [
|
|
2319
2486
|
React.createElement('div', { key: 'files-h', className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2320
|
-
React.createElement('b', {}, '读取文件'),
|
|
2487
|
+
React.createElement('b', {}, t('读取文件')),
|
|
2321
2488
|
),
|
|
2322
2489
|
...files.map((f, fi) =>
|
|
2323
2490
|
React.createElement('div', { key: `f${fi}`, className: 'iterate-obs-file' }, String(f)),
|
|
@@ -2325,10 +2492,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2325
2492
|
]
|
|
2326
2493
|
: []),
|
|
2327
2494
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2328
|
-
React.createElement('b', {}, '发现'),
|
|
2495
|
+
React.createElement('b', {}, t('发现')),
|
|
2329
2496
|
),
|
|
2330
2497
|
...(tFindings.length === 0
|
|
2331
|
-
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, '本线程无发现')]
|
|
2498
|
+
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, t('本线程无发现'))]
|
|
2332
2499
|
: tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))),
|
|
2333
2500
|
)
|
|
2334
2501
|
: null,
|
|
@@ -2337,7 +2504,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2337
2504
|
return React.createElement('div', { key: ri, className: 'iterate-obs-block' },
|
|
2338
2505
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2339
2506
|
React.createElement('b', {}, `Round ${typeof r.round === 'number' ? r.round : ri + 1}`),
|
|
2340
|
-
React.createElement('span', { className: 'iterate-obs-chip' },
|
|
2507
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, tf(`{a0} 线程`, { a0: threads.length })),
|
|
2341
2508
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `${fCount} findings`),
|
|
2342
2509
|
),
|
|
2343
2510
|
React.createElement('div', { className: 'iterate-obs-block-body' }, ...threadBlocks),
|
|
@@ -2349,7 +2516,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2349
2516
|
const renderTrend = () => {
|
|
2350
2517
|
const conv = manifest.convergence || []
|
|
2351
2518
|
if (conv.length === 0) {
|
|
2352
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无收敛数据')
|
|
2519
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无收敛数据'))
|
|
2353
2520
|
}
|
|
2354
2521
|
const points = conv.map((n, i) => ({ round: i + 1, count: n }))
|
|
2355
2522
|
// Gap #13 (step ①): single-session cross-round comparison. One row per
|
|
@@ -2364,11 +2531,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2364
2531
|
: React.createElement('table', { className: 'iterate-obs-table' },
|
|
2365
2532
|
React.createElement('thead', {},
|
|
2366
2533
|
React.createElement('tr', {},
|
|
2367
|
-
React.createElement('th', {}, '轮次'),
|
|
2368
|
-
React.createElement('th', {}, '发现'),
|
|
2369
|
-
React.createElement('th', {}, '已修复'),
|
|
2370
|
-
React.createElement('th', {}, '严重度分布'),
|
|
2371
|
-
React.createElement('th', {}, '验证'),
|
|
2534
|
+
React.createElement('th', {}, t('轮次')),
|
|
2535
|
+
React.createElement('th', {}, t('发现')),
|
|
2536
|
+
React.createElement('th', {}, t('已修复')),
|
|
2537
|
+
React.createElement('th', {}, t('严重度分布')),
|
|
2538
|
+
React.createElement('th', {}, t('验证')),
|
|
2372
2539
|
),
|
|
2373
2540
|
),
|
|
2374
2541
|
React.createElement('tbody', {},
|
|
@@ -2379,7 +2546,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2379
2546
|
const anyFailed = row.validations.some((v) => v.allowed !== true)
|
|
2380
2547
|
const valText = row.validations.length === 0
|
|
2381
2548
|
? '—'
|
|
2382
|
-
:
|
|
2549
|
+
: tf(`{a0}/{a1} 通过{a2}`, { a0: row.validations.filter((v) => v.allowed === true).length, a1: row.validations.length, a2: anyFailed ? t('(含失败)') : '' })
|
|
2383
2550
|
return React.createElement('tr', { key: `cmp-${row.round}` },
|
|
2384
2551
|
React.createElement('td', {}, `Round ${row.round}`),
|
|
2385
2552
|
React.createElement('td', {}, String(row.findings)),
|
|
@@ -2399,14 +2566,14 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2399
2566
|
return React.createElement('div', {},
|
|
2400
2567
|
React.createElement(TrendChart, { points }),
|
|
2401
2568
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2402
|
-
|
|
2569
|
+
tf(`各轮发现数量:{a0}{a1}`, { a0: conv.join(' → '), a1: typeof manifest.round === 'number' ? tf(` · 当前 Round {a0}`, { a0: manifest.round }) : '' }),
|
|
2403
2570
|
),
|
|
2404
2571
|
comparisonTable
|
|
2405
2572
|
? React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2406
2573
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2407
|
-
React.createElement('span', {}, '跨轮对比(本会话)'),
|
|
2574
|
+
React.createElement('span', {}, t('跨轮对比(本会话)')),
|
|
2408
2575
|
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2409
|
-
'跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行',
|
|
2576
|
+
t('跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行'),
|
|
2410
2577
|
),
|
|
2411
2578
|
),
|
|
2412
2579
|
React.createElement('div', { className: 'iterate-obs-block-body' }, comparisonTable),
|
|
@@ -2419,7 +2586,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2419
2586
|
const renderFindings = () => {
|
|
2420
2587
|
const findings = manifest.findings || []
|
|
2421
2588
|
if (findings.length === 0) {
|
|
2422
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
2589
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无发现'))
|
|
2423
2590
|
}
|
|
2424
2591
|
const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
|
|
2425
2592
|
const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
|
|
@@ -2433,26 +2600,26 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2433
2600
|
}))
|
|
2434
2601
|
}
|
|
2435
2602
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8, flexWrap: 'wrap' } },
|
|
2436
|
-
React.createElement('b', {}, '发现'),
|
|
2603
|
+
React.createElement('b', {}, t('发现')),
|
|
2437
2604
|
...opts.severities.map((s) =>
|
|
2438
2605
|
React.createElement('button', {
|
|
2439
2606
|
key: `sev-${s.value}`,
|
|
2440
2607
|
className: 'iterate-filter-chip',
|
|
2441
2608
|
'data-active': f3Filter.severities.includes(s.value) ? '' : undefined,
|
|
2442
|
-
title:
|
|
2609
|
+
title: tf(`{a0}({a1} 项)`, { a0: s.value, a1: s.count }),
|
|
2443
2610
|
onClick: () => toggleSeverity(s.value),
|
|
2444
2611
|
}, `${s.value} ${s.count}`),
|
|
2445
2612
|
),
|
|
2446
2613
|
React.createElement('select', {
|
|
2447
2614
|
className: 'iterate-filter-select',
|
|
2448
2615
|
value: f3Filter.dimensions[0] || '',
|
|
2449
|
-
'aria-label': '按维度筛选',
|
|
2616
|
+
'aria-label': t('按维度筛选'),
|
|
2450
2617
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
2451
2618
|
const v = e.target.value
|
|
2452
2619
|
setF3Filter((prev) => ({ ...prev, dimensions: v ? [v] : [] }))
|
|
2453
2620
|
},
|
|
2454
2621
|
},
|
|
2455
|
-
React.createElement('option', { value: '' }, '全部维度'),
|
|
2622
|
+
React.createElement('option', { value: '' }, t('全部维度')),
|
|
2456
2623
|
...opts.dimensions.map((d) =>
|
|
2457
2624
|
React.createElement('option', { key: d.value, value: d.value }, `${d.value} (${d.count})`),
|
|
2458
2625
|
),
|
|
@@ -2460,8 +2627,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2460
2627
|
React.createElement('input', {
|
|
2461
2628
|
className: 'iterate-filter-search',
|
|
2462
2629
|
type: 'search',
|
|
2463
|
-
placeholder: '搜索发现…',
|
|
2464
|
-
'aria-label': '搜索发现',
|
|
2630
|
+
placeholder: t('搜索发现…'),
|
|
2631
|
+
'aria-label': t('搜索发现'),
|
|
2465
2632
|
value: f3Filter.search,
|
|
2466
2633
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
2467
2634
|
setF3Filter((prev) => ({ ...prev, search: e.target.value })),
|
|
@@ -2470,8 +2637,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2470
2637
|
? React.createElement('button', {
|
|
2471
2638
|
className: 'iterate-filter-clear',
|
|
2472
2639
|
onClick: () => setF3Filter({ severities: [], dimensions: [], search: '' }),
|
|
2473
|
-
title: '清除全部筛选',
|
|
2474
|
-
}, '清除筛选')
|
|
2640
|
+
title: t('清除全部筛选'),
|
|
2641
|
+
}, t('清除筛选'))
|
|
2475
2642
|
: null,
|
|
2476
2643
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${findings.length}`),
|
|
2477
2644
|
)
|
|
@@ -2494,7 +2661,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2494
2661
|
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
2495
2662
|
React.createElement('span', {}, String(finding.dimension || '')),
|
|
2496
2663
|
finding.acknowledged === true
|
|
2497
|
-
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
2664
|
+
? React.createElement('span', { className: 'iterate-obs-chip' }, t('已确认'))
|
|
2498
2665
|
: null,
|
|
2499
2666
|
),
|
|
2500
2667
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2503,18 +2670,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2503
2670
|
React.createElement('button', {
|
|
2504
2671
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
2505
2672
|
onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
|
|
2506
|
-
title: '复制该 finding 的建议修复文本',
|
|
2507
|
-
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
2673
|
+
title: t('复制该 finding 的建议修复文本'),
|
|
2674
|
+
}, copiedKey === `${k}-fix` ? t('已复制') : t('复制 suggested_fix')),
|
|
2508
2675
|
React.createElement('button', {
|
|
2509
2676
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-known` ? '' : undefined,
|
|
2510
2677
|
onClick: () => copyInstruction(`${k}-known`, knownInstruction),
|
|
2511
|
-
title: '复制 iterate_triage 指令文本(标记为 known_intentional)',
|
|
2512
|
-
}, copiedKey === `${k}-known` ? '已复制' : 'known_intentional'),
|
|
2678
|
+
title: t('复制 iterate_triage 指令文本(标记为 known_intentional)'),
|
|
2679
|
+
}, copiedKey === `${k}-known` ? t('已复制') : 'known_intentional'),
|
|
2513
2680
|
React.createElement('button', {
|
|
2514
2681
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `${k}-repair` ? '' : undefined,
|
|
2515
2682
|
onClick: () => copyInstruction(`${k}-repair`, fixInstruction),
|
|
2516
|
-
title: '复制 iterate_fix 指令文本',
|
|
2517
|
-
}, copiedKey === `${k}-repair` ? '已复制' : '复制修复指令'),
|
|
2683
|
+
title: t('复制 iterate_fix 指令文本'),
|
|
2684
|
+
}, copiedKey === `${k}-repair` ? t('已复制') : t('复制修复指令')),
|
|
2518
2685
|
),
|
|
2519
2686
|
),
|
|
2520
2687
|
)
|
|
@@ -2522,7 +2689,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2522
2689
|
if (filtered.length === 0) {
|
|
2523
2690
|
return React.createElement('div', {},
|
|
2524
2691
|
filterBar,
|
|
2525
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的发现'),
|
|
2692
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的发现')),
|
|
2526
2693
|
)
|
|
2527
2694
|
}
|
|
2528
2695
|
return React.createElement('div', {}, filterBar, ...findingBlocks)
|
|
@@ -2532,7 +2699,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2532
2699
|
const renderFixes = () => {
|
|
2533
2700
|
const fixes = manifest.fixes || []
|
|
2534
2701
|
if (fixes.length === 0) {
|
|
2535
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无修复记录')
|
|
2702
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无修复记录'))
|
|
2536
2703
|
}
|
|
2537
2704
|
return React.createElement('div', {}, ...fixes.map((f, i) => {
|
|
2538
2705
|
const k = `f4-${i}`
|
|
@@ -2544,7 +2711,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2544
2711
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2545
2712
|
React.createElement('b', {}, id),
|
|
2546
2713
|
React.createElement('span', { className: 'iterate-obs-file' }, String(f.file || '?')),
|
|
2547
|
-
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? '成功' : '失败'),
|
|
2714
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? t('成功') : t('失败')),
|
|
2548
2715
|
typeof f.round === 'number'
|
|
2549
2716
|
? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${f.round}`)
|
|
2550
2717
|
: null,
|
|
@@ -2557,8 +2724,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2557
2724
|
React.createElement('button', {
|
|
2558
2725
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === `${k}-rb` ? '' : undefined,
|
|
2559
2726
|
onClick: () => copyInstruction(`${k}-rb`, rollbackText),
|
|
2560
|
-
title: '复制 iterate_rollback 指令文本',
|
|
2561
|
-
}, copiedKey === `${k}-rb` ? '已复制' : '回滚'),
|
|
2727
|
+
title: t('复制 iterate_rollback 指令文本'),
|
|
2728
|
+
}, copiedKey === `${k}-rb` ? t('已复制') : t('回滚')),
|
|
2562
2729
|
),
|
|
2563
2730
|
),
|
|
2564
2731
|
)
|
|
@@ -2570,7 +2737,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2570
2737
|
const cp = manifest.checkpoint
|
|
2571
2738
|
if (!cp) {
|
|
2572
2739
|
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2573
|
-
diskEmptyStateText(toolCalledInSession(session, 'iterate_transcript'), '断点'),
|
|
2740
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_transcript'), t('断点'), getLang()),
|
|
2574
2741
|
)
|
|
2575
2742
|
}
|
|
2576
2743
|
// Gap #10: `resume` accepts ONLY `operation` — `mode` / `round` /
|
|
@@ -2586,7 +2753,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2586
2753
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
2587
2754
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2588
2755
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2589
|
-
React.createElement('b', {}, '断点'),
|
|
2756
|
+
React.createElement('b', {}, t('断点')),
|
|
2590
2757
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, String(cp.updatedAt || '')),
|
|
2591
2758
|
),
|
|
2592
2759
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2601,13 +2768,13 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2601
2768
|
React.createElement('button', {
|
|
2602
2769
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'cp-resume' ? '' : undefined,
|
|
2603
2770
|
onClick: () => copyInstruction('cp-resume', resumeText),
|
|
2604
|
-
title: '复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)',
|
|
2605
|
-
}, copiedKey === 'cp-resume' ? '已复制' : '复制恢复指令'),
|
|
2771
|
+
title: t('复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)'),
|
|
2772
|
+
}, copiedKey === 'cp-resume' ? t('已复制') : t('复制恢复指令')),
|
|
2606
2773
|
React.createElement('button', {
|
|
2607
2774
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'cp-clear' ? '' : undefined,
|
|
2608
2775
|
onClick: () => copyInstruction('cp-clear', clearText),
|
|
2609
|
-
title: '复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)',
|
|
2610
|
-
}, copiedKey === 'cp-clear' ? '已复制' : '清除断点'),
|
|
2776
|
+
title: t('复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)'),
|
|
2777
|
+
}, copiedKey === 'cp-clear' ? t('已复制') : t('清除断点')),
|
|
2611
2778
|
),
|
|
2612
2779
|
),
|
|
2613
2780
|
)
|
|
@@ -2616,16 +2783,16 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2616
2783
|
// ── F6: run console (status / approval / nudge draft) ──────────────────
|
|
2617
2784
|
const renderConsole = () => {
|
|
2618
2785
|
const approval = manifest.approval || { policy: 'ask' }
|
|
2619
|
-
const policyLabelMap: Record<string, string> = { ask: '询问用户', deny: '拒绝', allow: '直接执行' }
|
|
2786
|
+
const policyLabelMap: Record<string, string> = { ask: t('询问用户'), deny: t('拒绝'), allow: t('直接执行') }
|
|
2620
2787
|
const policyLabel = policyLabelMap[String(approval.policy || 'ask')] || String(approval.policy || 'ask')
|
|
2621
2788
|
const nudge = manifest.nudge || null
|
|
2622
2789
|
const present = Boolean(nudge && nudge.text)
|
|
2623
2790
|
const activeNudgeText = nudge && nudge.text ? nudge.text : ''
|
|
2624
|
-
const nudgeInstruction = `请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n
|
|
2791
|
+
const nudgeInstruction = tf(`请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'nudge', text: nudgeText }, null, 2) })
|
|
2625
2792
|
// Clearing the PERSISTED nudge (as opposed to the draft) requires
|
|
2626
2793
|
// text:null. Copied as a paste-able instruction, mirroring the panel's
|
|
2627
2794
|
// copy-to-command pattern — it cannot be cleared from the client directly.
|
|
2628
|
-
const activeClearInstruction = `请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n
|
|
2795
|
+
const activeClearInstruction = tf(`请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'nudge', text: null }, null, 2) })
|
|
2629
2796
|
// Gap #6: per-round validation rows (command / exitCode / allowed) live on
|
|
2630
2797
|
// the manifest; when the run stopped on a validation failure the failing
|
|
2631
2798
|
// command is called out explicitly instead of being buried in a list.
|
|
@@ -2646,54 +2813,54 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2646
2813
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${typeof v.round === 'number' ? v.round : '?'}`),
|
|
2647
2814
|
React.createElement('code', { className: 'iterate-obs-mono' }, String(v.command || '')),
|
|
2648
2815
|
React.createElement('span', { className: 'iterate-obs-chip', 'data-status': allowed ? 'pass' : 'fail' },
|
|
2649
|
-
allowed ? '通过' : '失败',
|
|
2816
|
+
allowed ? t('通过') : t('失败'),
|
|
2650
2817
|
),
|
|
2651
2818
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, exit),
|
|
2652
|
-
highlight ? React.createElement('span', { className: 'iterate-obs-chip', 'data-status': 'fail' }, '导致中止') : null,
|
|
2819
|
+
highlight ? React.createElement('span', { className: 'iterate-obs-chip', 'data-status': 'fail' }, t('导致中止')) : null,
|
|
2653
2820
|
)
|
|
2654
2821
|
})
|
|
2655
2822
|
const validationsBlock = React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2656
2823
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2657
|
-
React.createElement('span', {}, '验证结果'),
|
|
2824
|
+
React.createElement('span', {}, t('验证结果')),
|
|
2658
2825
|
validations.length === 0
|
|
2659
|
-
? React.createElement('span', { className: 'iterate-obs-head-meta' }, '本轮尚无验证记录')
|
|
2660
|
-
: React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2826
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, t('本轮尚无验证记录'))
|
|
2827
|
+
: React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`{a0} 条命令`, { a0: validations.length })),
|
|
2661
2828
|
abortedByValidation
|
|
2662
|
-
? React.createElement('span', { className: 'iterate-obs-badge', 'data-status': 'fail' }, '因验证失败中止')
|
|
2829
|
+
? React.createElement('span', { className: 'iterate-obs-badge', 'data-status': 'fail' }, t('因验证失败中止'))
|
|
2663
2830
|
: null,
|
|
2664
2831
|
),
|
|
2665
2832
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2666
2833
|
validations.length === 0
|
|
2667
2834
|
? React.createElement('div', { className: 'iterate-obs-empty' },
|
|
2668
|
-
'尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。',
|
|
2835
|
+
t('尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。'),
|
|
2669
2836
|
)
|
|
2670
2837
|
: React.createElement(React.Fragment, null, ...validationRows),
|
|
2671
2838
|
),
|
|
2672
2839
|
)
|
|
2673
2840
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2674
2841
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2675
|
-
React.createElement('span', {}, '运行控制台'),
|
|
2842
|
+
React.createElement('span', {}, t('运行控制台')),
|
|
2676
2843
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2677
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2844
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`批准策略:{a0}`, { a0: policyLabel })),
|
|
2678
2845
|
),
|
|
2679
2846
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2680
2847
|
present
|
|
2681
2848
|
? React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 6 } },
|
|
2682
|
-
React.createElement('b', {}, '当前 nudge'),
|
|
2849
|
+
React.createElement('b', {}, t('当前 nudge')),
|
|
2683
2850
|
React.createElement('span', { className: 'iterate-obs-msg' }, activeNudgeText),
|
|
2684
2851
|
React.createElement('button', {
|
|
2685
2852
|
className: 'iterate-btn', 'data-copied': copiedKey === 'nudge-clear' ? '' : undefined,
|
|
2686
2853
|
onClick: () => copyInstruction('nudge-clear', activeClearInstruction),
|
|
2687
|
-
title: '复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge',
|
|
2688
|
-
}, copiedKey === 'nudge-clear' ? '已复制清除指令' : '复制清除指令'),
|
|
2854
|
+
title: t('复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge'),
|
|
2855
|
+
}, copiedKey === 'nudge-clear' ? t('已复制清除指令') : t('复制清除指令')),
|
|
2689
2856
|
)
|
|
2690
2857
|
: null,
|
|
2691
2858
|
React.createElement('textarea', {
|
|
2692
2859
|
className: 'iterate-obs-input iterate-obs-mono',
|
|
2693
2860
|
rows: 3,
|
|
2694
|
-
placeholder: '起草一条 nudge 方向指令…',
|
|
2861
|
+
placeholder: t('起草一条 nudge 方向指令…'),
|
|
2695
2862
|
value: nudgeText,
|
|
2696
|
-
'aria-label': 'nudge 草稿',
|
|
2863
|
+
'aria-label': t('nudge 草稿'),
|
|
2697
2864
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setNudgeText(e.target.value),
|
|
2698
2865
|
}),
|
|
2699
2866
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
@@ -2701,8 +2868,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2701
2868
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'nudge' ? '' : undefined,
|
|
2702
2869
|
disabled: !nudgeText,
|
|
2703
2870
|
onClick: () => copyInstruction('nudge', nudgeInstruction),
|
|
2704
|
-
title: '复制 iterate_transcript nudge 指令文本',
|
|
2705
|
-
}, copiedKey === 'nudge' ? '已复制' : '复制 nudge 指令'),
|
|
2871
|
+
title: t('复制 iterate_transcript nudge 指令文本'),
|
|
2872
|
+
}, copiedKey === 'nudge' ? t('已复制') : t('复制 nudge 指令')),
|
|
2706
2873
|
),
|
|
2707
2874
|
validationsBlock,
|
|
2708
2875
|
),
|
|
@@ -2713,7 +2880,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2713
2880
|
const renderTimeline = () => {
|
|
2714
2881
|
const entries = manifest.timeline || []
|
|
2715
2882
|
if (entries.length === 0) {
|
|
2716
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无时间线条目')
|
|
2883
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无时间线条目'))
|
|
2717
2884
|
}
|
|
2718
2885
|
const types = Array.from(new Set(entries.map((t) => String(t.type || 'unknown')).filter(Boolean))).sort()
|
|
2719
2886
|
const rounds = Array.from(
|
|
@@ -2749,30 +2916,30 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2749
2916
|
)
|
|
2750
2917
|
})
|
|
2751
2918
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2752
|
-
React.createElement('b', {}, '时间线'),
|
|
2919
|
+
React.createElement('b', {}, t('时间线')),
|
|
2753
2920
|
React.createElement('select', {
|
|
2754
2921
|
className: 'iterate-filter-select',
|
|
2755
2922
|
value: timelineType,
|
|
2756
|
-
'aria-label': '按时间线类型筛选',
|
|
2923
|
+
'aria-label': t('按时间线类型筛选'),
|
|
2757
2924
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineType(e.target.value),
|
|
2758
2925
|
},
|
|
2759
|
-
React.createElement('option', { value: '' }, '全部类型'),
|
|
2926
|
+
React.createElement('option', { value: '' }, t('全部类型')),
|
|
2760
2927
|
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
2761
2928
|
),
|
|
2762
2929
|
React.createElement('select', {
|
|
2763
2930
|
className: 'iterate-filter-select',
|
|
2764
2931
|
value: timelineRound,
|
|
2765
|
-
'aria-label': '按轮次筛选',
|
|
2932
|
+
'aria-label': t('按轮次筛选'),
|
|
2766
2933
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineRound(e.target.value),
|
|
2767
2934
|
},
|
|
2768
|
-
React.createElement('option', { value: '' }, '全部轮次'),
|
|
2935
|
+
React.createElement('option', { value: '' }, t('全部轮次')),
|
|
2769
2936
|
...rounds.map((r) => React.createElement('option', { key: r, value: r }, `Round ${r}`)),
|
|
2770
2937
|
),
|
|
2771
2938
|
React.createElement('input', {
|
|
2772
2939
|
className: 'iterate-filter-search',
|
|
2773
2940
|
type: 'search',
|
|
2774
|
-
placeholder: '搜索时间线…',
|
|
2775
|
-
'aria-label': '搜索时间线',
|
|
2941
|
+
placeholder: t('搜索时间线…'),
|
|
2942
|
+
'aria-label': t('搜索时间线'),
|
|
2776
2943
|
value: timelineSearch,
|
|
2777
2944
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setTimelineSearch(e.target.value),
|
|
2778
2945
|
}),
|
|
@@ -2780,15 +2947,15 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2780
2947
|
? React.createElement('button', {
|
|
2781
2948
|
className: 'iterate-filter-clear',
|
|
2782
2949
|
onClick: clearTimelineFilter,
|
|
2783
|
-
title: '清除全部筛选',
|
|
2784
|
-
}, '清除')
|
|
2950
|
+
title: t('清除全部筛选'),
|
|
2951
|
+
}, t('清除'))
|
|
2785
2952
|
: null,
|
|
2786
2953
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${entries.length}`),
|
|
2787
2954
|
)
|
|
2788
2955
|
if (rows.length === 0) {
|
|
2789
2956
|
return React.createElement('div', {},
|
|
2790
2957
|
filterBar,
|
|
2791
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的时间线条目'),
|
|
2958
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的时间线条目')),
|
|
2792
2959
|
)
|
|
2793
2960
|
}
|
|
2794
2961
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
@@ -2815,24 +2982,24 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2815
2982
|
const pulled = toolCalledInSession(session, 'iterate_quality_gate')
|
|
2816
2983
|
|
|
2817
2984
|
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2818
|
-
React.createElement('b', {}, '质量门禁'),
|
|
2985
|
+
React.createElement('b', {}, t('质量门禁')),
|
|
2819
2986
|
React.createElement('button', {
|
|
2820
2987
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2821
2988
|
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2822
|
-
title: '复制 iterate_quality_gate 查询指令(含 JSON 参数)',
|
|
2823
|
-
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2989
|
+
title: t('复制 iterate_quality_gate 查询指令(含 JSON 参数)'),
|
|
2990
|
+
}, copiedKey === 'qgate' ? t('已复制') : t('查询门禁')),
|
|
2824
2991
|
React.createElement('button', {
|
|
2825
2992
|
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
|
|
2826
2993
|
onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
|
|
2827
|
-
title: '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)',
|
|
2828
|
-
}, copiedKey === 'qgate-clear' ? '已复制' : '清除门禁'),
|
|
2994
|
+
title: t('复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)'),
|
|
2995
|
+
}, copiedKey === 'qgate-clear' ? t('已复制') : t('清除门禁')),
|
|
2829
2996
|
// Gap #4: the browser cannot read `.iterate/` directly — one click
|
|
2830
2997
|
// copies the pull instruction that makes the model echo it into the stream.
|
|
2831
2998
|
React.createElement('button', {
|
|
2832
2999
|
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot' ? '' : undefined,
|
|
2833
3000
|
onClick: () => copyInstruction('disk-snapshot', pullInstruction),
|
|
2834
|
-
title: '复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript',
|
|
2835
|
-
}, copiedKey === 'disk-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3001
|
+
title: t('复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript'),
|
|
3002
|
+
}, copiedKey === 'disk-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2836
3003
|
)
|
|
2837
3004
|
|
|
2838
3005
|
if (!hasGate) {
|
|
@@ -2840,29 +3007,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2840
3007
|
headerBar,
|
|
2841
3008
|
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2842
3009
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2843
|
-
React.createElement('span', {}, '质量门禁视图'),
|
|
3010
|
+
React.createElement('span', {}, t('质量门禁视图')),
|
|
2844
3011
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2845
3012
|
),
|
|
2846
3013
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2847
3014
|
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
2848
3015
|
// Gap #4: distinguish "we pulled it and it is empty" from "we
|
|
2849
3016
|
// never asked the model to read the disk".
|
|
2850
|
-
diskEmptyStateText(pulled, '质量门禁证书'),
|
|
3017
|
+
diskEmptyStateText(pulled, t('质量门禁证书'), getLang()),
|
|
2851
3018
|
),
|
|
2852
3019
|
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
2853
|
-
'质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。',
|
|
3020
|
+
t('质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
|
|
2854
3021
|
),
|
|
2855
3022
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2856
3023
|
React.createElement('button', {
|
|
2857
3024
|
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2858
3025
|
onClick: () => copyInstruction('qgate-instr', gateInstruction),
|
|
2859
|
-
title: '复制查询指令',
|
|
2860
|
-
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
3026
|
+
title: t('复制查询指令'),
|
|
3027
|
+
}, copiedKey === 'qgate-instr' ? t('已复制') : t('复制查询指令')),
|
|
2861
3028
|
React.createElement('button', {
|
|
2862
3029
|
className: 'iterate-btn', 'data-copied': copiedKey === 'disk-snapshot-2' ? '' : undefined,
|
|
2863
3030
|
onClick: () => copyInstruction('disk-snapshot-2', pullInstruction),
|
|
2864
|
-
title: '复制拉取磁盘快照指令',
|
|
2865
|
-
}, copiedKey === 'disk-snapshot-2' ? '已复制' : '拉取磁盘快照'),
|
|
3031
|
+
title: t('复制拉取磁盘快照指令'),
|
|
3032
|
+
}, copiedKey === 'disk-snapshot-2' ? t('已复制') : t('拉取磁盘快照')),
|
|
2866
3033
|
),
|
|
2867
3034
|
),
|
|
2868
3035
|
),
|
|
@@ -2875,7 +3042,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2875
3042
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2876
3043
|
React.createElement('span', { className: 'iterate-gate', 'data-status': status }, statusLabel),
|
|
2877
3044
|
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2878
|
-
`整体评分
|
|
3045
|
+
tf(`整体评分 {a0} · 验证 {a1}/{a2} · 通过率 {a3}%`, { a0: num(gate && gate.overallScore), a1: num(gate && gate.passedChecks), a2: num(gate && gate.totalChecks), a3: num(gate && gate.verificationPassRate) }),
|
|
2879
3046
|
),
|
|
2880
3047
|
),
|
|
2881
3048
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2883,7 +3050,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2883
3050
|
? React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6 } },
|
|
2884
3051
|
...dims.map((d, i) =>
|
|
2885
3052
|
React.createElement('span', { key: `gdim-${i}`, className: 'iterate-obs-chip' },
|
|
2886
|
-
|
|
3053
|
+
tf(`{a0} · 收敛 {a1}% · 评分 {a2}`, { a0: String(d.dimension || '?'), a1: num(d.convergenceRate), a2: num(d.score) }),
|
|
2887
3054
|
),
|
|
2888
3055
|
),
|
|
2889
3056
|
)
|
|
@@ -2900,12 +3067,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2900
3067
|
style: { width: `${fill}%` },
|
|
2901
3068
|
}),
|
|
2902
3069
|
),
|
|
2903
|
-
React.createElement('span', {}, `评分
|
|
3070
|
+
React.createElement('span', {}, tf(`评分 {a0} · 发现 {a1} · 已修复 {a2}`, { a0: num(d.score), a1: num(d.findingsCount), a2: num(d.fixedCount) })),
|
|
2904
3071
|
)
|
|
2905
3072
|
}),
|
|
2906
3073
|
),
|
|
2907
3074
|
gate && gate.failReason
|
|
2908
|
-
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3075
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } }, tf(`失败原因:{a0}`, { a0: String(gate.failReason) }))
|
|
2909
3076
|
: null,
|
|
2910
3077
|
),
|
|
2911
3078
|
),
|
|
@@ -2918,7 +3085,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2918
3085
|
// (list all / search / adopt one entry).
|
|
2919
3086
|
const renderExperienceBank = () => {
|
|
2920
3087
|
const expInstruction = expSearch
|
|
2921
|
-
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n
|
|
3088
|
+
? tf(`请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'search', query: expSearch }, null, 2) })
|
|
2922
3089
|
: buildExperienceListInstruction()
|
|
2923
3090
|
|
|
2924
3091
|
const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
|
|
@@ -2933,12 +3100,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2933
3100
|
const pulled = toolCalledInSession(session, 'iterate_experience')
|
|
2934
3101
|
|
|
2935
3102
|
const resultRows = visible.map((e, i) => {
|
|
2936
|
-
const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n
|
|
3103
|
+
const adoptInstruction = tf(`请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'get', id: e.id }, null, 2) })
|
|
2937
3104
|
return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-block' },
|
|
2938
3105
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2939
3106
|
React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
|
|
2940
3107
|
React.createElement('span', {}, String(e.pattern || '?')),
|
|
2941
|
-
React.createElement('span', { className: 'iterate-exp-hit' }, `命中
|
|
3108
|
+
React.createElement('span', { className: 'iterate-exp-hit' }, tf(`命中 {a0} 次`, { a0: e.hitCount ?? 0 })),
|
|
2942
3109
|
),
|
|
2943
3110
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2944
3111
|
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
@@ -2947,8 +3114,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2947
3114
|
? React.createElement('button', {
|
|
2948
3115
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `exp-adopt-${i}` ? '' : undefined,
|
|
2949
3116
|
onClick: () => copyInstruction(`exp-adopt-${i}`, adoptInstruction),
|
|
2950
|
-
title: '复制采纳指令(查询该经验并应用到当前上下文)',
|
|
2951
|
-
}, copiedKey === `exp-adopt-${i}` ? '已复制' : '采纳')
|
|
3117
|
+
title: t('复制采纳指令(查询该经验并应用到当前上下文)'),
|
|
3118
|
+
}, copiedKey === `exp-adopt-${i}` ? t('已复制') : t('采纳'))
|
|
2952
3119
|
: null,
|
|
2953
3120
|
),
|
|
2954
3121
|
),
|
|
@@ -2957,25 +3124,25 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2957
3124
|
|
|
2958
3125
|
return React.createElement('div', {},
|
|
2959
3126
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2960
|
-
React.createElement('b', {}, '经验银行'),
|
|
2961
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内
|
|
3127
|
+
React.createElement('b', {}, t('经验银行')),
|
|
3128
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`会话内 {a0} 条`, { a0: expEntries.length })),
|
|
2962
3129
|
React.createElement('button', {
|
|
2963
3130
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2964
3131
|
onClick: () => copyInstruction('exp-list', buildExperienceListInstruction()),
|
|
2965
|
-
title: '复制列出经验指令(含 JSON 参数)',
|
|
2966
|
-
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
3132
|
+
title: t('复制列出经验指令(含 JSON 参数)'),
|
|
3133
|
+
}, copiedKey === 'exp-list' ? t('已复制') : t('列出经验')),
|
|
2967
3134
|
React.createElement('button', {
|
|
2968
3135
|
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-snapshot' ? '' : undefined,
|
|
2969
3136
|
onClick: () => copyInstruction('exp-snapshot', pullInstruction),
|
|
2970
|
-
title: '复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话',
|
|
2971
|
-
}, copiedKey === 'exp-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3137
|
+
title: t('复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话'),
|
|
3138
|
+
}, copiedKey === 'exp-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
2972
3139
|
),
|
|
2973
3140
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2974
3141
|
React.createElement('input', {
|
|
2975
3142
|
className: 'iterate-filter-search',
|
|
2976
3143
|
type: 'search',
|
|
2977
|
-
placeholder: '搜索经验模式…',
|
|
2978
|
-
'aria-label': '搜索经验',
|
|
3144
|
+
placeholder: t('搜索经验模式…'),
|
|
3145
|
+
'aria-label': t('搜索经验'),
|
|
2979
3146
|
value: expSearch,
|
|
2980
3147
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
|
|
2981
3148
|
}),
|
|
@@ -2983,17 +3150,17 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2983
3150
|
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
|
|
2984
3151
|
disabled: !expSearch,
|
|
2985
3152
|
onClick: () => copyInstruction('exp-search', expInstruction),
|
|
2986
|
-
title: '复制搜索指令',
|
|
2987
|
-
}, copiedKey === 'exp-search' ? '已复制' : '搜索'),
|
|
3153
|
+
title: t('复制搜索指令'),
|
|
3154
|
+
}, copiedKey === 'exp-search' ? t('已复制') : t('搜索')),
|
|
2988
3155
|
),
|
|
2989
3156
|
resultRows.length > 0
|
|
2990
3157
|
? React.createElement('div', {}, ...resultRows)
|
|
2991
3158
|
: expEntries.length > 0
|
|
2992
|
-
? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
|
|
3159
|
+
? React.createElement('div', { className: 'iterate-obs-empty' }, t('没有匹配「') + expSearch + t('」的经验'))
|
|
2993
3160
|
: React.createElement('div', {},
|
|
2994
3161
|
// 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' }, '也可点「列出经验」查询磁盘上的全部历史经验。'),
|
|
3162
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, diskEmptyStateText(pulled, t('经验银行'), getLang())),
|
|
3163
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('也可点「列出经验」查询磁盘上的全部历史经验。')),
|
|
2997
3164
|
),
|
|
2998
3165
|
)
|
|
2999
3166
|
}
|
|
@@ -3004,21 +3171,21 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3004
3171
|
// falsifications); copy buttons reach the back end for the full history.
|
|
3005
3172
|
const renderDefenseEvents = () => {
|
|
3006
3173
|
const defenseInstruction = defenseFilter
|
|
3007
|
-
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n
|
|
3174
|
+
? tf(`请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2) })
|
|
3008
3175
|
: buildDefenseEventsListInstruction()
|
|
3009
3176
|
|
|
3010
3177
|
const typeOptions = [
|
|
3011
|
-
{ value: '', label: '全部类型' },
|
|
3012
|
-
{ value: 'precondition_failed', label: '前置校验失败' },
|
|
3013
|
-
{ value: 'rollback', label: '回滚' },
|
|
3014
|
-
{ value: 'invariant_violated', label: '不变量违反' },
|
|
3015
|
-
{ value: 'assumption_falsified', label: '假设被证伪' },
|
|
3178
|
+
{ value: '', label: t('全部类型') },
|
|
3179
|
+
{ value: 'precondition_failed', label: t('前置校验失败') },
|
|
3180
|
+
{ value: 'rollback', label: t('回滚') },
|
|
3181
|
+
{ value: 'invariant_violated', label: t('不变量违反') },
|
|
3182
|
+
{ value: 'assumption_falsified', label: t('假设被证伪') },
|
|
3016
3183
|
]
|
|
3017
3184
|
const TYPE_LABEL: Record<string, string> = {
|
|
3018
|
-
precondition_failed: '前置校验失败',
|
|
3019
|
-
rollback: '回滚',
|
|
3020
|
-
invariant_violated: '不变量违反',
|
|
3021
|
-
assumption_falsified: '假设被证伪',
|
|
3185
|
+
precondition_failed: t('前置校验失败'),
|
|
3186
|
+
rollback: t('回滚'),
|
|
3187
|
+
invariant_violated: t('不变量违反'),
|
|
3188
|
+
assumption_falsified: t('假设被证伪'),
|
|
3022
3189
|
}
|
|
3023
3190
|
|
|
3024
3191
|
const counts = (defenseEvents && defenseEvents.counts) || {}
|
|
@@ -3029,28 +3196,28 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3029
3196
|
: allEvents
|
|
3030
3197
|
|
|
3031
3198
|
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' }, `会话内
|
|
3199
|
+
React.createElement('b', {}, t('防御事件')),
|
|
3200
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`会话内 {a0} 条 / 累计 {a1}`, { a0: allEvents.length, a1: totalCount })),
|
|
3034
3201
|
React.createElement('button', {
|
|
3035
3202
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
3036
3203
|
onClick: () => copyInstruction('defense-list', buildDefenseEventsListInstruction()),
|
|
3037
|
-
title: '复制列出防御事件指令(含 JSON 参数)',
|
|
3038
|
-
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
3204
|
+
title: t('复制列出防御事件指令(含 JSON 参数)'),
|
|
3205
|
+
}, copiedKey === 'defense-list' ? t('已复制') : t('列出事件')),
|
|
3039
3206
|
React.createElement('button', {
|
|
3040
3207
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-snapshot' ? '' : undefined,
|
|
3041
3208
|
onClick: () => copyInstruction('defense-snapshot', pullInstruction),
|
|
3042
|
-
title: '复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话',
|
|
3043
|
-
}, copiedKey === 'defense-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3209
|
+
title: t('复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话'),
|
|
3210
|
+
}, copiedKey === 'defense-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
3044
3211
|
React.createElement('button', {
|
|
3045
3212
|
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' ? '已复制' : '统计'),
|
|
3213
|
+
onClick: () => copyInstruction('defense-counts', tf(`请调用 \`iterate_defense_events\` 查询事件统计:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'counts' }, null, 2) })),
|
|
3214
|
+
title: t('复制统计指令(含 JSON 参数)'),
|
|
3215
|
+
}, copiedKey === 'defense-counts' ? t('已复制') : t('统计')),
|
|
3049
3216
|
React.createElement('button', {
|
|
3050
3217
|
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' ? '已复制' : '清除事件'),
|
|
3218
|
+
onClick: () => copyInstruction('defense-clear', tf(`请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n{a0}\n\`\`\``, { a0: JSON.stringify({ operation: 'clear' }, null, 2) })),
|
|
3219
|
+
title: t('复制 iterate_defense_events clear 指令文本(重置陈旧事件流)'),
|
|
3220
|
+
}, copiedKey === 'defense-clear' ? t('已复制') : t('清除事件')),
|
|
3054
3221
|
)
|
|
3055
3222
|
|
|
3056
3223
|
if (allEvents.length === 0 && totalCount === 0) {
|
|
@@ -3058,22 +3225,22 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3058
3225
|
headerBar,
|
|
3059
3226
|
React.createElement('div', { className: 'iterate-obs-block' },
|
|
3060
3227
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3061
|
-
React.createElement('span', {}, '防御事件流'),
|
|
3228
|
+
React.createElement('span', {}, t('防御事件流')),
|
|
3062
3229
|
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
3063
3230
|
),
|
|
3064
3231
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3065
3232
|
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3066
|
-
diskEmptyStateText(toolCalledInSession(session, 'iterate_defense_events'), '防御事件流'),
|
|
3233
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_defense_events'), t('防御事件流'), getLang()),
|
|
3067
3234
|
),
|
|
3068
3235
|
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3069
|
-
'防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。',
|
|
3236
|
+
t('防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
|
|
3070
3237
|
),
|
|
3071
3238
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
3072
3239
|
React.createElement('button', {
|
|
3073
3240
|
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
3074
3241
|
onClick: () => copyInstruction('defense-instr', defenseInstruction),
|
|
3075
|
-
title: '复制查询指令',
|
|
3076
|
-
}, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
|
|
3242
|
+
title: t('复制查询指令'),
|
|
3243
|
+
}, copiedKey === 'defense-instr' ? t('已复制') : t('复制查询指令')),
|
|
3077
3244
|
),
|
|
3078
3245
|
),
|
|
3079
3246
|
),
|
|
@@ -3084,7 +3251,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3084
3251
|
React.createElement('select', {
|
|
3085
3252
|
className: 'iterate-filter-select',
|
|
3086
3253
|
value: defenseFilter,
|
|
3087
|
-
'aria-label': '按事件类型筛选',
|
|
3254
|
+
'aria-label': t('按事件类型筛选'),
|
|
3088
3255
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
|
|
3089
3256
|
},
|
|
3090
3257
|
...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
|
|
@@ -3093,8 +3260,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3093
3260
|
? React.createElement('button', {
|
|
3094
3261
|
className: 'iterate-filter-clear',
|
|
3095
3262
|
onClick: () => setDefenseFilter(''),
|
|
3096
|
-
title: '清除类型筛选',
|
|
3097
|
-
}, '清除')
|
|
3263
|
+
title: t('清除类型筛选'),
|
|
3264
|
+
}, t('清除'))
|
|
3098
3265
|
: null,
|
|
3099
3266
|
)
|
|
3100
3267
|
|
|
@@ -3114,7 +3281,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3114
3281
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3115
3282
|
React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
|
|
3116
3283
|
e.defense
|
|
3117
|
-
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } },
|
|
3284
|
+
? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } }, tf(`防线:{a0}`, { a0: String(e.defense) }))
|
|
3118
3285
|
: null,
|
|
3119
3286
|
),
|
|
3120
3287
|
)
|
|
@@ -3133,7 +3300,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3133
3300
|
),
|
|
3134
3301
|
eventRows.length > 0
|
|
3135
3302
|
? React.createElement('div', {}, ...eventRows)
|
|
3136
|
-
: React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配该类型的防御事件'),
|
|
3303
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, t('没有匹配该类型的防御事件')),
|
|
3137
3304
|
)
|
|
3138
3305
|
}
|
|
3139
3306
|
|
|
@@ -3148,25 +3315,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3148
3315
|
const hasFindings = (manifest.findings || []).length > 0
|
|
3149
3316
|
if (hasThreads || hasFindings) {
|
|
3150
3317
|
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
3151
|
-
React.createElement('div', {}, '实时活动流仅在运行期间记录,当前已完成。'),
|
|
3318
|
+
React.createElement('div', {}, t('实时活动流仅在运行期间记录,当前已完成。')),
|
|
3152
3319
|
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') }, '查看发现'),
|
|
3320
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f1') }, t('查看审查线程')),
|
|
3321
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f3') }, t('查看发现')),
|
|
3155
3322
|
),
|
|
3156
3323
|
)
|
|
3157
3324
|
}
|
|
3158
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无实时活动')
|
|
3325
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无实时活动'))
|
|
3159
3326
|
}
|
|
3160
3327
|
const types = Array.from(new Set(liveEntries.map((e) => String(e.type || 'unknown')).filter(Boolean))).sort()
|
|
3161
3328
|
const filtered = filterLiveEntries(liveEntries, liveType)
|
|
3162
3329
|
const rows = filtered.map((e, i) => {
|
|
3163
3330
|
const type = String(e.type || 'unknown')
|
|
3164
|
-
|
|
3331
|
+
// Module constants carry BOTH languages; resolve at render time so a
|
|
3332
|
+
// language switch repaints the badges.
|
|
3333
|
+
const meta = OBS_LIVE_META[type]
|
|
3334
|
+
const typeLabel = meta ? meta.label[getLang()] : String(e.type || '?')
|
|
3335
|
+
const typeColor = meta ? meta.color : OBS_LIVE_FALLBACK_COLOR
|
|
3165
3336
|
const tool = String(e.tool || '')
|
|
3166
3337
|
const target = String(e.target || '')
|
|
3167
3338
|
const time = formatObsLiveTime(String(e.ts || ''))
|
|
3168
3339
|
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
3169
|
-
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color:
|
|
3340
|
+
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: typeColor } }, typeLabel),
|
|
3170
3341
|
tool
|
|
3171
3342
|
? React.createElement('span', { className: 'iterate-obs-chip' }, tool)
|
|
3172
3343
|
: null,
|
|
@@ -3175,29 +3346,29 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3175
3346
|
)
|
|
3176
3347
|
})
|
|
3177
3348
|
const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
3178
|
-
React.createElement('b', {}, '实时活动'),
|
|
3349
|
+
React.createElement('b', {}, t('实时活动')),
|
|
3179
3350
|
React.createElement('select', {
|
|
3180
3351
|
className: 'iterate-filter-select',
|
|
3181
3352
|
value: liveType,
|
|
3182
|
-
'aria-label': '按活动类型筛选',
|
|
3353
|
+
'aria-label': t('按活动类型筛选'),
|
|
3183
3354
|
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setLiveType(e.target.value),
|
|
3184
3355
|
},
|
|
3185
|
-
React.createElement('option', { value: '' }, '全部活动'),
|
|
3356
|
+
React.createElement('option', { value: '' }, t('全部活动')),
|
|
3186
3357
|
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
3187
3358
|
),
|
|
3188
3359
|
liveType
|
|
3189
3360
|
? React.createElement('button', {
|
|
3190
3361
|
className: 'iterate-filter-clear',
|
|
3191
3362
|
onClick: () => setLiveType(''),
|
|
3192
|
-
title: '清除类型筛选',
|
|
3193
|
-
}, '清除')
|
|
3363
|
+
title: t('清除类型筛选'),
|
|
3364
|
+
}, t('清除'))
|
|
3194
3365
|
: null,
|
|
3195
3366
|
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${liveEntries.length}`),
|
|
3196
3367
|
)
|
|
3197
3368
|
if (rows.length === 0) {
|
|
3198
3369
|
return React.createElement('div', {},
|
|
3199
3370
|
filterBar,
|
|
3200
|
-
React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的实时活动'),
|
|
3371
|
+
React.createElement('div', { className: 'iterate-obs-empty' }, t('无匹配的实时活动')),
|
|
3201
3372
|
)
|
|
3202
3373
|
}
|
|
3203
3374
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
@@ -3223,30 +3394,30 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3223
3394
|
|
|
3224
3395
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
3225
3396
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': open ? undefined : '', onClick: () => setOpen((v) => !v) },
|
|
3226
|
-
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
3397
|
+
React.createElement('span', { className: 'iterate-obs-title' }, t('iterate 观测台')),
|
|
3227
3398
|
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
3228
3399
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, headMeta || 'runtime'),
|
|
3229
3400
|
// Gap #14: make the tab system discoverable without expanding it.
|
|
3230
3401
|
React.createElement('span', {
|
|
3231
3402
|
className: 'iterate-obs-head-meta',
|
|
3232
|
-
title:
|
|
3233
|
-
},
|
|
3403
|
+
title: obsHotkeyHint(),
|
|
3404
|
+
}, obsHotkeyHint()),
|
|
3234
3405
|
React.createElement('button', {
|
|
3235
3406
|
className: 'iterate-btn', 'data-ghost': '',
|
|
3236
3407
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setOpen((v) => !v) },
|
|
3237
|
-
}, open ? '收起' : '展开'),
|
|
3408
|
+
}, open ? t('收起') : t('展开')),
|
|
3238
3409
|
),
|
|
3239
3410
|
open
|
|
3240
3411
|
? React.createElement('div', {},
|
|
3241
3412
|
copyFailText
|
|
3242
3413
|
? React.createElement('div', { className: 'iterate-obs-block' },
|
|
3243
3414
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3244
|
-
React.createElement('b', {}, '复制未成功'),
|
|
3245
|
-
React.createElement('span', { className: 'iterate-obs-head-meta' }, '剪贴板写入被拒绝,请手动选中下方指令复制'),
|
|
3415
|
+
React.createElement('b', {}, t('复制未成功')),
|
|
3416
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, t('剪贴板写入被拒绝,请手动选中下方指令复制')),
|
|
3246
3417
|
React.createElement('button', {
|
|
3247
|
-
className: 'iterate-btn', 'data-ghost': '', 'aria-label': '关闭复制失败提示',
|
|
3418
|
+
className: 'iterate-btn', 'data-ghost': '', 'aria-label': t('关闭复制失败提示'),
|
|
3248
3419
|
onClick: () => setCopyFailText(null),
|
|
3249
|
-
}, '关闭'),
|
|
3420
|
+
}, t('关闭')),
|
|
3250
3421
|
),
|
|
3251
3422
|
React.createElement('div', { className: 'iterate-obs-code' }, copyFailText),
|
|
3252
3423
|
)
|
|
@@ -3258,8 +3429,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3258
3429
|
React.createElement('button', {
|
|
3259
3430
|
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'export' ? '' : undefined,
|
|
3260
3431
|
onClick: exportObservatory,
|
|
3261
|
-
title: '将观测台全部数据导出为 JSON(优先下载,失败则复制)',
|
|
3262
|
-
}, copiedKey === 'export' ? '已导出' : '导出 JSON'),
|
|
3432
|
+
title: t('将观测台全部数据导出为 JSON(优先下载,失败则复制)'),
|
|
3433
|
+
}, copiedKey === 'export' ? t('已导出') : t('导出 JSON')),
|
|
3263
3434
|
// Gap #4: `.iterate/` artifacts are unreachable from the browser;
|
|
3264
3435
|
// this copies the one instruction that makes the model echo them
|
|
3265
3436
|
// into the session stream (which is what every panel scans).
|
|
@@ -3267,9 +3438,9 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3267
3438
|
className: 'iterate-btn', 'data-copied': copiedKey === 'pull-snapshot' ? '' : undefined,
|
|
3268
3439
|
onClick: () => copyInstruction('pull-snapshot', pullInstruction),
|
|
3269
3440
|
title: pullTitle,
|
|
3270
|
-
}, copiedKey === 'pull-snapshot' ? '已复制' : '拉取磁盘快照'),
|
|
3441
|
+
}, copiedKey === 'pull-snapshot' ? t('已复制') : t('拉取磁盘快照')),
|
|
3271
3442
|
),
|
|
3272
|
-
React.createElement('div', { className: 'iterate-obs-tabs', role: 'tablist', 'aria-label': '观测台页签(快捷键 1-9 / 0 / -)' },
|
|
3443
|
+
React.createElement('div', { className: 'iterate-obs-tabs', role: 'tablist', 'aria-label': t('观测台页签(快捷键 1-9 / 0 / -)') },
|
|
3273
3444
|
...OBS_TABS.map((t) =>
|
|
3274
3445
|
React.createElement('button', {
|
|
3275
3446
|
key: t.key,
|
|
@@ -3278,7 +3449,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3278
3449
|
'aria-selected': tab === t.key,
|
|
3279
3450
|
'data-active': tab === t.key ? '' : undefined,
|
|
3280
3451
|
onClick: () => setTab(t.key),
|
|
3281
|
-
title: t.hotkey ?
|
|
3452
|
+
title: t.hotkey ? tf(`{a0}(按 {a1} 切换)`, { a0: obsTabLabel(t), a1: t.hotkey }) : tf(`{a0}(按 - 切换)`, { a0: obsTabLabel(t) }),
|
|
3282
3453
|
}, obsTabLabel(t)),
|
|
3283
3454
|
),
|
|
3284
3455
|
),
|
|
@@ -3301,6 +3472,17 @@ function selectTurnTail(owner: SlotProps): { matched: boolean } | null {
|
|
|
3301
3472
|
return null
|
|
3302
3473
|
}
|
|
3303
3474
|
|
|
3475
|
+
/**
|
|
3476
|
+
* Slot components wrapped once at module scope so the wrapper identity is
|
|
3477
|
+
* stable — creating the wrapper inside the render callback would remount the
|
|
3478
|
+
* subtree (and wipe triage verdicts, expanded tabs …) on every render.
|
|
3479
|
+
*/
|
|
3480
|
+
const LangDashboard = withLang(ConvergenceDashboard)
|
|
3481
|
+
const LangObservatory = withLang(ObservatoryPanel)
|
|
3482
|
+
const LangTurnTail = withLang(TurnTailEntry)
|
|
3483
|
+
const LangProgress = withLang(ProgressCapsule)
|
|
3484
|
+
const LangSettings = withLang(SettingsPanel)
|
|
3485
|
+
|
|
3304
3486
|
/**
|
|
3305
3487
|
* Plugin client entry. Called by the dsh client runtime with its context.
|
|
3306
3488
|
*/
|
|
@@ -3310,6 +3492,15 @@ export function apply(ctx: ClientContext): void {
|
|
|
3310
3492
|
themeSvc = readTheme(ctx)
|
|
3311
3493
|
storage = createStorage()
|
|
3312
3494
|
|
|
3495
|
+
// 1b. Language: an explicit stored choice wins, then the browser locale.
|
|
3496
|
+
// Deliberately NOT persisted here (see setLang's `persist` flag) — an
|
|
3497
|
+
// auto-detected language must not shadow a choice the user never made.
|
|
3498
|
+
configureLangStorage(storage)
|
|
3499
|
+
setLang(
|
|
3500
|
+
detectLang(storage.get(LANG_STORAGE_KEY), typeof navigator !== 'undefined' ? navigator.language : ''),
|
|
3501
|
+
false,
|
|
3502
|
+
)
|
|
3503
|
+
|
|
3313
3504
|
// 2. Theme preference bootstrap.
|
|
3314
3505
|
const savedTheme = storage.get(THEME_STORAGE_KEY)
|
|
3315
3506
|
themeEnabled = savedTheme === null ? true : savedTheme === '1'
|
|
@@ -3344,7 +3535,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3344
3535
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
3345
3536
|
slotsSvc?.register(
|
|
3346
3537
|
{ name: 'conversation.input.dock', id: 'iterate-dashboard', order: 90 },
|
|
3347
|
-
(props) => React.createElement(
|
|
3538
|
+
(props) => React.createElement(LangDashboard, props),
|
|
3348
3539
|
),
|
|
3349
3540
|
)
|
|
3350
3541
|
|
|
@@ -3353,7 +3544,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3353
3544
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
3354
3545
|
slotsSvc?.register(
|
|
3355
3546
|
{ name: 'conversation.input.dock', id: 'iterate-observatory', order: 91 },
|
|
3356
|
-
(props) => React.createElement(
|
|
3547
|
+
(props) => React.createElement(LangObservatory, props),
|
|
3357
3548
|
),
|
|
3358
3549
|
)
|
|
3359
3550
|
|
|
@@ -3361,7 +3552,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3361
3552
|
slotsSvc.inject('conversation.chat.turnTail', () =>
|
|
3362
3553
|
slotsSvc?.register(
|
|
3363
3554
|
{ name: 'conversation.chat.turnTail', id: 'iterate-turn-tail', select: selectTurnTail },
|
|
3364
|
-
(props) => React.createElement(
|
|
3555
|
+
(props) => React.createElement(LangTurnTail, props),
|
|
3365
3556
|
),
|
|
3366
3557
|
)
|
|
3367
3558
|
|
|
@@ -3369,7 +3560,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3369
3560
|
slotsSvc.inject('shell.overlay', () =>
|
|
3370
3561
|
slotsSvc?.register(
|
|
3371
3562
|
{ name: 'shell.overlay', id: 'iterate-progress', order: 0 },
|
|
3372
|
-
(props) => React.createElement(
|
|
3563
|
+
(props) => React.createElement(LangProgress, props),
|
|
3373
3564
|
),
|
|
3374
3565
|
)
|
|
3375
3566
|
|
|
@@ -3377,7 +3568,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3377
3568
|
slotsSvc.inject('settings.section', () =>
|
|
3378
3569
|
slotsSvc?.register(
|
|
3379
3570
|
{ name: 'settings.section', id: 'iterate-settings', order: 30, label: () => 'iterate' },
|
|
3380
|
-
(props) => React.createElement(
|
|
3571
|
+
(props) => React.createElement(LangSettings, props),
|
|
3381
3572
|
),
|
|
3382
3573
|
)
|
|
3383
3574
|
}
|