@goodandready/dsh-moa 0.2.41 → 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/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +11 -0
- package/lib/client.js +407 -98
- package/lib/history.js +1 -0
- package/lib/index.js +21 -3
- package/lib/live-canvas.js +39 -6
- package/lib/moa-runner.js +6 -3
- package/lib/moa-stream.js +57 -1
- package/lib/routes.js +3 -2
- package/lib/updater.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,13 @@ The flag form is equivalent:
|
|
|
116
116
|
/moa --preset=deep-reasoning solve this math problem step by step
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
Or promote a candidate solution directly (by candidate index or runId + candidate index):
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
/moa promote 2
|
|
123
|
+
/moa promote <runId> 2
|
|
124
|
+
```
|
|
125
|
+
|
|
119
126
|
### 2. Adaptive Questionnaire Gate
|
|
120
127
|
When prompts are open-ended or lack architectural specifications (e.g. *"build a calculator app"*), advisor models detect ambiguities and formulate focused clarifying questions (e.g., UI style, persistence backend, framework choice) before generating code.
|
|
121
128
|
|
package/README.ru.md
CHANGED
|
@@ -116,6 +116,13 @@ graph TD
|
|
|
116
116
|
/moa --preset=deep-reasoning реши эту математическую задачу по шагам
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
Или прямое продвижение решения кандидата (по индексу кандидата или runId + индексу):
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
/moa promote 2
|
|
123
|
+
/moa promote <runId> 2
|
|
124
|
+
```
|
|
125
|
+
|
|
119
126
|
### 2. Адаптивный гейт уточнения требований
|
|
120
127
|
Когда запрос сформулирован слишком обобщенно (например, *"сделай калькулятор"*), советники определяют недостающие архитектурные требования и формулируют целевые вопросы (стиль интерфейса, сохранение состояния, стек технологий) до генерации кода.
|
|
121
128
|
|
package/README.zh.md
CHANGED
|
@@ -116,6 +116,13 @@ graph TD
|
|
|
116
116
|
/moa --preset=deep-reasoning 逐步求解这道数学题
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
或直接提升候选者方案(支持指定 runId 与候选序号):
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
/moa promote 2
|
|
123
|
+
/moa promote <runId> 2
|
|
124
|
+
```
|
|
125
|
+
|
|
119
126
|
### 2. 自适应澄清问卷
|
|
120
127
|
当需求过于抽象(例如 *"制作一个计算器"*)时,系统会在生成代码前主动询问 UI 风格、数据持久化方式或框架偏好。
|
|
121
128
|
|
|
@@ -128,6 +135,7 @@ graph TD
|
|
|
128
135
|
* 各提案模型在 `.moa/candidate-1/`、`.moa/candidate-2/` 等独立目录生成工程代码。
|
|
129
136
|
* 裁判模型对比各版本实现,通过 `WINNER_CANDIDATE_INDEX: N` 指定最优方案。
|
|
130
137
|
* 胜出方案自动提升至项目根目录,临时沙箱随后自动清理。
|
|
138
|
+
* 支持随时通过命令 `/moa promote [runId] <candidateIndex>` 或 UI 界面一键手动提升指定候选者的方案。
|
|
131
139
|
|
|
132
140
|
### 5. 原生设置卡片与预设管理
|
|
133
141
|
在 `设置 → 插件 → Mixture of Agents` 中可视化配置模型:
|
|
@@ -234,6 +242,9 @@ dsh-moa:
|
|
|
234
242
|
| `/dsh-moa/leaderboard` | `GET` | 计算模型胜率排行榜与平均执行成本 |
|
|
235
243
|
| `/dsh-moa/runs/<id>` | `GET` | 按 id 返回单条运行记录 |
|
|
236
244
|
| `/dsh-moa/run` | `POST` | 通过 HTTP 运行完整 MoA 管线(`enabled: false` 时返回 400) |
|
|
245
|
+
| `/dsh-moa/diff` | `GET` | 计算候选运行结果或综合输出的行级别差异(支持按 runId 解析工作区) |
|
|
246
|
+
| `/dsh-moa/promote` | `POST` | 手动将候选工作区文件提升到项目根目录(带时间机器快照) |
|
|
247
|
+
| `/api/dsh-moa/update` | `POST` | 从 npm 一键更新插件,带安全写入源校验 |
|
|
237
248
|
|
|
238
249
|
---
|
|
239
250
|
|
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',
|
|
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
|
-
'
|
|
1360
|
-
{
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
/*
|
|
1765
|
+
/* Smart Prompt Routing (#176) */
|
|
1498
1766
|
React.createElement(
|
|
1499
1767
|
'div',
|
|
1500
|
-
{ style: {
|
|
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
|
-
'
|
|
1503
|
-
{ style: {
|
|
1504
|
-
|
|
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)',
|
|
1509
|
-
t('
|
|
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
|
-
|
|
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
|
-
'
|
|
1554
|
-
{
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1798
|
+
smart_routing_model: { provider: prov, model: mod },
|
|
1562
1799
|
}))
|
|
1563
1800
|
},
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
|
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:
|
|
2095
|
-
notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}',
|
|
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
|
|
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) => {
|
package/lib/history.js
CHANGED
|
@@ -86,6 +86,7 @@ function normalizeRunRecord(record) {
|
|
|
86
86
|
preset: record.preset || 'default',
|
|
87
87
|
isRefinement: Boolean(record.isRefinement),
|
|
88
88
|
isFastMode: Boolean(record.isFastMode),
|
|
89
|
+
cwd: record.cwd ? path.resolve(record.cwd) : undefined,
|
|
89
90
|
candidates: Array.isArray(record.candidates)
|
|
90
91
|
? record.candidates.map((c, idx) => ({
|
|
91
92
|
index: typeof c.index === 'number' ? c.index : (idx + 1),
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createLlmCaller } from './moa-llm.js'
|
|
|
12
12
|
import {
|
|
13
13
|
parseMoACommand,
|
|
14
14
|
streamMoATurn,
|
|
15
|
+
streamPromoteTurn,
|
|
15
16
|
} from './moa-runner.js'
|
|
16
17
|
import { registerMoaRoutes } from './routes.js'
|
|
17
18
|
import { resolvePresetForPrompt } from './moa-router.js'
|
|
@@ -174,6 +175,20 @@ export function apply(ctx, config) {
|
|
|
174
175
|
if (cfg.enabled !== false) {
|
|
175
176
|
const effectiveDefault = cfg.default_preset || 'default'
|
|
176
177
|
const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
|
|
178
|
+
if (parsed.isPromote) {
|
|
179
|
+
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
180
|
+
const turnContext = {
|
|
181
|
+
isPromoteTurn: true,
|
|
182
|
+
runId: parsed.runId,
|
|
183
|
+
candidateIndex: parsed.candidateIndex,
|
|
184
|
+
cwd: sessCwd,
|
|
185
|
+
webServerPort: ctx.webServer?.port,
|
|
186
|
+
timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
|
|
187
|
+
}
|
|
188
|
+
if (event.signal) moaPendingTurns.set(event.signal, turnContext)
|
|
189
|
+
if (event.session?.id) setSessionTurn(event.session.id, turnContext)
|
|
190
|
+
return next()
|
|
191
|
+
}
|
|
177
192
|
const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
|
|
178
193
|
if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
|
|
179
194
|
try {
|
|
@@ -234,11 +249,11 @@ export function apply(ctx, config) {
|
|
|
234
249
|
const turnContext = (payload.signal ? moaPendingTurns.get(payload.signal) : null)
|
|
235
250
|
|| (payload.session?.id ? getSessionTurn(payload.session.id) : null)
|
|
236
251
|
|
|
237
|
-
if (turnContext) {
|
|
252
|
+
if (turnContext && !turnContext.isPromoteTurn) {
|
|
238
253
|
return {
|
|
239
254
|
...resolved,
|
|
240
|
-
provider: turnContext.aggregator.provider,
|
|
241
|
-
model: turnContext.aggregator.model,
|
|
255
|
+
provider: turnContext.aggregator?.provider || resolved.provider,
|
|
256
|
+
model: turnContext.aggregator?.model || resolved.model,
|
|
242
257
|
}
|
|
243
258
|
}
|
|
244
259
|
return resolved
|
|
@@ -254,6 +269,9 @@ export function apply(ctx, config) {
|
|
|
254
269
|
if (options.signal) moaPendingTurns.delete(options.signal)
|
|
255
270
|
if (options.session?.id) consumeSessionTurn(options.session.id)
|
|
256
271
|
|
|
272
|
+
if (turnContext.isPromoteTurn) {
|
|
273
|
+
return streamPromoteTurn(turnContext, options)
|
|
274
|
+
}
|
|
257
275
|
return streamMoATurn(turnContext, options)
|
|
258
276
|
}
|
|
259
277
|
|
package/lib/live-canvas.js
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import fs from "node:fs/promises"
|
|
2
3
|
|
|
3
|
-
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
4
|
+
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, options = {}) {
|
|
4
5
|
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== "function") return null
|
|
5
6
|
const htmlRel = (promotedFiles || []).find((f) => f.endsWith(".html") || f.endsWith(".htm"))
|
|
6
7
|
if (!htmlRel) return null
|
|
8
|
+
|
|
7
9
|
let content = null
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
// 1. Try reading directly from disk in cwd (the promoted winner or curator synthesis)
|
|
12
|
+
if (cwd) {
|
|
13
|
+
try {
|
|
14
|
+
const fullPath = path.resolve(cwd, htmlRel)
|
|
15
|
+
content = await fs.readFile(fullPath, "utf8")
|
|
16
|
+
} catch {
|
|
17
|
+
content = null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Fallback to synthesized files / winning files
|
|
22
|
+
if (!content) {
|
|
23
|
+
const winningFiles = options.winningFiles || options.synthesizedFiles
|
|
24
|
+
if (Array.isArray(winningFiles)) {
|
|
25
|
+
const block = winningFiles.find((f) => f.relativePath === htmlRel)
|
|
26
|
+
if (block?.content) content = block.content
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Fallback to winning candidate by index
|
|
31
|
+
if (!content && typeof options.winningIndex === 'number' && referenceOutputs) {
|
|
32
|
+
const winnerRef = referenceOutputs[options.winningIndex - 1]
|
|
33
|
+
const block = (winnerRef?.files || []).find((f) => f.relativePath === htmlRel)
|
|
34
|
+
if (block?.content) content = block.content
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 4. Fallback to searching referenceOutputs
|
|
38
|
+
if (!content) {
|
|
39
|
+
for (const r of referenceOutputs || []) {
|
|
40
|
+
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
41
|
+
if (block?.content) {
|
|
42
|
+
content = block.content
|
|
43
|
+
break
|
|
44
|
+
}
|
|
13
45
|
}
|
|
14
46
|
}
|
|
47
|
+
|
|
15
48
|
if (!content) return null
|
|
16
49
|
try {
|
|
17
50
|
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|
package/lib/moa-runner.js
CHANGED
|
@@ -18,7 +18,7 @@ export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSu
|
|
|
18
18
|
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
19
19
|
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
20
20
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
21
|
-
export { streamMoATurn } from './moa-stream.js'
|
|
21
|
+
export { streamMoATurn, streamPromoteTurn } from './moa-stream.js'
|
|
22
22
|
/**
|
|
23
23
|
* Executes the full Mixture of Agents pipeline.
|
|
24
24
|
*/
|
|
@@ -229,6 +229,7 @@ export async function runMoAPipeline({
|
|
|
229
229
|
const durationMs = Date.now() - startTime
|
|
230
230
|
try {
|
|
231
231
|
await recordMoaRunAsync({
|
|
232
|
+
cwd,
|
|
232
233
|
prompt: userPrompt,
|
|
233
234
|
preset: preset?.name || 'default',
|
|
234
235
|
isRefinement,
|
|
@@ -281,13 +282,14 @@ export async function runMoAPipeline({
|
|
|
281
282
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
285
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
|
|
285
286
|
const totalTokens = single.usage?.totalTokens || 0
|
|
286
287
|
const totalCostUsd = Number((single.costUsd || 0).toFixed(5))
|
|
287
288
|
const durationMs = Date.now() - startTime
|
|
288
289
|
|
|
289
290
|
try {
|
|
290
291
|
await recordMoaRunAsync({
|
|
292
|
+
cwd,
|
|
291
293
|
prompt: userPrompt,
|
|
292
294
|
preset: preset?.name || 'default',
|
|
293
295
|
isRefinement,
|
|
@@ -512,7 +514,7 @@ export async function runMoAPipeline({
|
|
|
512
514
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
513
515
|
}
|
|
514
516
|
|
|
515
|
-
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
517
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
|
|
516
518
|
const durationMs = Date.now() - startTime
|
|
517
519
|
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
|
|
518
520
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
@@ -542,6 +544,7 @@ export async function runMoAPipeline({
|
|
|
542
544
|
try {
|
|
543
545
|
await recordMoaRunAsync({
|
|
544
546
|
id: runId,
|
|
547
|
+
cwd,
|
|
545
548
|
prompt: userPrompt,
|
|
546
549
|
preset: preset?.name || 'default',
|
|
547
550
|
isRefinement,
|
package/lib/moa-stream.js
CHANGED
|
@@ -3,7 +3,8 @@ import crypto from 'node:crypto'
|
|
|
3
3
|
* Zero-latency Live Streaming Generator for MoA Turns
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { cleanMoaWorkspaces } from './file-workspace.js'
|
|
6
|
+
import { cleanMoaWorkspaces, promoteCandidateWorkspace } from './file-workspace.js'
|
|
7
|
+
import { isTestEnvironment } from './history.js'
|
|
7
8
|
import { formatMoAResponse } from './moa-parser.js'
|
|
8
9
|
import { runMoAPipeline } from './moa-runner.js'
|
|
9
10
|
|
|
@@ -132,3 +133,58 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Streams a /moa promote candidate turn directly to chat with zero LLM calls (#177).
|
|
139
|
+
*/
|
|
140
|
+
export async function* streamPromoteTurn({ runId, candidateIndex, cwd, webServerPort, timeMachineEngine, force = isTestEnvironment() }, options = {}) {
|
|
141
|
+
const signal = options?.signal
|
|
142
|
+
if (signal?.aborted) return
|
|
143
|
+
|
|
144
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
145
|
+
|
|
146
|
+
if (typeof candidateIndex !== 'number' || isNaN(candidateIndex) || candidateIndex < 1) {
|
|
147
|
+
const errText = '⚠️ **MoA Promote Error**: Valid candidate index required (>= 1).'
|
|
148
|
+
yield { type: 'text-delta', index: 0, text: errText }
|
|
149
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
150
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
yield { type: 'text-delta', index: 0, text: `🔄 *Promoting candidate ${candidateIndex}${runId ? ` for run \`${runId}\`` : ''}...*\n\n` }
|
|
156
|
+
|
|
157
|
+
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
158
|
+
runId,
|
|
159
|
+
keepMoa: true,
|
|
160
|
+
port: webServerPort,
|
|
161
|
+
timeMachineEngine,
|
|
162
|
+
force: Boolean(force || options?.force),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (!promotedFiles || promotedFiles.length === 0) {
|
|
166
|
+
const noFilesText = `⚠️ **MoA Promote Notice**: No candidate files found for candidate ${candidateIndex}${runId ? ` in run \`${runId}\`` : ''}. Workspace may have expired or run does not exist.`
|
|
167
|
+
yield { type: 'text-delta', index: 0, text: noFilesText }
|
|
168
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: noFilesText } }
|
|
169
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fileList = promotedFiles.map((f) => `\`${f}\``).join(', ')
|
|
174
|
+
const checkpointId = promotedFiles.checkpoint || null
|
|
175
|
+
let successMsg = `✅ **MoA Candidate ${candidateIndex} promoted successfully!**\n\n`
|
|
176
|
+
successMsg += `> 📦 **Files applied to workspace**: ${fileList}\n`
|
|
177
|
+
if (checkpointId) {
|
|
178
|
+
successMsg += `> 🛡️ **Time Machine Checkpoint**: \`${checkpointId}\`\n`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
yield { type: 'text-delta', index: 0, text: successMsg }
|
|
182
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: successMsg } }
|
|
183
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const errText = `⚠️ **MoA Promote Failed**: ${err?.message || String(err)}`
|
|
186
|
+
yield { type: 'text-delta', index: 0, text: errText }
|
|
187
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
188
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
189
|
+
}
|
|
190
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -356,7 +356,9 @@ ctx.effect(() => {
|
|
|
356
356
|
}
|
|
357
357
|
try {
|
|
358
358
|
const u = new URL(req.url, 'http://localhost')
|
|
359
|
-
const
|
|
359
|
+
const runId = u.searchParams.get('runId') || undefined
|
|
360
|
+
const run = runId ? getMoaRunById(runId) : null
|
|
361
|
+
const cwd = run?.cwd || u.searchParams.get('cwd') || process.cwd()
|
|
360
362
|
const sanitizeTarget = (val, def) => {
|
|
361
363
|
const clean = String(val || '').trim().replace(/[^a-zA-Z0-9_-]/g, '')
|
|
362
364
|
return clean || def
|
|
@@ -366,7 +368,6 @@ ctx.effect(() => {
|
|
|
366
368
|
const rawFile = u.searchParams.get('file') || ''
|
|
367
369
|
const file = (rawFile && isSafeRelativePath(rawFile)) ? rawFile : ''
|
|
368
370
|
|
|
369
|
-
const runId = u.searchParams.get('runId') || undefined
|
|
370
371
|
const fromFiles = await readCandidateFiles(cwd, from, { runId })
|
|
371
372
|
const toFiles = await readCandidateFiles(cwd, to, { runId })
|
|
372
373
|
|
package/lib/updater.js
CHANGED
|
@@ -43,18 +43,39 @@ export function isTrustedUpdateRequest(request) {
|
|
|
43
43
|
|
|
44
44
|
/** General safe write check for plugin write endpoints (CSRF / Cross-Site protection) */
|
|
45
45
|
export function isSafeWriteRequest(request) {
|
|
46
|
+
const remote = request?.socket?.remoteAddress || request?.connection?.remoteAddress;
|
|
47
|
+
const isLoop = !remote || isLoopback(remote);
|
|
48
|
+
|
|
46
49
|
const site = header(request, 'sec-fetch-site');
|
|
47
50
|
if (site === 'cross-site') return false;
|
|
51
|
+
|
|
48
52
|
const origin = header(request, 'origin');
|
|
49
53
|
const host = header(request, 'host');
|
|
54
|
+
|
|
55
|
+
// If non-loopback caller, require valid same-origin browser proof
|
|
56
|
+
if (!isLoop) {
|
|
57
|
+
if (!origin || !host) return false;
|
|
58
|
+
if (site && site !== 'same-origin' && site !== 'none') return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Same-site without matching origin is rejected regardless of remote
|
|
62
|
+
if (site === 'same-site') {
|
|
63
|
+
if (!origin || !host) return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
if (origin) {
|
|
67
|
+
if (origin === 'null') return false;
|
|
51
68
|
try {
|
|
52
69
|
const url = new URL(origin);
|
|
53
70
|
if (host && url.host !== host) return false;
|
|
71
|
+
if (!isLoop) {
|
|
72
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
73
|
+
}
|
|
54
74
|
} catch {
|
|
55
75
|
return false;
|
|
56
76
|
}
|
|
57
77
|
}
|
|
78
|
+
|
|
58
79
|
return true;
|
|
59
80
|
}
|
|
60
81
|
|
|
@@ -353,6 +374,8 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
353
374
|
}
|
|
354
375
|
await installExact(target, `${options.packageName}@${before.latestVersion}`, options);
|
|
355
376
|
json(response, 200, {
|
|
377
|
+
ok: true,
|
|
378
|
+
updated: true,
|
|
356
379
|
...before,
|
|
357
380
|
updatedVersion: before.latestVersion,
|
|
358
381
|
restartRequired: true,
|