@goodandready/dsh-moa 0.2.40 → 0.2.42

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
@@ -142,6 +142,64 @@ window.__ModuleLoader__.load({
142
142
  'diff.curator': 'Curator Synthesis',
143
143
  'diff.candidate': 'Candidate {num}',
144
144
  'diff.empty': 'Selected files are identical (zero diff).',
145
+ // Multi-Judge
146
+ 'aggregator.multi_judge_title': 'Multi-Judge Panel & Consensus Voting',
147
+ 'aggregator.multi_judge_desc': 'Deploy multiple judge models to score candidates and determine winner via consensus voting.',
148
+ 'aggregator.voting_strategy_label': 'Voting Strategy:',
149
+ 'aggregator.voting_majority': 'Majority Vote',
150
+ 'aggregator.voting_highest_score': 'Highest Score',
151
+ 'aggregator.voting_unanimous': 'Unanimous',
152
+ 'aggregator.judge_models_title': 'Judge Panel Models',
153
+ 'aggregator.judge_models_desc': 'Specify 2 or more judge models for panel consensus.',
154
+ 'aggregator.add_judge_btn': '+ Add Judge Model',
155
+ 'aggregator.judge_badge': 'Judge #{num}',
156
+ 'aggregator.judge_models_empty': 'At least 2 judge models are required for consensus voting.',
157
+
158
+ // Composite
159
+ 'aggregator.composite_title': 'Composite Hybrid Synthesis (AST / Block Merge)',
160
+ 'aggregator.composite_desc': 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.',
161
+
162
+ // Budget
163
+ 'aggregator.budget_title': 'Cost Budget Guardrails & Auto-Fallback',
164
+ 'aggregator.budget_desc': 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.',
165
+ 'aggregator.budget_max_usd': 'Max USD ($):',
166
+ 'aggregator.budget_action': 'Action:',
167
+ 'aggregator.budget_action_trim': 'Trim Pool',
168
+ 'aggregator.budget_action_abort': 'Abort',
169
+
170
+ // Reports
171
+ 'aggregator.report_title': 'Automated Benchmark & Post-Mortem Report',
172
+ 'aggregator.report_desc': 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.',
173
+
174
+ // Local Fallback
175
+ 'aggregator.local_fallback_title': 'Graceful Degradation & Local Fallback',
176
+ 'aggregator.local_fallback_desc': 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.',
177
+ 'aggregator.local_models_title': 'Local Fallback Models',
178
+ 'aggregator.local_models_desc': 'Local offline models used when cloud candidate APIs fail.',
179
+ 'aggregator.add_local_btn': '+ Add Local Model',
180
+ 'aggregator.local_badge': 'Local #{num}',
181
+ 'aggregator.local_models_empty': 'Configure at least one local fallback model (e.g. Ollama).',
182
+
183
+ // Smart Routing Model
184
+ 'config.smart_routing_title': 'Smart Prompt Routing',
185
+ 'config.smart_routing_desc': 'Analyze task complexity and automatically select the optimal preset.',
186
+ 'config.smart_routing_model': 'Routing Model:',
187
+
188
+ // Multi-Turn Context
189
+ 'aggregator.multi_turn_title': 'Multi-Turn Context & Diff Pruning',
190
+ 'aggregator.multi_turn_desc': 'Retain conversation context across turns while pruning duplicate code blocks.',
191
+
192
+ // UI States & Errors
193
+ 'status.loading': 'Loading MoA settings…',
194
+ 'status.unavailable': 'MoA service temporarily unavailable',
195
+ 'status.retry': 'Retry',
196
+ 'status.error_saving': 'Error saving: ',
197
+ 'error_boundary.title': '⚠️ MoA UI Error:',
198
+ 'error_boundary.retry': 'Retry',
199
+
200
+ // Diff Viewer
201
+ 'diff.select_run': 'Run:',
202
+ 'diff.latest_run': 'Latest / Active Workspace',
145
203
  'updater.success': 'Successfully updated to v{version}. Restart DSH to apply.',
146
204
  }
147
205
 
@@ -260,6 +318,64 @@ window.__ModuleLoader__.load({
260
318
  'diff.curator': '裁判合成 (Curator Synthesis)',
261
319
  'diff.candidate': '候选方案 {num}',
262
320
  'diff.empty': '选中的文件完全一致(无差异)。',
321
+ // Multi-Judge
322
+ 'aggregator.multi_judge_title': '多裁判委员会与共识投票',
323
+ 'aggregator.multi_judge_desc': '部署多个裁判模型对候选方案打分并通过共识投票评定获胜者。',
324
+ 'aggregator.voting_strategy_label': '投票策略:',
325
+ 'aggregator.voting_majority': '多数决',
326
+ 'aggregator.voting_highest_score': '最高分',
327
+ 'aggregator.voting_unanimous': '全票通过',
328
+ 'aggregator.judge_models_title': '裁判模型列表',
329
+ 'aggregator.judge_models_desc': '指定 2 个或更多裁判模型以实现委员会共识。',
330
+ 'aggregator.add_judge_btn': '+ 添加裁判模型',
331
+ 'aggregator.judge_badge': '裁判 #{num}',
332
+ 'aggregator.judge_models_empty': '共识投票至少需要配置 2 个裁判模型。',
333
+
334
+ // Composite
335
+ 'aggregator.composite_title': '混合复合综合 (AST / 代码块合并)',
336
+ 'aggregator.composite_desc': '从多个候选方案中提取模块化代码块、类型定义与算法并综合为统一方案。',
337
+
338
+ // Budget
339
+ 'aggregator.budget_title': '成本预算护栏与自动分流',
340
+ 'aggregator.budget_desc': '限制单次最高花费;自动削减候选模型或中止以防止超额消耗。',
341
+ 'aggregator.budget_max_usd': '最高金额 ($):',
342
+ 'aggregator.budget_action': '处置方式:',
343
+ 'aggregator.budget_action_trim': '削减候选',
344
+ 'aggregator.budget_action_abort': '中止运行',
345
+
346
+ // Reports
347
+ 'aggregator.report_title': '自动化基准与复盘报告',
348
+ 'aggregator.report_desc': '生成可下载的 Markdown 与 JSON 报告,详细记录候选方案耗时、一致性、测试门禁与成本分布。',
349
+
350
+ // Local Fallback
351
+ 'aggregator.local_fallback_title': '优雅降级与本地备用模型',
352
+ 'aggregator.local_fallback_desc': '在线候选模型 API 故障时自动降级到本地 Ollama / MiniPC 模型。',
353
+ 'aggregator.local_models_title': '本地备用模型列表',
354
+ 'aggregator.local_models_desc': '云端候选 API 异常时调用的本地离线模型。',
355
+ 'aggregator.add_local_btn': '+ 添加本地模型',
356
+ 'aggregator.local_badge': '本地 #{num}',
357
+ 'aggregator.local_models_empty': '请至少配置一个本地备用模型(例如 Ollama)。',
358
+
359
+ // Smart Routing Model
360
+ 'config.smart_routing_title': '智能提示词分流',
361
+ 'config.smart_routing_desc': '分析任务复杂度并自动匹配最佳预设。',
362
+ 'config.smart_routing_model': '分流决策模型:',
363
+
364
+ // Multi-Turn Context
365
+ 'aggregator.multi_turn_title': '多轮对话上下文与差异剪枝',
366
+ 'aggregator.multi_turn_desc': '跨轮次保留对话上下文并剪枝重复代码块。',
367
+
368
+ // UI States & Errors
369
+ 'status.loading': '正在加载 MoA 设置…',
370
+ 'status.unavailable': 'MoA 服务暂不可用',
371
+ 'status.retry': '重试',
372
+ 'status.error_saving': '保存失败:',
373
+ 'error_boundary.title': '⚠️ MoA 界面错误:',
374
+ 'error_boundary.retry': '重试',
375
+
376
+ // Diff Viewer
377
+ 'diff.select_run': '运行记录:',
378
+ 'diff.latest_run': '最新 / 当前工作区',
263
379
  'updater.success': '成功更新至 v{version},请重启 DSH 使其生效。',
264
380
  }
265
381
 
@@ -307,6 +423,55 @@ window.__ModuleLoader__.load({
307
423
  Chevron = FallbackChevron
308
424
  }
309
425
 
426
+
427
+ function renderSlotEditorList({ items, badgePrefix, onUpdate, onAdd, onRemove, addLabel, emptyWarning, availableModels, t }) {
428
+ return React.createElement(
429
+ 'div',
430
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
431
+ (Array.isArray(items) ? items : []).map((item, idx) =>
432
+ React.createElement(
433
+ 'div',
434
+ { key: idx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
435
+ React.createElement('span', { className: 'moa-cand-badge' }, `${badgePrefix} #${idx + 1}`),
436
+ React.createElement(
437
+ 'div',
438
+ { style: { flex: 1 } },
439
+ React.createElement(SearchableModelPicker, {
440
+ provider: (item && item.provider) || '',
441
+ model: (item && item.model) || '',
442
+ onChange: (prov, mod) => onUpdate(idx, prov, mod),
443
+ availableModels,
444
+ t,
445
+ })
446
+ ),
447
+ React.createElement(
448
+ 'button',
449
+ {
450
+ type: 'button',
451
+ className: 'moa-btn moa-btn-danger moa-btn-sub',
452
+ onClick: () => onRemove(idx),
453
+ },
454
+ '✕'
455
+ )
456
+ )
457
+ ),
458
+ React.createElement(
459
+ 'button',
460
+ {
461
+ type: 'button',
462
+ className: 'moa-btn moa-btn-sub',
463
+ style: { marginTop: 4, alignSelf: 'flex-start' },
464
+ onClick: onAdd,
465
+ },
466
+ addLabel
467
+ ),
468
+ emptyWarning ? React.createElement(
469
+ 'div',
470
+ { style: { fontSize: 11, color: 'var(--dsw-alias-state-warning-primary, #e6a23c)', marginTop: 2 } },
471
+ emptyWarning
472
+ ) : null
473
+ )
474
+ }
310
475
  const STYLES = `
311
476
  /* dsh-moa modern clinebot-aligned styling */
312
477
  .moa-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}
@@ -635,8 +800,25 @@ window.__ModuleLoader__.load({
635
800
  const [loading, setLoading] = React.useState(false)
636
801
  const [error, setError] = React.useState(null)
637
802
  const [hasFetched, setHasFetched] = React.useState(false)
803
+ const [historyRuns, setHistoryRuns] = React.useState([])
804
+ const [selectedRunId, setSelectedRunId] = React.useState('')
805
+
806
+ React.useEffect(() => {
807
+ fetch('/dsh-moa/history?limit=30', { cache: 'no-store' })
808
+ .then((r) => r.json())
809
+ .then((data) => {
810
+ if (data && data.ok && Array.isArray(data.runs)) {
811
+ setHistoryRuns(data.runs)
812
+ }
813
+ })
814
+ .catch(() => {})
815
+ }, [])
816
+
817
+ const selectedRun = historyRuns.find((r) => r.id === selectedRunId)
818
+ const candidateCount = selectedRun && Array.isArray(selectedRun.candidates) && selectedRun.candidates.length > 0
819
+ ? selectedRun.candidates.length
820
+ : Math.max(currentRefs.length || 0, 2)
638
821
 
639
- const candidateCount = Math.max(currentRefs.length || 0, 2)
640
822
  const candidateOptions = []
641
823
  for (let i = 1; i <= candidateCount; i++) {
642
824
  candidateOptions.push({
@@ -649,6 +831,7 @@ window.__ModuleLoader__.load({
649
831
  setLoading(true)
650
832
  setError(null)
651
833
  const params = new URLSearchParams()
834
+ if (selectedRunId) params.set('runId', selectedRunId)
652
835
  if (fromCandidate) params.set('from', fromCandidate)
653
836
  if (toCandidate) params.set('to', toCandidate)
654
837
  if (targetFile) params.set('file', targetFile)
@@ -679,11 +862,11 @@ window.__ModuleLoader__.load({
679
862
  setHasFetched(true)
680
863
  setError(err.message || String(err))
681
864
  })
682
- }, [fromCandidate, toCandidate, selectedFile])
865
+ }, [fromCandidate, toCandidate, selectedFile, selectedRunId])
683
866
 
684
867
  React.useEffect(() => {
685
868
  loadDiff()
686
- }, [fromCandidate, toCandidate])
869
+ }, [fromCandidate, toCandidate, selectedRunId])
687
870
 
688
871
  const handleFileChange = (e) => {
689
872
  const nextFile = e.target.value
@@ -707,6 +890,25 @@ window.__ModuleLoader__.load({
707
890
  React.createElement(
708
891
  'div',
709
892
  { className: 'moa-diff-controls' },
893
+ /* Run selector dropdown (#179) */
894
+ React.createElement(
895
+ 'label',
896
+ { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
897
+ t('diff.select_run') || 'Run:',
898
+ React.createElement(
899
+ 'select',
900
+ {
901
+ className: 'moa-diff-select',
902
+ value: selectedRunId,
903
+ onChange: (e) => setSelectedRunId(e.target.value),
904
+ },
905
+ React.createElement('option', { value: '' }, t('diff.latest_run') || 'Latest / Active Workspace'),
906
+ historyRuns.map((r) => {
907
+ const label = `${r.preset || 'default'} [${(r.id || '').slice(0, 8)}] ${(r.prompt || '').slice(0, 24)}...`
908
+ return React.createElement('option', { key: r.id, value: r.id }, label)
909
+ })
910
+ )
911
+ ),
710
912
  React.createElement(
711
913
  'label',
712
914
  { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
@@ -1331,7 +1533,7 @@ window.__ModuleLoader__.load({
1331
1533
  })
1332
1534
  )
1333
1535
  ),
1334
- /* Multi-Judge Panel & Consensus Voting */
1536
+ /* Multi-Judge Panel & Consensus Voting (#175, #176) */
1335
1537
  React.createElement(
1336
1538
  'div',
1337
1539
  { 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 +1546,62 @@ window.__ModuleLoader__.load({
1344
1546
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, multi_judge_enabled: e.target.checked })),
1345
1547
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1346
1548
  }),
1347
- 'Multi-Judge Panel & Consensus Voting'
1549
+ t('aggregator.multi_judge_title') || 'Multi-Judge Panel & Consensus Voting'
1348
1550
  ),
1349
1551
  React.createElement(
1350
1552
  'div',
1351
1553
  { 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.'
1554
+ t('aggregator.multi_judge_desc') || 'Deploy multiple judge models to score candidates and determine winner via consensus voting.'
1353
1555
  ),
1354
1556
  currentPreset.multi_judge_enabled && React.createElement(
1355
1557
  '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:'),
1558
+ { style: { marginLeft: 22, display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
1358
1559
  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 })),
1560
+ 'div',
1561
+ { style: { display: 'flex', alignItems: 'center', gap: 8 } },
1562
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.voting_strategy_label') || 'Voting Strategy:'),
1563
+ React.createElement(
1564
+ 'select',
1565
+ {
1566
+ className: 'moa-select',
1567
+ style: { width: 140, height: 28, fontSize: 12, padding: '0 6px' },
1568
+ value: currentPreset.judge_voting_strategy || 'majority',
1569
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, judge_voting_strategy: e.target.value })),
1570
+ },
1571
+ React.createElement('option', { value: 'majority' }, t('aggregator.voting_majority') || 'Majority Vote'),
1572
+ React.createElement('option', { value: 'highest_score' }, t('aggregator.voting_highest_score') || 'Highest Score'),
1573
+ React.createElement('option', { value: 'unanimous' }, t('aggregator.voting_unanimous') || 'Unanimous')
1574
+ )
1575
+ ),
1576
+ renderSlotEditorList({
1577
+ items: currentPreset.judge_models,
1578
+ badgePrefix: 'Judge',
1579
+ onUpdate: (idx, prov, mod) => {
1580
+ updateCurrentPreset((p) => {
1581
+ const list = [...(p.judge_models || [])]
1582
+ list[idx] = { provider: prov, model: mod }
1583
+ return { ...p, judge_models: list }
1584
+ })
1365
1585
  },
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
- )
1586
+ onRemove: (idx) => {
1587
+ updateCurrentPreset((p) => ({
1588
+ ...p,
1589
+ judge_models: (p.judge_models || []).filter((_, i) => i !== idx),
1590
+ }))
1591
+ },
1592
+ onAdd: () => {
1593
+ updateCurrentPreset((p) => ({
1594
+ ...p,
1595
+ judge_models: [...(p.judge_models || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1596
+ }))
1597
+ },
1598
+ addLabel: t('aggregator.add_judge_btn') || '+ Add Judge Model',
1599
+ emptyWarning: (!currentPreset.judge_models || currentPreset.judge_models.length < 2)
1600
+ ? (t('aggregator.judge_models_empty') || 'At least 2 judge models are required for consensus voting.')
1601
+ : null,
1602
+ availableModels,
1603
+ t,
1604
+ })
1370
1605
  )
1371
1606
  ),
1372
1607
 
@@ -1383,12 +1618,12 @@ window.__ModuleLoader__.load({
1383
1618
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, composite_merge_enabled: e.target.checked })),
1384
1619
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1385
1620
  }),
1386
- 'Composite Hybrid Synthesis (AST / Block Merge)'
1621
+ t('aggregator.composite_title') || 'Composite Hybrid Synthesis (AST / Block Merge)'
1387
1622
  ),
1388
1623
  React.createElement(
1389
1624
  'div',
1390
1625
  { 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.'
1626
+ t('aggregator.composite_desc') || 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.'
1392
1627
  )
1393
1628
  ),
1394
1629
 
@@ -1405,12 +1640,12 @@ window.__ModuleLoader__.load({
1405
1640
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, budget_guard_enabled: e.target.checked })),
1406
1641
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1407
1642
  }),
1408
- 'Cost Budget Guardrails & Auto-Fallback'
1643
+ t('aggregator.budget_title') || 'Cost Budget Guardrails & Auto-Fallback'
1409
1644
  ),
1410
1645
  React.createElement(
1411
1646
  'div',
1412
1647
  { 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.'
1648
+ t('aggregator.budget_desc') || 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.'
1414
1649
  ),
1415
1650
  currentPreset.budget_guard_enabled && React.createElement(
1416
1651
  'div',
@@ -1418,7 +1653,7 @@ window.__ModuleLoader__.load({
1418
1653
  React.createElement(
1419
1654
  'div',
1420
1655
  { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1421
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Max USD ($):'),
1656
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.budget_max_usd') || 'Max USD ($):'),
1422
1657
  React.createElement('input', {
1423
1658
  type: 'number',
1424
1659
  step: '0.01',
@@ -1434,7 +1669,7 @@ window.__ModuleLoader__.load({
1434
1669
  React.createElement(
1435
1670
  'div',
1436
1671
  { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1437
- React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Action:'),
1672
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.budget_action') || 'Action:'),
1438
1673
  React.createElement(
1439
1674
  'select',
1440
1675
  {
@@ -1443,8 +1678,8 @@ window.__ModuleLoader__.load({
1443
1678
  value: currentPreset.budget_action || 'trim',
1444
1679
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, budget_action: e.target.value })),
1445
1680
  },
1446
- React.createElement('option', { value: 'trim' }, 'Trim Pool'),
1447
- React.createElement('option', { value: 'abort' }, 'Abort')
1681
+ React.createElement('option', { value: 'trim' }, t('aggregator.budget_action_trim') || 'Trim Pool'),
1682
+ React.createElement('option', { value: 'abort' }, t('aggregator.budget_action_abort') || 'Abort')
1448
1683
  )
1449
1684
  )
1450
1685
  )
@@ -1463,16 +1698,16 @@ window.__ModuleLoader__.load({
1463
1698
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, report_generation_enabled: e.target.checked })),
1464
1699
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1465
1700
  }),
1466
- 'Automated Benchmark & Post-Mortem Report'
1701
+ t('aggregator.report_title') || 'Automated Benchmark & Post-Mortem Report'
1467
1702
  ),
1468
1703
  React.createElement(
1469
1704
  'div',
1470
1705
  { 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.'
1706
+ t('aggregator.report_desc') || 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.'
1472
1707
  )
1473
1708
  ),
1474
1709
 
1475
- /* Graceful Degradation & Local Fallback */
1710
+ /* Graceful Degradation & Local Fallback (#175, #176) */
1476
1711
  React.createElement(
1477
1712
  'div',
1478
1713
  { 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 +1720,154 @@ window.__ModuleLoader__.load({
1485
1720
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, local_fallback_enabled: e.target.checked })),
1486
1721
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1487
1722
  }),
1488
- 'Graceful Degradation & Local Fallback'
1723
+ t('aggregator.local_fallback_title') || 'Graceful Degradation & Local Fallback'
1489
1724
  ),
1490
1725
  React.createElement(
1491
1726
  'div',
1492
1727
  { 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.'
1728
+ t('aggregator.local_fallback_desc') || 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.'
1729
+ ),
1730
+ currentPreset.local_fallback_enabled && React.createElement(
1731
+ 'div',
1732
+ { style: { marginLeft: 22, marginTop: 4 } },
1733
+ renderSlotEditorList({
1734
+ items: currentPreset.local_fallback_models,
1735
+ badgePrefix: 'Local',
1736
+ onUpdate: (idx, prov, mod) => {
1737
+ updateCurrentPreset((p) => {
1738
+ const list = [...(p.local_fallback_models || [])]
1739
+ list[idx] = { provider: prov, model: mod }
1740
+ return { ...p, local_fallback_models: list }
1741
+ })
1742
+ },
1743
+ onRemove: (idx) => {
1744
+ updateCurrentPreset((p) => ({
1745
+ ...p,
1746
+ local_fallback_models: (p.local_fallback_models || []).filter((_, i) => i !== idx),
1747
+ }))
1748
+ },
1749
+ onAdd: () => {
1750
+ updateCurrentPreset((p) => ({
1751
+ ...p,
1752
+ local_fallback_models: [...(p.local_fallback_models || []), { provider: 'ollama', model: '' }],
1753
+ }))
1754
+ },
1755
+ addLabel: t('aggregator.add_local_btn') || '+ Add Local Model',
1756
+ emptyWarning: (!currentPreset.local_fallback_models || currentPreset.local_fallback_models.length === 0)
1757
+ ? (t('aggregator.local_models_empty') || 'Configure at least one local fallback model (e.g. Ollama).')
1758
+ : null,
1759
+ availableModels,
1760
+ t,
1761
+ })
1494
1762
  )
1495
1763
  ),
1496
1764
 
1497
- /* Fallback Judges Chain */
1765
+ /* Smart Prompt Routing (#176) */
1498
1766
  React.createElement(
1499
1767
  'div',
1500
- { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
1768
+ { 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
1769
  React.createElement(
1502
- 'div',
1503
- { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
1504
- t('aggregator.fallbacks_title')
1770
+ 'label',
1771
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
1772
+ React.createElement('input', {
1773
+ type: 'checkbox',
1774
+ checked: Boolean(currentPreset.smart_routing_enabled),
1775
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, smart_routing_enabled: e.target.checked })),
1776
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1777
+ }),
1778
+ t('config.smart_routing_title') || 'Smart Prompt Routing'
1505
1779
  ),
1506
1780
  React.createElement(
1507
1781
  '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
- )
1782
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1783
+ t('config.smart_routing_desc') || 'Analyze task complexity and automatically select the optimal preset.'
1548
1784
  ),
1549
- React.createElement(
1785
+ currentPreset.smart_routing_enabled && React.createElement(
1550
1786
  'div',
1551
- null,
1787
+ { style: { marginLeft: 22, marginTop: 4, display: 'flex', alignItems: 'center', gap: 8 } },
1788
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('config.smart_routing_model') || 'Routing Model:'),
1552
1789
  React.createElement(
1553
- 'button',
1554
- {
1555
- type: 'button',
1556
- className: 'moa-btn moa-btn-sub',
1557
- style: { marginTop: 4 },
1558
- onClick: () => {
1790
+ 'div',
1791
+ { style: { flex: 1, maxWidth: 320 } },
1792
+ React.createElement(SearchableModelPicker, {
1793
+ provider: currentPreset.smart_routing_model?.provider || '',
1794
+ model: currentPreset.smart_routing_model?.model || '',
1795
+ onChange: (prov, mod) => {
1559
1796
  updateCurrentPreset((p) => ({
1560
1797
  ...p,
1561
- aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1798
+ smart_routing_model: { provider: prov, model: mod },
1562
1799
  }))
1563
1800
  },
1564
- },
1565
- t('aggregator.add_fallback_btn')
1801
+ availableModels,
1802
+ t,
1803
+ })
1566
1804
  )
1567
1805
  )
1806
+ ),
1807
+
1808
+ /* Multi-Turn Context & Diff Pruning (#176) */
1809
+ React.createElement(
1810
+ 'div',
1811
+ { 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 } },
1812
+ React.createElement(
1813
+ 'label',
1814
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
1815
+ React.createElement('input', {
1816
+ type: 'checkbox',
1817
+ checked: currentPreset.multi_turn_enabled !== false,
1818
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, multi_turn_enabled: e.target.checked })),
1819
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1820
+ }),
1821
+ t('aggregator.multi_turn_title') || 'Multi-Turn Context & Diff Pruning'
1822
+ ),
1823
+ React.createElement(
1824
+ 'div',
1825
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1826
+ t('aggregator.multi_turn_desc') || 'Retain conversation context across turns while pruning duplicate code blocks.'
1827
+ )
1828
+ ),
1829
+
1830
+ /* Fallback Judges Chain */
1831
+ React.createElement(
1832
+ 'div',
1833
+ { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
1834
+ React.createElement(
1835
+ 'div',
1836
+ { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
1837
+ t('aggregator.fallbacks_title')
1838
+ ),
1839
+ React.createElement(
1840
+ 'div',
1841
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } },
1842
+ t('aggregator.fallbacks_desc')
1843
+ ),
1844
+ renderSlotEditorList({
1845
+ items: currentPreset.aggregator_fallbacks,
1846
+ badgePrefix: 'Fallback',
1847
+ onUpdate: (idx, prov, mod) => {
1848
+ updateCurrentPreset((p) => {
1849
+ const list = [...(p.aggregator_fallbacks || [])]
1850
+ list[idx] = { provider: prov, model: mod }
1851
+ return { ...p, aggregator_fallbacks: list }
1852
+ })
1853
+ },
1854
+ onRemove: (idx) => {
1855
+ updateCurrentPreset((p) => ({
1856
+ ...p,
1857
+ aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== idx),
1858
+ }))
1859
+ },
1860
+ onAdd: () => {
1861
+ updateCurrentPreset((p) => ({
1862
+ ...p,
1863
+ aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1864
+ }))
1865
+ },
1866
+ addLabel: t('aggregator.add_fallback_btn'),
1867
+ emptyWarning: null,
1868
+ availableModels,
1869
+ t,
1870
+ })
1568
1871
  )
1569
1872
  )
1570
1873
  )
@@ -2021,6 +2324,9 @@ window.__ModuleLoader__.load({
2021
2324
  )
2022
2325
  }
2023
2326
 
2327
+ const UNAVAILABLE_SNAPSHOT = Object.freeze({ status: 'unavailable' })
2328
+ const LOADING_SNAPSHOT = Object.freeze({ status: 'loading' })
2329
+
2024
2330
  function useMoASettings(props) {
2025
2331
  const effectiveCtx = (props && props.ctx) || rootCtx
2026
2332
  // English is canonical; prefer the core-provided translator for the
@@ -2033,12 +2339,12 @@ window.__ModuleLoader__.load({
2033
2339
  return s && typeof s.get === 'function' ? s.get(NS) : undefined
2034
2340
  }, [effectiveCtx])
2035
2341
 
2342
+ const getSnapshot = React.useCallback(() => (scope ? scope.getSnapshot() : UNAVAILABLE_SNAPSHOT), [scope])
2343
+ const getServerSnapshot = React.useCallback(() => LOADING_SNAPSHOT, [])
2344
+ const subscribe = React.useCallback((cb) => (scope ? scope.subscribe(cb) : () => {}), [scope])
2345
+
2036
2346
  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
- )
2347
+ ? React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
2042
2348
  : null
2043
2349
 
2044
2350
  const [status, setStatus] = React.useState('loading')
@@ -2052,6 +2358,7 @@ window.__ModuleLoader__.load({
2052
2358
  currentVersion: '0.2.16',
2053
2359
  latestVersion: undefined,
2054
2360
  updateAvailable: false,
2361
+ canAutoUpdate: true,
2055
2362
  notice: null,
2056
2363
  error: null,
2057
2364
  })
@@ -2068,10 +2375,11 @@ window.__ModuleLoader__.load({
2068
2375
  currentVersion: data.currentVersion,
2069
2376
  latestVersion: data.latestVersion,
2070
2377
  updateAvailable: Boolean(data.updateAvailable),
2378
+ canAutoUpdate: data.canAutoUpdate !== undefined ? Boolean(data.canAutoUpdate) : prev.canAutoUpdate,
2071
2379
  error: data.latestCheckFailed ? (t('updater.checkFailed') || 'Registry check failed') : null,
2072
2380
  }))
2073
2381
  } else {
2074
- setUpdateState((prev) => ({ ...prev, checking: false, error: data.error || 'Check failed' }))
2382
+ setUpdateState((prev) => ({ ...prev, checking: false, error: data?.error || 'Check failed' }))
2075
2383
  }
2076
2384
  } catch (err) {
2077
2385
  setUpdateState((prev) => ({ ...prev, checking: false, error: String(err?.message || err) }))
@@ -2086,19 +2394,20 @@ window.__ModuleLoader__.load({
2086
2394
  headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' },
2087
2395
  })
2088
2396
  const data = await res.json()
2089
- if (data && data.updated) {
2397
+ if (data && (data.updated || data.updatedVersion || data.restartRequired || data.ok)) {
2398
+ const nextVer = data.updatedVersion || data.latestVersion || data.currentVersion
2090
2399
  setUpdateState((prev) => ({
2091
2400
  ...prev,
2092
2401
  updating: false,
2093
2402
  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),
2403
+ currentVersion: nextVer,
2404
+ notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}', nextVer),
2096
2405
  }))
2097
2406
  } else {
2098
2407
  setUpdateState((prev) => ({
2099
2408
  ...prev,
2100
2409
  updating: false,
2101
- error: data.error || data.message || 'Update failed',
2410
+ error: data?.error || data?.message || 'Update failed',
2102
2411
  }))
2103
2412
  }
2104
2413
  } catch (err) {
@@ -2258,7 +2567,7 @@ window.__ModuleLoader__.load({
2258
2567
  if (res.defaultPreset) setDefaultPreset(res.defaultPreset)
2259
2568
  setTimeout(() => setSaveStatus(''), 3000)
2260
2569
  } else {
2261
- setSaveStatus('Error saving: ' + (res?.error || 'failed'))
2570
+ setSaveStatus((t('status.error_saving') || 'Error saving: ') + (res?.error || 'failed'))
2262
2571
  }
2263
2572
  })
2264
2573
  .catch((err) => {