@goodandready/dsh-moa 0.2.41 → 0.2.43

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/lib/client.js CHANGED
@@ -101,6 +101,8 @@ window.__ModuleLoader__.load({
101
101
  'proposers.add_btn': '+ Add Candidate Model',
102
102
  'proposers.temp_label': 'Candidates Temperature',
103
103
  'proposers.temp_hint': 'Recommended: 0.6 – 0.8 for creative, independent solution exploration.',
104
+ 'proposers.temperature_gradient': 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)',
105
+ 'proposers.candidate_temp_tooltip': 'Candidate Temperature (leave empty for default/gradient)',
104
106
  'stats.title': '📈 MoA Analytics & Telemetry',
105
107
  'stats.desc': 'Session telemetry, historical runs, and model win-rate tracking.',
106
108
  'stats.total_runs': 'Total Runs',
@@ -142,6 +144,64 @@ window.__ModuleLoader__.load({
142
144
  'diff.curator': 'Curator Synthesis',
143
145
  'diff.candidate': 'Candidate {num}',
144
146
  'diff.empty': 'Selected files are identical (zero diff).',
147
+ // Multi-Judge
148
+ 'aggregator.multi_judge_title': 'Multi-Judge Panel & Consensus Voting',
149
+ 'aggregator.multi_judge_desc': 'Deploy multiple judge models to score candidates and determine winner via consensus voting.',
150
+ 'aggregator.voting_strategy_label': 'Voting Strategy:',
151
+ 'aggregator.voting_majority': 'Majority Vote',
152
+ 'aggregator.voting_highest_score': 'Highest Score',
153
+ 'aggregator.voting_unanimous': 'Unanimous',
154
+ 'aggregator.judge_models_title': 'Judge Panel Models',
155
+ 'aggregator.judge_models_desc': 'Specify 2 or more judge models for panel consensus.',
156
+ 'aggregator.add_judge_btn': '+ Add Judge Model',
157
+ 'aggregator.judge_badge': 'Judge #{num}',
158
+ 'aggregator.judge_models_empty': 'At least 2 judge models are required for consensus voting.',
159
+
160
+ // Composite
161
+ 'aggregator.composite_title': 'Composite Hybrid Synthesis (AST / Block Merge)',
162
+ 'aggregator.composite_desc': 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.',
163
+
164
+ // Budget
165
+ 'aggregator.budget_title': 'Cost Budget Guardrails & Auto-Fallback',
166
+ 'aggregator.budget_desc': 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.',
167
+ 'aggregator.budget_max_usd': 'Max USD ($):',
168
+ 'aggregator.budget_action': 'Action:',
169
+ 'aggregator.budget_action_trim': 'Trim Pool',
170
+ 'aggregator.budget_action_abort': 'Abort',
171
+
172
+ // Reports
173
+ 'aggregator.report_title': 'Automated Benchmark & Post-Mortem Report',
174
+ 'aggregator.report_desc': 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.',
175
+
176
+ // Local Fallback
177
+ 'aggregator.local_fallback_title': 'Graceful Degradation & Local Fallback',
178
+ 'aggregator.local_fallback_desc': 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.',
179
+ 'aggregator.local_models_title': 'Local Fallback Models',
180
+ 'aggregator.local_models_desc': 'Local offline models used when cloud candidate APIs fail.',
181
+ 'aggregator.add_local_btn': '+ Add Local Model',
182
+ 'aggregator.local_badge': 'Local #{num}',
183
+ 'aggregator.local_models_empty': 'Configure at least one local fallback model (e.g. Ollama).',
184
+
185
+ // Smart Routing Model
186
+ 'config.smart_routing_title': 'Smart Prompt Routing',
187
+ 'config.smart_routing_desc': 'Analyze task complexity and automatically select the optimal preset.',
188
+ 'config.smart_routing_model': 'Routing Model:',
189
+
190
+ // Multi-Turn Context
191
+ 'aggregator.multi_turn_title': 'Multi-Turn Context & Diff Pruning',
192
+ 'aggregator.multi_turn_desc': 'Retain conversation context across turns while pruning duplicate code blocks.',
193
+
194
+ // UI States & Errors
195
+ 'status.loading': 'Loading MoA settings…',
196
+ 'status.unavailable': 'MoA service temporarily unavailable',
197
+ 'status.retry': 'Retry',
198
+ 'status.error_saving': 'Error saving: ',
199
+ 'error_boundary.title': '⚠️ MoA UI Error:',
200
+ 'error_boundary.retry': 'Retry',
201
+
202
+ // Diff Viewer
203
+ 'diff.select_run': 'Run:',
204
+ 'diff.latest_run': 'Latest / Active Workspace',
145
205
  'updater.success': 'Successfully updated to v{version}. Restart DSH to apply.',
146
206
  }
147
207
 
@@ -219,6 +279,8 @@ window.__ModuleLoader__.load({
219
279
  'proposers.add_btn': '+ 添加候选模型',
220
280
  'proposers.temp_label': '候选模型采样温度 (Temperature)',
221
281
  'proposers.temp_hint': '推荐:0.6 – 0.8,鼓励创造性、发散性的独立探索。',
282
+ 'proposers.temperature_gradient': '温度梯度探索(候选模型采样温度从 0.2 到 0.9 递增)',
283
+ 'proposers.candidate_temp_tooltip': '候选模型采样温度(留空使用默认/梯度)',
222
284
  'stats.title': '📈 MoA 数据分析与遥测',
223
285
  'stats.desc': '会话遥测、历史运行追踪及模型胜率统计。',
224
286
  'stats.total_runs': '总运行次数',
@@ -260,6 +322,64 @@ window.__ModuleLoader__.load({
260
322
  'diff.curator': '裁判合成 (Curator Synthesis)',
261
323
  'diff.candidate': '候选方案 {num}',
262
324
  'diff.empty': '选中的文件完全一致(无差异)。',
325
+ // Multi-Judge
326
+ 'aggregator.multi_judge_title': '多裁判委员会与共识投票',
327
+ 'aggregator.multi_judge_desc': '部署多个裁判模型对候选方案打分并通过共识投票评定获胜者。',
328
+ 'aggregator.voting_strategy_label': '投票策略:',
329
+ 'aggregator.voting_majority': '多数决',
330
+ 'aggregator.voting_highest_score': '最高分',
331
+ 'aggregator.voting_unanimous': '全票通过',
332
+ 'aggregator.judge_models_title': '裁判模型列表',
333
+ 'aggregator.judge_models_desc': '指定 2 个或更多裁判模型以实现委员会共识。',
334
+ 'aggregator.add_judge_btn': '+ 添加裁判模型',
335
+ 'aggregator.judge_badge': '裁判 #{num}',
336
+ 'aggregator.judge_models_empty': '共识投票至少需要配置 2 个裁判模型。',
337
+
338
+ // Composite
339
+ 'aggregator.composite_title': '混合复合综合 (AST / 代码块合并)',
340
+ 'aggregator.composite_desc': '从多个候选方案中提取模块化代码块、类型定义与算法并综合为统一方案。',
341
+
342
+ // Budget
343
+ 'aggregator.budget_title': '成本预算护栏与自动分流',
344
+ 'aggregator.budget_desc': '限制单次最高花费;自动削减候选模型或中止以防止超额消耗。',
345
+ 'aggregator.budget_max_usd': '最高金额 ($):',
346
+ 'aggregator.budget_action': '处置方式:',
347
+ 'aggregator.budget_action_trim': '削减候选',
348
+ 'aggregator.budget_action_abort': '中止运行',
349
+
350
+ // Reports
351
+ 'aggregator.report_title': '自动化基准与复盘报告',
352
+ 'aggregator.report_desc': '生成可下载的 Markdown 与 JSON 报告,详细记录候选方案耗时、一致性、测试门禁与成本分布。',
353
+
354
+ // Local Fallback
355
+ 'aggregator.local_fallback_title': '优雅降级与本地备用模型',
356
+ 'aggregator.local_fallback_desc': '在线候选模型 API 故障时自动降级到本地 Ollama / MiniPC 模型。',
357
+ 'aggregator.local_models_title': '本地备用模型列表',
358
+ 'aggregator.local_models_desc': '云端候选 API 异常时调用的本地离线模型。',
359
+ 'aggregator.add_local_btn': '+ 添加本地模型',
360
+ 'aggregator.local_badge': '本地 #{num}',
361
+ 'aggregator.local_models_empty': '请至少配置一个本地备用模型(例如 Ollama)。',
362
+
363
+ // Smart Routing Model
364
+ 'config.smart_routing_title': '智能提示词分流',
365
+ 'config.smart_routing_desc': '分析任务复杂度并自动匹配最佳预设。',
366
+ 'config.smart_routing_model': '分流决策模型:',
367
+
368
+ // Multi-Turn Context
369
+ 'aggregator.multi_turn_title': '多轮对话上下文与差异剪枝',
370
+ 'aggregator.multi_turn_desc': '跨轮次保留对话上下文并剪枝重复代码块。',
371
+
372
+ // UI States & Errors
373
+ 'status.loading': '正在加载 MoA 设置…',
374
+ 'status.unavailable': 'MoA 服务暂不可用',
375
+ 'status.retry': '重试',
376
+ 'status.error_saving': '保存失败:',
377
+ 'error_boundary.title': '⚠️ MoA 界面错误:',
378
+ 'error_boundary.retry': '重试',
379
+
380
+ // Diff Viewer
381
+ 'diff.select_run': '运行记录:',
382
+ 'diff.latest_run': '最新 / 当前工作区',
263
383
  'updater.success': '成功更新至 v{version},请重启 DSH 使其生效。',
264
384
  }
265
385
 
@@ -307,6 +427,55 @@ window.__ModuleLoader__.load({
307
427
  Chevron = FallbackChevron
308
428
  }
309
429
 
430
+
431
+ function renderSlotEditorList({ items, badgePrefix, onUpdate, onAdd, onRemove, addLabel, emptyWarning, availableModels, t }) {
432
+ return React.createElement(
433
+ 'div',
434
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
435
+ (Array.isArray(items) ? items : []).map((item, idx) =>
436
+ React.createElement(
437
+ 'div',
438
+ { key: idx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
439
+ React.createElement('span', { className: 'moa-cand-badge' }, `${badgePrefix} #${idx + 1}`),
440
+ React.createElement(
441
+ 'div',
442
+ { style: { flex: 1 } },
443
+ React.createElement(SearchableModelPicker, {
444
+ provider: (item && item.provider) || '',
445
+ model: (item && item.model) || '',
446
+ onChange: (prov, mod) => onUpdate(idx, prov, mod),
447
+ availableModels,
448
+ t,
449
+ })
450
+ ),
451
+ React.createElement(
452
+ 'button',
453
+ {
454
+ type: 'button',
455
+ className: 'moa-btn moa-btn-danger moa-btn-sub',
456
+ onClick: () => onRemove(idx),
457
+ },
458
+ '✕'
459
+ )
460
+ )
461
+ ),
462
+ React.createElement(
463
+ 'button',
464
+ {
465
+ type: 'button',
466
+ className: 'moa-btn moa-btn-sub',
467
+ style: { marginTop: 4, alignSelf: 'flex-start' },
468
+ onClick: onAdd,
469
+ },
470
+ addLabel
471
+ ),
472
+ emptyWarning ? React.createElement(
473
+ 'div',
474
+ { style: { fontSize: 11, color: 'var(--dsw-alias-state-warning-primary, #e6a23c)', marginTop: 2 } },
475
+ emptyWarning
476
+ ) : null
477
+ )
478
+ }
310
479
  const STYLES = `
311
480
  /* dsh-moa modern clinebot-aligned styling */
312
481
  .moa-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}
@@ -635,8 +804,25 @@ window.__ModuleLoader__.load({
635
804
  const [loading, setLoading] = React.useState(false)
636
805
  const [error, setError] = React.useState(null)
637
806
  const [hasFetched, setHasFetched] = React.useState(false)
807
+ const [historyRuns, setHistoryRuns] = React.useState([])
808
+ const [selectedRunId, setSelectedRunId] = React.useState('')
809
+
810
+ React.useEffect(() => {
811
+ fetch('/dsh-moa/history?limit=30', { cache: 'no-store' })
812
+ .then((r) => r.json())
813
+ .then((data) => {
814
+ if (data && data.ok && Array.isArray(data.runs)) {
815
+ setHistoryRuns(data.runs)
816
+ }
817
+ })
818
+ .catch(() => {})
819
+ }, [])
820
+
821
+ const selectedRun = historyRuns.find((r) => r.id === selectedRunId)
822
+ const candidateCount = selectedRun && Array.isArray(selectedRun.candidates) && selectedRun.candidates.length > 0
823
+ ? selectedRun.candidates.length
824
+ : Math.max(currentRefs.length || 0, 2)
638
825
 
639
- const candidateCount = Math.max(currentRefs.length || 0, 2)
640
826
  const candidateOptions = []
641
827
  for (let i = 1; i <= candidateCount; i++) {
642
828
  candidateOptions.push({
@@ -649,6 +835,7 @@ window.__ModuleLoader__.load({
649
835
  setLoading(true)
650
836
  setError(null)
651
837
  const params = new URLSearchParams()
838
+ if (selectedRunId) params.set('runId', selectedRunId)
652
839
  if (fromCandidate) params.set('from', fromCandidate)
653
840
  if (toCandidate) params.set('to', toCandidate)
654
841
  if (targetFile) params.set('file', targetFile)
@@ -679,11 +866,11 @@ window.__ModuleLoader__.load({
679
866
  setHasFetched(true)
680
867
  setError(err.message || String(err))
681
868
  })
682
- }, [fromCandidate, toCandidate, selectedFile])
869
+ }, [fromCandidate, toCandidate, selectedFile, selectedRunId])
683
870
 
684
871
  React.useEffect(() => {
685
872
  loadDiff()
686
- }, [fromCandidate, toCandidate])
873
+ }, [fromCandidate, toCandidate, selectedRunId])
687
874
 
688
875
  const handleFileChange = (e) => {
689
876
  const nextFile = e.target.value
@@ -707,6 +894,25 @@ window.__ModuleLoader__.load({
707
894
  React.createElement(
708
895
  'div',
709
896
  { className: 'moa-diff-controls' },
897
+ /* Run selector dropdown (#179) */
898
+ React.createElement(
899
+ 'label',
900
+ { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
901
+ t('diff.select_run') || 'Run:',
902
+ React.createElement(
903
+ 'select',
904
+ {
905
+ className: 'moa-diff-select',
906
+ value: selectedRunId,
907
+ onChange: (e) => setSelectedRunId(e.target.value),
908
+ },
909
+ React.createElement('option', { value: '' }, t('diff.latest_run') || 'Latest / Active Workspace'),
910
+ historyRuns.map((r) => {
911
+ const label = `${r.preset || 'default'} [${(r.id || '').slice(0, 8)}] ${(r.prompt || '').slice(0, 24)}...`
912
+ return React.createElement('option', { key: r.id, value: r.id }, label)
913
+ })
914
+ )
915
+ ),
710
916
  React.createElement(
711
917
  'label',
712
918
  { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
@@ -1331,7 +1537,7 @@ window.__ModuleLoader__.load({
1331
1537
  })
1332
1538
  )
1333
1539
  ),
1334
- /* Multi-Judge Panel & Consensus Voting */
1540
+ /* Multi-Judge Panel & Consensus Voting (#175, #176) */
1335
1541
  React.createElement(
1336
1542
  'div',
1337
1543
  { style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
@@ -1344,29 +1550,62 @@ window.__ModuleLoader__.load({
1344
1550
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, multi_judge_enabled: e.target.checked })),
1345
1551
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1346
1552
  }),
1347
- 'Multi-Judge Panel & Consensus Voting'
1553
+ t('aggregator.multi_judge_title') || 'Multi-Judge Panel & Consensus Voting'
1348
1554
  ),
1349
1555
  React.createElement(
1350
1556
  'div',
1351
1557
  { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1352
- 'Deploy multiple judge models to score candidates and determine winner via consensus voting.'
1558
+ t('aggregator.multi_judge_desc') || 'Deploy multiple judge models to score candidates and determine winner via consensus voting.'
1353
1559
  ),
1354
1560
  currentPreset.multi_judge_enabled && React.createElement(
1355
1561
  'div',
1356
- { style: { marginLeft: 22, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 } },
1357
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Voting Strategy:'),
1562
+ { style: { marginLeft: 22, display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
1358
1563
  React.createElement(
1359
- 'select',
1360
- {
1361
- className: 'moa-select',
1362
- style: { width: 140, height: 28, fontSize: 12, padding: '0 6px' },
1363
- value: currentPreset.judge_voting_strategy || 'majority',
1364
- onChange: (e) => updateCurrentPreset((p) => ({ ...p, judge_voting_strategy: e.target.value })),
1564
+ 'div',
1565
+ { style: { display: 'flex', alignItems: 'center', gap: 8 } },
1566
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.voting_strategy_label') || 'Voting Strategy:'),
1567
+ React.createElement(
1568
+ 'select',
1569
+ {
1570
+ className: 'moa-select',
1571
+ style: { width: 140, height: 28, fontSize: 12, padding: '0 6px' },
1572
+ value: currentPreset.judge_voting_strategy || 'majority',
1573
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, judge_voting_strategy: e.target.value })),
1574
+ },
1575
+ React.createElement('option', { value: 'majority' }, t('aggregator.voting_majority') || 'Majority Vote'),
1576
+ React.createElement('option', { value: 'highest_score' }, t('aggregator.voting_highest_score') || 'Highest Score'),
1577
+ React.createElement('option', { value: 'unanimous' }, t('aggregator.voting_unanimous') || 'Unanimous')
1578
+ )
1579
+ ),
1580
+ renderSlotEditorList({
1581
+ items: currentPreset.judge_models,
1582
+ badgePrefix: 'Judge',
1583
+ onUpdate: (idx, prov, mod) => {
1584
+ updateCurrentPreset((p) => {
1585
+ const list = [...(p.judge_models || [])]
1586
+ list[idx] = { provider: prov, model: mod }
1587
+ return { ...p, judge_models: list }
1588
+ })
1365
1589
  },
1366
- React.createElement('option', { value: 'majority' }, 'Majority Vote'),
1367
- React.createElement('option', { value: 'highest_score' }, 'Highest Score'),
1368
- React.createElement('option', { value: 'unanimous' }, 'Unanimous')
1369
- )
1590
+ onRemove: (idx) => {
1591
+ updateCurrentPreset((p) => ({
1592
+ ...p,
1593
+ judge_models: (p.judge_models || []).filter((_, i) => i !== idx),
1594
+ }))
1595
+ },
1596
+ onAdd: () => {
1597
+ updateCurrentPreset((p) => ({
1598
+ ...p,
1599
+ judge_models: [...(p.judge_models || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1600
+ }))
1601
+ },
1602
+ addLabel: t('aggregator.add_judge_btn') || '+ Add Judge Model',
1603
+ emptyWarning: (!currentPreset.judge_models || currentPreset.judge_models.length < 2)
1604
+ ? (t('aggregator.judge_models_empty') || 'At least 2 judge models are required for consensus voting.')
1605
+ : null,
1606
+ availableModels,
1607
+ t,
1608
+ })
1370
1609
  )
1371
1610
  ),
1372
1611
 
@@ -1383,12 +1622,12 @@ window.__ModuleLoader__.load({
1383
1622
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, composite_merge_enabled: e.target.checked })),
1384
1623
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1385
1624
  }),
1386
- 'Composite Hybrid Synthesis (AST / Block Merge)'
1625
+ t('aggregator.composite_title') || 'Composite Hybrid Synthesis (AST / Block Merge)'
1387
1626
  ),
1388
1627
  React.createElement(
1389
1628
  'div',
1390
1629
  { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1391
- 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.'
1630
+ t('aggregator.composite_desc') || 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.'
1392
1631
  )
1393
1632
  ),
1394
1633
 
@@ -1405,12 +1644,12 @@ window.__ModuleLoader__.load({
1405
1644
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, budget_guard_enabled: e.target.checked })),
1406
1645
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1407
1646
  }),
1408
- 'Cost Budget Guardrails & Auto-Fallback'
1647
+ t('aggregator.budget_title') || 'Cost Budget Guardrails & Auto-Fallback'
1409
1648
  ),
1410
1649
  React.createElement(
1411
1650
  'div',
1412
1651
  { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1413
- 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.'
1652
+ t('aggregator.budget_desc') || 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.'
1414
1653
  ),
1415
1654
  currentPreset.budget_guard_enabled && React.createElement(
1416
1655
  'div',
@@ -1418,7 +1657,7 @@ window.__ModuleLoader__.load({
1418
1657
  React.createElement(
1419
1658
  'div',
1420
1659
  { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1421
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Max USD ($):'),
1660
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.budget_max_usd') || 'Max USD ($):'),
1422
1661
  React.createElement('input', {
1423
1662
  type: 'number',
1424
1663
  step: '0.01',
@@ -1434,7 +1673,7 @@ window.__ModuleLoader__.load({
1434
1673
  React.createElement(
1435
1674
  'div',
1436
1675
  { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1437
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Action:'),
1676
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.budget_action') || 'Action:'),
1438
1677
  React.createElement(
1439
1678
  'select',
1440
1679
  {
@@ -1443,8 +1682,8 @@ window.__ModuleLoader__.load({
1443
1682
  value: currentPreset.budget_action || 'trim',
1444
1683
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, budget_action: e.target.value })),
1445
1684
  },
1446
- React.createElement('option', { value: 'trim' }, 'Trim Pool'),
1447
- React.createElement('option', { value: 'abort' }, 'Abort')
1685
+ React.createElement('option', { value: 'trim' }, t('aggregator.budget_action_trim') || 'Trim Pool'),
1686
+ React.createElement('option', { value: 'abort' }, t('aggregator.budget_action_abort') || 'Abort')
1448
1687
  )
1449
1688
  )
1450
1689
  )
@@ -1463,16 +1702,16 @@ window.__ModuleLoader__.load({
1463
1702
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, report_generation_enabled: e.target.checked })),
1464
1703
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1465
1704
  }),
1466
- 'Automated Benchmark & Post-Mortem Report'
1705
+ t('aggregator.report_title') || 'Automated Benchmark & Post-Mortem Report'
1467
1706
  ),
1468
1707
  React.createElement(
1469
1708
  'div',
1470
1709
  { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1471
- 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.'
1710
+ t('aggregator.report_desc') || 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.'
1472
1711
  )
1473
1712
  ),
1474
1713
 
1475
- /* Graceful Degradation & Local Fallback */
1714
+ /* Graceful Degradation & Local Fallback (#175, #176) */
1476
1715
  React.createElement(
1477
1716
  'div',
1478
1717
  { style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
@@ -1485,86 +1724,154 @@ window.__ModuleLoader__.load({
1485
1724
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, local_fallback_enabled: e.target.checked })),
1486
1725
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1487
1726
  }),
1488
- 'Graceful Degradation & Local Fallback'
1727
+ t('aggregator.local_fallback_title') || 'Graceful Degradation & Local Fallback'
1489
1728
  ),
1490
1729
  React.createElement(
1491
1730
  'div',
1492
1731
  { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1493
- 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.'
1732
+ t('aggregator.local_fallback_desc') || 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.'
1733
+ ),
1734
+ currentPreset.local_fallback_enabled && React.createElement(
1735
+ 'div',
1736
+ { style: { marginLeft: 22, marginTop: 4 } },
1737
+ renderSlotEditorList({
1738
+ items: currentPreset.local_fallback_models,
1739
+ badgePrefix: 'Local',
1740
+ onUpdate: (idx, prov, mod) => {
1741
+ updateCurrentPreset((p) => {
1742
+ const list = [...(p.local_fallback_models || [])]
1743
+ list[idx] = { provider: prov, model: mod }
1744
+ return { ...p, local_fallback_models: list }
1745
+ })
1746
+ },
1747
+ onRemove: (idx) => {
1748
+ updateCurrentPreset((p) => ({
1749
+ ...p,
1750
+ local_fallback_models: (p.local_fallback_models || []).filter((_, i) => i !== idx),
1751
+ }))
1752
+ },
1753
+ onAdd: () => {
1754
+ updateCurrentPreset((p) => ({
1755
+ ...p,
1756
+ local_fallback_models: [...(p.local_fallback_models || []), { provider: 'ollama', model: '' }],
1757
+ }))
1758
+ },
1759
+ addLabel: t('aggregator.add_local_btn') || '+ Add Local Model',
1760
+ emptyWarning: (!currentPreset.local_fallback_models || currentPreset.local_fallback_models.length === 0)
1761
+ ? (t('aggregator.local_models_empty') || 'Configure at least one local fallback model (e.g. Ollama).')
1762
+ : null,
1763
+ availableModels,
1764
+ t,
1765
+ })
1494
1766
  )
1495
1767
  ),
1496
1768
 
1497
- /* Fallback Judges Chain */
1769
+ /* Smart Prompt Routing (#176) */
1498
1770
  React.createElement(
1499
1771
  'div',
1500
- { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
1772
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
1501
1773
  React.createElement(
1502
- 'div',
1503
- { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
1504
- t('aggregator.fallbacks_title')
1774
+ 'label',
1775
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
1776
+ React.createElement('input', {
1777
+ type: 'checkbox',
1778
+ checked: Boolean(currentPreset.smart_routing_enabled),
1779
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, smart_routing_enabled: e.target.checked })),
1780
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1781
+ }),
1782
+ t('config.smart_routing_title') || 'Smart Prompt Routing'
1505
1783
  ),
1506
1784
  React.createElement(
1507
1785
  'div',
1508
- { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } },
1509
- t('aggregator.fallbacks_desc')
1510
- ),
1511
- (Array.isArray(currentPreset.aggregator_fallbacks) ? currentPreset.aggregator_fallbacks : []).map((fb, fbIdx) =>
1512
- React.createElement(
1513
- 'div',
1514
- { key: fbIdx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
1515
- React.createElement('span', { className: 'moa-cand-badge' }, `Fallback #${fbIdx + 1}`),
1516
- React.createElement(
1517
- 'div',
1518
- { style: { flex: 1 } },
1519
- React.createElement(SearchableModelPicker, {
1520
- provider: (fb && fb.provider) || '',
1521
- model: (fb && fb.model) || '',
1522
- onChange: (prov, mod) => {
1523
- updateCurrentPreset((p) => {
1524
- const list = [...(p.aggregator_fallbacks || [])]
1525
- list[fbIdx] = { provider: prov, model: mod }
1526
- return { ...p, aggregator_fallbacks: list }
1527
- })
1528
- },
1529
- availableModels,
1530
- t,
1531
- })
1532
- ),
1533
- React.createElement(
1534
- 'button',
1535
- {
1536
- type: 'button',
1537
- className: 'moa-btn moa-btn-danger moa-btn-sub',
1538
- onClick: () => {
1539
- updateCurrentPreset((p) => ({
1540
- ...p,
1541
- aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== fbIdx),
1542
- }))
1543
- },
1544
- },
1545
- '✕'
1546
- )
1547
- )
1786
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1787
+ t('config.smart_routing_desc') || 'Analyze task complexity and automatically select the optimal preset.'
1548
1788
  ),
1549
- React.createElement(
1789
+ currentPreset.smart_routing_enabled && React.createElement(
1550
1790
  'div',
1551
- null,
1791
+ { style: { marginLeft: 22, marginTop: 4, display: 'flex', alignItems: 'center', gap: 8 } },
1792
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('config.smart_routing_model') || 'Routing Model:'),
1552
1793
  React.createElement(
1553
- 'button',
1554
- {
1555
- type: 'button',
1556
- className: 'moa-btn moa-btn-sub',
1557
- style: { marginTop: 4 },
1558
- onClick: () => {
1794
+ 'div',
1795
+ { style: { flex: 1, maxWidth: 320 } },
1796
+ React.createElement(SearchableModelPicker, {
1797
+ provider: currentPreset.smart_routing_model?.provider || '',
1798
+ model: currentPreset.smart_routing_model?.model || '',
1799
+ onChange: (prov, mod) => {
1559
1800
  updateCurrentPreset((p) => ({
1560
1801
  ...p,
1561
- aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1802
+ smart_routing_model: { provider: prov, model: mod },
1562
1803
  }))
1563
1804
  },
1564
- },
1565
- t('aggregator.add_fallback_btn')
1805
+ availableModels,
1806
+ t,
1807
+ })
1566
1808
  )
1567
1809
  )
1810
+ ),
1811
+
1812
+ /* Multi-Turn Context & Diff Pruning (#176) */
1813
+ React.createElement(
1814
+ 'div',
1815
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
1816
+ React.createElement(
1817
+ 'label',
1818
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
1819
+ React.createElement('input', {
1820
+ type: 'checkbox',
1821
+ checked: currentPreset.multi_turn_enabled !== false,
1822
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, multi_turn_enabled: e.target.checked })),
1823
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1824
+ }),
1825
+ t('aggregator.multi_turn_title') || 'Multi-Turn Context & Diff Pruning'
1826
+ ),
1827
+ React.createElement(
1828
+ 'div',
1829
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1830
+ t('aggregator.multi_turn_desc') || 'Retain conversation context across turns while pruning duplicate code blocks.'
1831
+ )
1832
+ ),
1833
+
1834
+ /* Fallback Judges Chain */
1835
+ React.createElement(
1836
+ 'div',
1837
+ { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
1838
+ React.createElement(
1839
+ 'div',
1840
+ { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
1841
+ t('aggregator.fallbacks_title')
1842
+ ),
1843
+ React.createElement(
1844
+ 'div',
1845
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } },
1846
+ t('aggregator.fallbacks_desc')
1847
+ ),
1848
+ renderSlotEditorList({
1849
+ items: currentPreset.aggregator_fallbacks,
1850
+ badgePrefix: 'Fallback',
1851
+ onUpdate: (idx, prov, mod) => {
1852
+ updateCurrentPreset((p) => {
1853
+ const list = [...(p.aggregator_fallbacks || [])]
1854
+ list[idx] = { provider: prov, model: mod }
1855
+ return { ...p, aggregator_fallbacks: list }
1856
+ })
1857
+ },
1858
+ onRemove: (idx) => {
1859
+ updateCurrentPreset((p) => ({
1860
+ ...p,
1861
+ aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== idx),
1862
+ }))
1863
+ },
1864
+ onAdd: () => {
1865
+ updateCurrentPreset((p) => ({
1866
+ ...p,
1867
+ aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1868
+ }))
1869
+ },
1870
+ addLabel: t('aggregator.add_fallback_btn'),
1871
+ emptyWarning: null,
1872
+ availableModels,
1873
+ t,
1874
+ })
1568
1875
  )
1569
1876
  )
1570
1877
  )
@@ -1659,7 +1966,7 @@ window.__ModuleLoader__.load({
1659
1966
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, temperature_gradient_enabled: e.target.checked })),
1660
1967
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1661
1968
  }),
1662
- 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)'
1969
+ t('proposers.temperature_gradient') || 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)'
1663
1970
  )
1664
1971
  ),
1665
1972
  React.createElement(
@@ -1687,7 +1994,7 @@ window.__ModuleLoader__.load({
1687
1994
  min: '0',
1688
1995
  max: '2',
1689
1996
  placeholder: 'Temp',
1690
- title: 'Candidate Temperature (leave empty for default/gradient)',
1997
+ title: t('proposers.candidate_temp_tooltip') || 'Candidate Temperature (leave empty for default/gradient)',
1691
1998
  className: 'moa-input',
1692
1999
  style: { width: 50, height: 32, fontSize: 12, padding: '0 4px', textAlign: 'center' },
1693
2000
  value: (typeof ref.temperature === 'number' && ref.temperature >= 0) ? ref.temperature : '',
@@ -2021,6 +2328,9 @@ window.__ModuleLoader__.load({
2021
2328
  )
2022
2329
  }
2023
2330
 
2331
+ const UNAVAILABLE_SNAPSHOT = Object.freeze({ status: 'unavailable' })
2332
+ const LOADING_SNAPSHOT = Object.freeze({ status: 'loading' })
2333
+
2024
2334
  function useMoASettings(props) {
2025
2335
  const effectiveCtx = (props && props.ctx) || rootCtx
2026
2336
  // English is canonical; prefer the core-provided translator for the
@@ -2033,12 +2343,12 @@ window.__ModuleLoader__.load({
2033
2343
  return s && typeof s.get === 'function' ? s.get(NS) : undefined
2034
2344
  }, [effectiveCtx])
2035
2345
 
2346
+ const getSnapshot = React.useCallback(() => (scope ? scope.getSnapshot() : UNAVAILABLE_SNAPSHOT), [scope])
2347
+ const getServerSnapshot = React.useCallback(() => LOADING_SNAPSHOT, [])
2348
+ const subscribe = React.useCallback((cb) => (scope ? scope.subscribe(cb) : () => {}), [scope])
2349
+
2036
2350
  const snapshot = React.useSyncExternalStore
2037
- ? React.useSyncExternalStore(
2038
- React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
2039
- React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'unavailable' }), [scope]),
2040
- React.useCallback(() => ({ status: 'loading' }), [])
2041
- )
2351
+ ? React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
2042
2352
  : null
2043
2353
 
2044
2354
  const [status, setStatus] = React.useState('loading')
@@ -2052,6 +2362,7 @@ window.__ModuleLoader__.load({
2052
2362
  currentVersion: '0.2.16',
2053
2363
  latestVersion: undefined,
2054
2364
  updateAvailable: false,
2365
+ canAutoUpdate: true,
2055
2366
  notice: null,
2056
2367
  error: null,
2057
2368
  })
@@ -2068,10 +2379,11 @@ window.__ModuleLoader__.load({
2068
2379
  currentVersion: data.currentVersion,
2069
2380
  latestVersion: data.latestVersion,
2070
2381
  updateAvailable: Boolean(data.updateAvailable),
2382
+ canAutoUpdate: data.canAutoUpdate !== undefined ? Boolean(data.canAutoUpdate) : prev.canAutoUpdate,
2071
2383
  error: data.latestCheckFailed ? (t('updater.checkFailed') || 'Registry check failed') : null,
2072
2384
  }))
2073
2385
  } else {
2074
- setUpdateState((prev) => ({ ...prev, checking: false, error: data.error || 'Check failed' }))
2386
+ setUpdateState((prev) => ({ ...prev, checking: false, error: data?.error || 'Check failed' }))
2075
2387
  }
2076
2388
  } catch (err) {
2077
2389
  setUpdateState((prev) => ({ ...prev, checking: false, error: String(err?.message || err) }))
@@ -2086,19 +2398,20 @@ window.__ModuleLoader__.load({
2086
2398
  headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' },
2087
2399
  })
2088
2400
  const data = await res.json()
2089
- if (data && data.updated) {
2401
+ if (data && (data.updated || data.updatedVersion || data.restartRequired || data.ok)) {
2402
+ const nextVer = data.updatedVersion || data.latestVersion || data.currentVersion
2090
2403
  setUpdateState((prev) => ({
2091
2404
  ...prev,
2092
2405
  updating: false,
2093
2406
  updateAvailable: false,
2094
- currentVersion: data.updatedVersion || data.latestVersion,
2095
- notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}', data.updatedVersion || data.latestVersion),
2407
+ currentVersion: nextVer,
2408
+ notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}', nextVer),
2096
2409
  }))
2097
2410
  } else {
2098
2411
  setUpdateState((prev) => ({
2099
2412
  ...prev,
2100
2413
  updating: false,
2101
- error: data.error || data.message || 'Update failed',
2414
+ error: data?.error || data?.message || 'Update failed',
2102
2415
  }))
2103
2416
  }
2104
2417
  } catch (err) {
@@ -2258,7 +2571,7 @@ window.__ModuleLoader__.load({
2258
2571
  if (res.defaultPreset) setDefaultPreset(res.defaultPreset)
2259
2572
  setTimeout(() => setSaveStatus(''), 3000)
2260
2573
  } else {
2261
- setSaveStatus('Error saving: ' + (res?.error || 'failed'))
2574
+ setSaveStatus((t('status.error_saving') || 'Error saving: ') + (res?.error || 'failed'))
2262
2575
  }
2263
2576
  })
2264
2577
  .catch((err) => {
@@ -2303,13 +2616,14 @@ window.__ModuleLoader__.load({
2303
2616
  }
2304
2617
  render() {
2305
2618
  if (this.state.hasError) {
2619
+ const t = typeof this.props?.t === 'function' ? this.props.t : (typeof makeT === 'function' ? makeT(en, null) : ((k) => k))
2306
2620
  return React.createElement(
2307
2621
  'div',
2308
2622
  {
2309
2623
  className: 'moa-alert-err',
2310
2624
  style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
2311
2625
  },
2312
- React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ MoA UI Error:'),
2626
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, t('error_boundary.title') || '⚠️ MoA UI Error:'),
2313
2627
  React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
2314
2628
  React.createElement(
2315
2629
  'button',
@@ -2319,7 +2633,7 @@ window.__ModuleLoader__.load({
2319
2633
  style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
2320
2634
  onClick: () => this.setState({ hasError: false, error: null }),
2321
2635
  },
2322
- 'Retry'
2636
+ t('error_boundary.retry') || 'Retry'
2323
2637
  )
2324
2638
  )
2325
2639
  }
@@ -2366,12 +2680,12 @@ window.__ModuleLoader__.load({
2366
2680
  'div',
2367
2681
  { className: 'moa-body' },
2368
2682
  state.status === 'loading'
2369
- ? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, 'Loading MoA settings…')
2683
+ ? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, state.t('status.loading') || 'Loading MoA settings…')
2370
2684
  : state.status === 'unavailable'
2371
2685
  ? React.createElement(
2372
2686
  'div',
2373
2687
  { style: { padding: '16px 0', display: 'flex', flexDirection: 'column', gap: 10 } },
2374
- React.createElement('div', { className: 'moa-alert-err' }, 'MoA service temporarily unavailable'),
2688
+ React.createElement('div', { className: 'moa-alert-err' }, state.t('status.unavailable') || 'MoA service temporarily unavailable'),
2375
2689
  React.createElement(
2376
2690
  'button',
2377
2691
  {
@@ -2380,7 +2694,7 @@ window.__ModuleLoader__.load({
2380
2694
  style: { width: 'fit-content' },
2381
2695
  onClick: state.reload,
2382
2696
  },
2383
- state.t('actions.retry')
2697
+ state.t('status.retry') || state.t('actions.retry')
2384
2698
  )
2385
2699
  )
2386
2700
  : React.createElement(MoAEditor, state)
@@ -2429,7 +2743,7 @@ window.__ModuleLoader__.load({
2429
2743
  locale: NS,
2430
2744
  inject: () => ({ ctx }),
2431
2745
  },
2432
- (props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
2746
+ (props) => React.createElement(MoAErrorBoundary, props, React.createElement(MoACard, props))
2433
2747
  )
2434
2748
  } catch (err) {
2435
2749
  console.warn('[dsh-moa] Failed to register ' + slotName + ':', err)
@@ -2452,7 +2766,7 @@ window.__ModuleLoader__.load({
2452
2766
  locale: NS,
2453
2767
  inject: () => ({ ctx }),
2454
2768
  },
2455
- (props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
2769
+ (props) => React.createElement(MoAErrorBoundary, props, React.createElement(MoACard, props))
2456
2770
  )
2457
2771
  } catch (err) {
2458
2772
  console.warn('[dsh-moa] Failed to register plugins.item:', err)