@goodandready/dsh-moa 0.2.9 → 0.2.10

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.
@@ -0,0 +1,57 @@
1
+ # План: разбиение lib/client.js (issue #47)
2
+
3
+ Статус: принято решение, реализация отложена до появления подтверждённой механики бандлера.
4
+ Дата: 2026-09-11. Исполнитель: zcode. Связанная issue: #47.
5
+
6
+ ## Контекст
7
+
8
+ Корневой чек-лист code review требует файл ≤800 строк; `lib/client.js` — ~1240 строк
9
+ после батча качества. `MoAEditor` ≈500 строк, словари ≈110 строк, CSS ≈85 строк.
10
+ Прямое разбиение упирается в механику клиентского бандлера DSH.
11
+
12
+ ## Собранный evidence (2026-09-11)
13
+
14
+ 1. **Харнесс отдаёт ровно один модуль на пакет**: на тест-контуре (DSH 0.1.5-rc.2)
15
+ клиент плагина обслуживается по единому URL
16
+ `/plugins/??@goodandready/dsh-moa/client.js&rev=<hash>` (combo-загрузчик);
17
+ реестр модулей в индекс-странице ссылается только на `client.js` пакета.
18
+ 2. **Ни один плагин семейства не использует относительный require в клиентском
19
+ коде**: dsh-lanmode (23 файла в lib/), dsh-tts (client.js 1851 строка),
20
+ dsh-model-sync (17 файлов), dsh-clinebot (5 файлов), dsh-grok-xsearch (14
21
+ файлов) — везде `require('./...')` в client.js отсутствует. Многофайловость
22
+ живёт только в серверной половине.
23
+ 3. Клиентская factory получает `require` (пакетные зависимости: `react`,
24
+ `@deepseek-ai/dsh-client-ui-primitives`) — документированных свидетельств
25
+ поддержки относительных require внутри клиентского модуля нет.
26
+
27
+ ## Варианты
28
+
29
+ - **A. Оставить один файл (принято сейчас).** Нулевой риск для загрузки карточки;
30
+ известный компромисс с чек-листом зафиксирован в issue.
31
+ - **B. Сборочная склейка (concat/минимальный bundler перед `npm pack`).** С.Split
32
+ исходников на части + prepublish-скрипт, собирающий `lib/client.js`.
33
+ Плюс: файлы в репозитории маленькие, публикуется по-прежнему один модуль.
34
+ Минус: в семействе нет ни одного плагина со сборочным шагом — вводит новую
35
+ конвенцию, усложняет publish-пайплайн и локальную отладку.
36
+ - **C. Относительный require в клиенте.** Самый чистый вариант, но требует
37
+ подтверждённой поддержки в бандлере харнесса (эксперимент на тест-контуре).
38
+
39
+ ## Решение и критерии пересмотра
40
+
41
+ - Сейчас — вариант A: риск поломки загрузки карточки выше пользы, а файл после
42
+ батча качества сократился (1240 строк против 1311) и остаётся цельным по смыслу.
43
+ - Пересмотреть к варианту C, если появится подтверждённый пример относительного
44
+ require в клиентском модуле любого плагина семейства (эксперимент: маленький
45
+ пакет-проба `require('./client-part.js')` на тест-контуре + проверка загрузки
46
+ карточки без ошибок в консоли).
47
+ - Пересмотреть к варианту B, если файл превысит ~1800 строк или в семействе
48
+ появится первый плагин со сборочным шагом.
49
+
50
+ ## Чек-лист разбиения (когда механика подтверждена)
51
+
52
+ 1. Вынести словари и `makeT` в `lib/client-dict.js`, стили — в `lib/client-styles.js`,
53
+ `SearchableModelPicker` — в `lib/client-picker.js`.
54
+ 2. `lib/client.js` остаётся единственной точкой входа `__ModuleLoader__.load`.
55
+ 3. Проверки: клиентский модуль отдаётся 200 по per-plugin URL; карточка
56
+ рендерится (settings.plugin.item) без ошибок в консоли; локали регистрируются;
57
+ HMR/обновление соседнего плагина не ломает стили (атрибут data-dsh-plugin).
package/lib/client.js CHANGED
@@ -1,12 +1,15 @@
1
1
  // Plugin Settings Card (settings.plugin.item).
2
2
  //
3
3
  // Aligned with dsh-clinebot design language:
4
- // - Header with real-time status chips (Host online, active preset, model counts)
4
+ // - Header with real status chips (host online/offline/disabled via /dsh-moa/status)
5
5
  // - Clean section cards (.moa-section-card) using native DSH design tokens (--dsw-alias-*)
6
6
  // - Segmented preset selectors and count pills
7
7
  // - Refined temperature sliders and searchable model picker
8
- // - Statistics & telemetry summary grid
8
+ // - Telemetry summary grid fed by /dsh-moa/history
9
9
  // - Robust error boundary and settingsScope reactivity
10
+ //
11
+ // English is the canonical source language; the ru translation is provided
12
+ // by the DSH translation plugin at runtime (no bundled ru duplicate).
10
13
 
11
14
  window.__ModuleLoader__.load({
12
15
  id: '@goodandready/dsh-moa',
@@ -48,6 +51,16 @@ window.__ModuleLoader__.load({
48
51
  'aggregator.criteria_placeholder': 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
49
52
  'aggregator.questions_label': 'Ask clarifying questions on broad tasks (Questionnaire)',
50
53
  'aggregator.questions_hint': 'When enabled: for short/vague prompts, the judge synthesizes a 2-4 question checklist before code generation.',
54
+ 'aggregator.curator_label': 'Curator Synthesis & Master Assembler Advice',
55
+ 'aggregator.curator_hint': 'When enabled: the curator highlights the finest components of each candidate, applies the antipatterns rubric, and advises which agent model should assemble the solution.',
56
+ 'aggregator.stream_label': 'Live Stream Curator/Judge Thinking',
57
+ 'aggregator.stream_hint': 'Streams aggregator tokens directly to chat in real time for zero-latency initial response.',
58
+ 'aggregator.fallbacks_title': 'Fallback Judges Chain',
59
+ 'aggregator.fallbacks_desc': 'Sequential backup models invoked automatically if the primary judge experiences rate limits or outages.',
60
+ 'aggregator.add_fallback_btn': '+ Add Fallback Judge',
61
+ 'proposers.quorum_label': 'Quorum Fast-Forward (Straggler Mitigation)',
62
+ 'proposers.quorum_hint': 'When 67%+ candidates finish, start a grace timer (default: 10s) and proceed without waiting for hanging providers.',
63
+ 'proposers.grace_label': 'Grace Period (sec):',
51
64
  'proposers.title': '👥 Candidate Models (Proposers)',
52
65
  'proposers.desc': 'Generate independent solutions in parallel to maximize diversity and solution space coverage.',
53
66
  'proposers.parallel_count': 'Parallel Proposers',
@@ -71,65 +84,12 @@ window.__ModuleLoader__.load({
71
84
  'actions.save': 'Save Configuration',
72
85
  'actions.saving': 'Saving…',
73
86
  'actions.saved': 'Configuration saved successfully!',
74
- 'actions.retry': 'Retry Connection',
87
+ 'actions.retry': 'Reload Configuration',
88
+ 'badge.disabled': 'MoA Disabled',
89
+ 'config.enabled_label': 'Enable MoA (slash command and turn routing)',
75
90
  'slash.desc': 'Run turn with Mixture of Agents ensemble synthesis (/moa [preset] <prompt>)',
76
91
  }
77
92
 
78
- const ru = {
79
- title: 'Mixture of Agents (MoA)',
80
- description: 'Ансамбль моделей: параллельная генерация советниками и синтез ведущим судьей через /moa <prompt>.',
81
- 'header.title': 'Оркестрация Mixture of Agents (MoA)',
82
- 'header.sub': 'Параллельная генерация вариантов советниками с оценкой и продвижением лучшего кода судьей. Модель сессии восстанавливается автоматически.',
83
- 'badge.online': 'MoA Активен',
84
- 'badge.offline': 'Хост недоступен',
85
- 'badge.preset': 'Пресет: {name}',
86
- 'badge.models': '{count} моделей доступно',
87
- 'badge.candidates': '{count} советников',
88
- 'presets.title': '📋 Управление пресетами',
89
- 'presets.desc': 'Выберите активный пресет или создайте специализированную конфигурацию (например: для код-ревью, математики или быстрого скрипта).',
90
- 'presets.active_badge': 'По умолчанию',
91
- 'presets.set_default': 'Сделать по умолчанию',
92
- 'presets.new_btn': '+ Создать пресет',
93
- 'presets.del_btn': 'Удалить',
94
- 'presets.del_confirm': 'Вы уверены, что хотите удалить пресет "{name}"?',
95
- 'presets.create': 'Создать',
96
- 'presets.cancel': 'Отмена',
97
- 'presets.name_placeholder': 'Название нового пресета (например: fast-audit)',
98
- 'aggregator.title': '⚖️ Ведущая модель / Судья (Синтез и аудит)',
99
- 'aggregator.desc': 'Сопоставляет варианты кандидатов, устраняет ошибки, отбирает лучший код и синтезирует итоговое решение.',
100
- 'aggregator.temp_label': 'Температура судьи',
101
- 'aggregator.temp_hint': 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
102
- 'aggregator.criteria_label': 'Критерии оценки судьи (опционально):',
103
- 'aggregator.criteria_placeholder': 'например: Приоритет — производительность, чистый код, минимум зависимостей, тесты...',
104
- 'aggregator.questions_label': 'Спрашивать уточняющие вопросы при неясных задачах (Опросник)',
105
- 'aggregator.questions_hint': 'Если включено: при коротких задачах судья сформирует опросник. Если выключено: MoA сразу переходит к генерации решения.',
106
- 'proposers.title': '👥 Модели-кандидаты (Советники)',
107
- 'proposers.desc': 'Параллельно формируют независимые варианты решения одной задачи.',
108
- 'proposers.parallel_count': 'Параллельных моделей',
109
- 'proposers.candidate': 'Кандидат',
110
- 'proposers.add_btn': '+ Добавить модель в параллель',
111
- 'proposers.temp_label': 'Температура кандидатов',
112
- 'proposers.temp_hint': 'Рекомендуется 0.6 – 0.8 для получения независимых и разнообразных вариантов.',
113
- 'stats.title': '📈 Аналитика и телеметрия MoA',
114
- 'stats.desc': 'Мониторинг запусков, статистика побед моделей и оценка расхода токенов.',
115
- 'stats.total_runs': 'Всего запусков MoA',
116
- 'stats.avg_cost': 'Средняя стоимость',
117
- 'stats.active_models': 'Настроено моделей',
118
- 'stats.preset_count': 'Всего пресетов',
119
- 'picker.select': 'Выберите модель…',
120
- 'picker.search_placeholder': 'Поиск модели по названию или провайдеру…',
121
- 'picker.found': 'Найдено',
122
- 'picker.of': 'из',
123
- 'picker.clear': 'Сбросить',
124
- 'picker.none': 'Модели не найдены.',
125
- 'picker.custom': 'Использовать: ',
126
- 'actions.save': 'Сохранить настройки',
127
- 'actions.saving': 'Сохранение…',
128
- 'actions.saved': 'Настройки успешно сохранены!',
129
- 'actions.retry': 'Повторить подключение',
130
- 'slash.desc': 'Запуск ансамбля Mixture of Agents (/moa [preset] <prompt>)',
131
- }
132
-
133
93
  function makeT(dict, fallback) {
134
94
  return function t(key, vars) {
135
95
  let val = (dict && dict[key]) || (fallback && fallback[key]) || key
@@ -163,7 +123,15 @@ window.__ModuleLoader__.load({
163
123
  })
164
124
  )
165
125
  }
166
- const Chevron = FallbackChevron
126
+ let Chevron = FallbackChevron
127
+ try {
128
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
129
+ if (primitives && typeof primitives.IconChevronDownOutline14 === 'function') {
130
+ Chevron = primitives.IconChevronDownOutline14
131
+ }
132
+ } catch (noPrimitives) {
133
+ Chevron = FallbackChevron
134
+ }
167
135
 
168
136
  const STYLES = `
169
137
  /* dsh-moa modern clinebot-aligned styling */
@@ -174,8 +142,8 @@ window.__ModuleLoader__.load({
174
142
 
175
143
  .moa-badge-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:2px}
176
144
  .moa-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
177
- .moa-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
178
- .moa-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
145
+ .moa-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}
146
+ .moa-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent)}
179
147
  .moa-badge-brand{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
180
148
 
181
149
  .moa-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px}
@@ -196,8 +164,8 @@ window.__ModuleLoader__.load({
196
164
  .moa-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-1));border-color:var(--dsw-alias-label-secondary)}
197
165
  .moa-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
198
166
  .moa-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.9}
199
- .moa-btn-danger{color:var(--dsw-alias-state-error-primary, #ef4444);border-color:rgba(239,68,68,0.3)}
200
- .moa-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12) !important;border-color:rgba(239,68,68,0.5)}
167
+ .moa-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 35%, transparent)}
168
+ .moa-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent) !important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}
201
169
  .moa-btn-sub{padding:5px 12px;font-size:12px;border-radius:6px}
202
170
 
203
171
  /* Segmented Buttons */
@@ -223,9 +191,9 @@ window.__ModuleLoader__.load({
223
191
  .moa-picker-btn{width:100%;min-height:38px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;cursor:pointer;font-size:13px;text-align:left;box-sizing:border-box;gap:8px}
224
192
  .moa-picker-btn:hover{border-color:var(--dsw-alias-label-secondary)}
225
193
  .moa-picker-summary{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
226
- .moa-prov-chip{font-size:11px;font-weight:600;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.08));color:var(--dsw-alias-label-secondary);flex-shrink:0}
194
+ .moa-prov-chip{font-size:11px;font-weight:600;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);flex-shrink:0}
227
195
  .moa-model-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
228
- .moa-picker-popover{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 12px 36px rgba(0,0,0,0.45);z-index:1000;max-height:360px;display:flex;flex-direction:column;overflow:hidden}
196
+ .moa-picker-popover{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 12px 36px color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);z-index:1000;max-height:360px;display:flex;flex-direction:column;overflow:hidden}
229
197
  .moa-picker-search-bar{padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
230
198
  .moa-search-input{width:100%;height:32px;padding:0 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);font-size:13px;outline:none;box-sizing:border-box}
231
199
  .moa-search-input:focus{border-color:var(--dsw-alias-label-primary)}
@@ -233,13 +201,13 @@ window.__ModuleLoader__.load({
233
201
  .moa-picker-list{overflow-y:auto;max-height:280px;padding:6px;display:flex;flex-direction:column;gap:2px}
234
202
  .moa-group-title{font-size:11px;font-weight:700;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);padding:8px 8px 4px}
235
203
  .moa-picker-item{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-radius:6px;font-size:13px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border:none;background:transparent;width:100%;gap:8px}
236
- .moa-picker-item:hover, .moa-picker-item-selected{background:var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))}
237
- .moa-picker-check{color:var(--dsw-alias-state-success-primary, #10b981);font-weight:bold;font-size:14px;flex-shrink:0}
204
+ .moa-picker-item:hover, .moa-picker-item-selected{background:var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2))}
205
+ .moa-picker-check{color:var(--dsw-alias-state-success-primary);font-weight:bold;font-size:14px;flex-shrink:0}
238
206
  .moa-custom-opt{padding:8px 10px;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-primary);cursor:pointer;display:flex;align-items:center;gap:6px}
239
207
 
240
208
  /* Alerts & Collapsible Card */
241
- .moa-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;font-weight:500}
242
- .moa-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:500}
209
+ .moa-alert-ok{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:13px;font-weight:500}
210
+ .moa-alert-err{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:500}
243
211
  .moa-card{list-style:none;margin:0;padding:0;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;overflow:hidden;transition:border-color .15s ease}
244
212
  .moa-head{width:100%;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;background:transparent;border:none;color:inherit;cursor:pointer;text-align:left}
245
213
  .moa-head:hover{background:var(--dsw-alias-bg-layer-2)}
@@ -476,6 +444,10 @@ window.__ModuleLoader__.load({
476
444
  defaultPreset,
477
445
  setDefaultPreset,
478
446
  availableModels,
447
+ enabled,
448
+ setEnabled,
449
+ hostStatus,
450
+ stats,
479
451
  handleSave,
480
452
  saveStatus,
481
453
  reload,
@@ -624,6 +596,11 @@ window.__ModuleLoader__.load({
624
596
  const currentRefs = currentPreset.reference_models || []
625
597
  const judgeCriteria = currentPreset.judge_criteria || ''
626
598
  const isDefault = defaultPreset === currentPreset.name
599
+ const hostStatusBadge = hostStatus === 'online'
600
+ ? React.createElement('span', { className: 'moa-badge moa-badge-ok' }, '🟢 ' + t('badge.online'))
601
+ : hostStatus === 'disabled'
602
+ ? React.createElement('span', { className: 'moa-badge moa-badge-warn' }, '⏸ ' + t('badge.disabled'))
603
+ : React.createElement('span', { className: 'moa-badge moa-badge-warn' }, '🔴 ' + t('badge.offline'))
627
604
 
628
605
  return React.createElement(
629
606
  'div',
@@ -637,11 +614,11 @@ window.__ModuleLoader__.load({
637
614
  'div',
638
615
  { className: 'moa-page-title' },
639
616
  React.createElement('span', null, '🧠 ' + t('header.title')),
640
- React.createElement(
641
- 'div',
642
- { className: 'moa-badge-row' },
643
- React.createElement('span', { className: 'moa-badge moa-badge-ok' }, '🟢 ' + t('badge.online')),
644
- React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.preset', { name: currentPreset.name })),
617
+ React.createElement(
618
+ 'div',
619
+ { className: 'moa-badge-row' },
620
+ hostStatusBadge,
621
+ React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.preset', { name: currentPreset.name })),
645
622
  React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.models', { count: availableModels.length })),
646
623
  React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.candidates', { count: currentRefs.length }))
647
624
  )
@@ -821,6 +798,109 @@ window.__ModuleLoader__.load({
821
798
  'div',
822
799
  { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
823
800
  t('aggregator.questions_hint')
801
+ ),
802
+ React.createElement(
803
+ 'label',
804
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
805
+ React.createElement('input', {
806
+ type: 'checkbox',
807
+ checked: Boolean(currentPreset.curator_synthesis),
808
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, curator_synthesis: e.target.checked })),
809
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
810
+ }),
811
+ t('aggregator.curator_label')
812
+ ),
813
+ React.createElement(
814
+ 'div',
815
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
816
+ t('aggregator.curator_hint')
817
+ ),
818
+ React.createElement(
819
+ 'label',
820
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
821
+ React.createElement('input', {
822
+ type: 'checkbox',
823
+ checked: currentPreset.stream_aggregator !== false,
824
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, stream_aggregator: e.target.checked })),
825
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
826
+ }),
827
+ t('aggregator.stream_label')
828
+ ),
829
+ React.createElement(
830
+ 'div',
831
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
832
+ t('aggregator.stream_hint')
833
+ ),
834
+ /* Fallback Judges Chain */
835
+ React.createElement(
836
+ 'div',
837
+ { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
838
+ React.createElement(
839
+ 'div',
840
+ { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
841
+ t('aggregator.fallbacks_title')
842
+ ),
843
+ React.createElement(
844
+ 'div',
845
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } },
846
+ t('aggregator.fallbacks_desc')
847
+ ),
848
+ (Array.isArray(currentPreset.aggregator_fallbacks) ? currentPreset.aggregator_fallbacks : []).map((fb, fbIdx) =>
849
+ React.createElement(
850
+ 'div',
851
+ { key: fbIdx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
852
+ React.createElement('span', { className: 'moa-cand-badge' }, `Fallback #${fbIdx + 1}`),
853
+ React.createElement(
854
+ 'div',
855
+ { style: { flex: 1 } },
856
+ React.createElement(SearchableModelPicker, {
857
+ value: fb,
858
+ onChange: (val) => {
859
+ updateCurrentPreset((p) => {
860
+ const list = [...(p.aggregator_fallbacks || [])]
861
+ list[fbIdx] = val
862
+ return { ...p, aggregator_fallbacks: list }
863
+ })
864
+ },
865
+ allModels,
866
+ t,
867
+ })
868
+ ),
869
+ React.createElement(
870
+ 'button',
871
+ {
872
+ type: 'button',
873
+ className: 'moa-btn moa-btn-danger moa-btn-sub',
874
+ onClick: () => {
875
+ updateCurrentPreset((p) => ({
876
+ ...p,
877
+ aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== fbIdx),
878
+ }))
879
+ },
880
+ },
881
+ '✕'
882
+ )
883
+ )
884
+ ),
885
+ React.createElement(
886
+ 'div',
887
+ null,
888
+ React.createElement(
889
+ 'button',
890
+ {
891
+ type: 'button',
892
+ className: 'moa-btn moa-btn-sub',
893
+ style: { marginTop: 4 },
894
+ onClick: () => {
895
+ updateCurrentPreset((p) => ({
896
+ ...p,
897
+ aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: 'opencode-go', model: 'deepseek-v4-flash' }],
898
+ }))
899
+ },
900
+ },
901
+ t('aggregator.add_fallback_btn')
902
+ )
903
+ )
824
904
  )
825
905
  )
826
906
  )
@@ -853,6 +933,43 @@ window.__ModuleLoader__.load({
853
933
  )
854
934
  ),
855
935
  React.createElement('div', { className: 'moa-section-desc' }, t('proposers.desc')),
936
+ React.createElement(
937
+ 'div',
938
+ { 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)' } },
939
+ React.createElement(
940
+ 'label',
941
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
942
+ React.createElement('input', {
943
+ type: 'checkbox',
944
+ checked: Boolean(currentPreset.quorum_enabled),
945
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, quorum_enabled: e.target.checked })),
946
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
947
+ }),
948
+ t('proposers.quorum_label')
949
+ ),
950
+ React.createElement(
951
+ 'div',
952
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
953
+ t('proposers.quorum_hint')
954
+ ),
955
+ currentPreset.quorum_enabled && React.createElement(
956
+ 'div',
957
+ { style: { marginLeft: 22, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 } },
958
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('proposers.grace_label')),
959
+ React.createElement('input', {
960
+ type: 'number',
961
+ className: 'moa-input',
962
+ style: { width: 70, height: 28, padding: '0 6px', fontSize: 12 },
963
+ min: 2,
964
+ max: 60,
965
+ value: currentPreset.grace_period_sec ?? 10,
966
+ onChange: (e) => {
967
+ const val = parseInt(e.target.value, 10)
968
+ updateCurrentPreset((p) => ({ ...p, grace_period_sec: isNaN(val) ? 10 : val }))
969
+ },
970
+ })
971
+ )
972
+ ),
856
973
  React.createElement(
857
974
  'div',
858
975
  { className: 'moa-cand-list' },
@@ -940,18 +1057,37 @@ window.__ModuleLoader__.load({
940
1057
  React.createElement(
941
1058
  'div',
942
1059
  { className: 'moa-stat-box' },
943
- React.createElement('div', { className: 'moa-stat-val' }, String(currentRefs.length)),
944
- React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.active_models'))
1060
+ React.createElement('div', { className: 'moa-stat-val' }, (stats.totalRuns === null || stats.totalRuns === undefined) ? '—' : String(stats.totalRuns)),
1061
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.total_runs'))
945
1062
  ),
946
1063
  React.createElement(
947
1064
  'div',
948
1065
  { className: 'moa-stat-box' },
949
- React.createElement('div', { className: 'moa-stat-val' }, String(availableModels.length)),
950
- React.createElement('div', { className: 'moa-stat-lbl' }, t('badge.models', { count: availableModels.length }))
1066
+ React.createElement('div', { className: 'moa-stat-val' }, (stats.avgCostUsd === null || stats.avgCostUsd === undefined) ? '—' : `~\$${stats.avgCostUsd.toFixed(4)}`),
1067
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.avg_cost'))
1068
+ ),
1069
+ React.createElement(
1070
+ 'div',
1071
+ { className: 'moa-stat-box' },
1072
+ React.createElement('div', { className: 'moa-stat-val' }, String(currentRefs.length)),
1073
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.active_models'))
951
1074
  )
952
1075
  )
953
1076
  ),
954
1077
 
1078
+ /* ── ENABLED TOGGLE ── */
1079
+ React.createElement(
1080
+ 'label',
1081
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, alignSelf: 'flex-start' } },
1082
+ React.createElement('input', {
1083
+ type: 'checkbox',
1084
+ checked: enabled !== false,
1085
+ onChange: (e) => setEnabled(e.target.checked),
1086
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1087
+ }),
1088
+ t('config.enabled_label')
1089
+ ),
1090
+
955
1091
  /* ── FOOTER ACTIONS & SAVE ── */
956
1092
  React.createElement(
957
1093
  'div',
@@ -989,7 +1125,9 @@ window.__ModuleLoader__.load({
989
1125
 
990
1126
  function useMoASettings(props) {
991
1127
  const effectiveCtx = (props && props.ctx) || rootCtx
992
- const t = makeT(ru, en)
1128
+ // English is canonical; prefer the core-provided translator for the
1129
+ // active UI language (the slot declares locale: NS), fall back to en.
1130
+ const t = typeof props?.t === 'function' ? props.t : makeT(en, null)
993
1131
 
994
1132
  const scope = React.useMemo(() => {
995
1133
  if (!effectiveCtx) return undefined
@@ -1010,6 +1148,9 @@ window.__ModuleLoader__.load({
1010
1148
  const [defaultPreset, setDefaultPreset] = React.useState('default')
1011
1149
  const [availableModels, setAvailableModels] = React.useState([])
1012
1150
  const [saveStatus, setSaveStatus] = React.useState('')
1151
+ const [enabled, setEnabled] = React.useState(true)
1152
+ const [hostStatus, setHostStatus] = React.useState('connecting')
1153
+ const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
1013
1154
 
1014
1155
  React.useEffect(() => {
1015
1156
  if (!scope || !snapshot) return
@@ -1018,6 +1159,7 @@ window.__ModuleLoader__.load({
1018
1159
  const val = snapshot.value || {}
1019
1160
  if (Array.isArray(val.presets)) setPresets(val.presets)
1020
1161
  if (val.default_preset) setDefaultPreset(val.default_preset)
1162
+ if (typeof val.enabled === 'boolean') setEnabled(val.enabled)
1021
1163
  setStatus('ready')
1022
1164
  } else if (snapStatus === 'unavailable') {
1023
1165
  setStatus('unavailable')
@@ -1027,6 +1169,35 @@ window.__ModuleLoader__.load({
1027
1169
  const reload = React.useCallback(() => {
1028
1170
  if (!scope) setStatus('loading')
1029
1171
 
1172
+ fetch('/dsh-moa/status', { cache: 'no-store' })
1173
+ .then((r) => r.json())
1174
+ .then((data) => {
1175
+ if (data && data.ok) {
1176
+ setHostStatus(data.enabled === false ? 'disabled' : 'online')
1177
+ } else {
1178
+ setHostStatus('offline')
1179
+ }
1180
+ })
1181
+ .catch(() => {
1182
+ setHostStatus('offline')
1183
+ })
1184
+
1185
+ fetch('/dsh-moa/history?limit=100', { cache: 'no-store' })
1186
+ .then((r) => r.json())
1187
+ .then((data) => {
1188
+ if (data && data.ok && Array.isArray(data.runs)) {
1189
+ const runs = data.runs
1190
+ const withCost = runs.filter((run) => typeof run.totalCostUsd === 'number')
1191
+ const avgCostUsd = withCost.length > 0
1192
+ ? withCost.reduce((acc, run) => acc + run.totalCostUsd, 0) / withCost.length
1193
+ : null
1194
+ setStats({ totalRuns: data.total || runs.length, avgCostUsd })
1195
+ }
1196
+ })
1197
+ .catch((err) => {
1198
+ console.warn('[dsh-moa] History fetch failed:', err)
1199
+ })
1200
+
1030
1201
  fetch('/dsh-moa/presets', { cache: 'no-store' })
1031
1202
  .then((r) => {
1032
1203
  if (!r.ok) throw new Error(`HTTP ${r.status}`)
@@ -1067,6 +1238,7 @@ window.__ModuleLoader__.load({
1067
1238
 
1068
1239
  if (scope && typeof scope.set === 'function') {
1069
1240
  Promise.all([
1241
+ scope.set('enabled', enabled),
1070
1242
  scope.set('presets', presets),
1071
1243
  scope.set('default_preset', defaultPreset),
1072
1244
  ])
@@ -1088,7 +1260,7 @@ window.__ModuleLoader__.load({
1088
1260
  fetch('/dsh-moa/presets', {
1089
1261
  method: 'POST',
1090
1262
  headers: { 'Content-Type': 'application/json' },
1091
- body: JSON.stringify({ presets, default_preset: defaultPreset }),
1263
+ body: JSON.stringify({ enabled, presets, default_preset: defaultPreset }),
1092
1264
  })
1093
1265
  .then((r) => r.json())
1094
1266
  .then((res) => {
@@ -1114,6 +1286,10 @@ window.__ModuleLoader__.load({
1114
1286
  defaultPreset,
1115
1287
  setDefaultPreset,
1116
1288
  availableModels,
1289
+ enabled,
1290
+ setEnabled,
1291
+ hostStatus,
1292
+ stats,
1117
1293
  handleSave,
1118
1294
  saveStatus,
1119
1295
  reload,
@@ -1227,7 +1403,7 @@ window.__ModuleLoader__.load({
1227
1403
 
1228
1404
  if (typeof ctx.effect === 'function') {
1229
1405
  ctx.effect(() => {
1230
- const undo = [addLocale('en', en), addLocale('ru', ru)]
1406
+ const undo = [addLocale('en', en)]
1231
1407
  return () => {
1232
1408
  for (const off of undo) {
1233
1409
  if (typeof off === 'function') {
@@ -1238,7 +1414,6 @@ window.__ModuleLoader__.load({
1238
1414
  }, 'dsh-moa: dictionaries')
1239
1415
  } else {
1240
1416
  addLocale('en', en)
1241
- addLocale('ru', ru)
1242
1417
  }
1243
1418
 
1244
1419
  if (ctx.slots) {
@@ -1275,7 +1450,7 @@ window.__ModuleLoader__.load({
1275
1450
  trigger: '/',
1276
1451
  name: 'moa',
1277
1452
  order: 35,
1278
- description: ru['slash.desc'] || en['slash.desc'],
1453
+ description: en['slash.desc'],
1279
1454
  candidates: (_session, req) => {
1280
1455
  if (req.position !== 'leading') return Promise.resolve([])
1281
1456
  const q = req.query.trim().toLowerCase()
@@ -1283,7 +1458,7 @@ window.__ModuleLoader__.load({
1283
1458
  return Promise.resolve([
1284
1459
  {
1285
1460
  name: 'moa',
1286
- description: ru['slash.desc'] || en['slash.desc'],
1461
+ description: en['slash.desc'],
1287
1462
  },
1288
1463
  ])
1289
1464
  },
@@ -1308,4 +1483,4 @@ window.__ModuleLoader__.load({
1308
1483
  return module.exports
1309
1484
  },
1310
1485
  })
1311
-
1486
+
@@ -107,7 +107,9 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
107
107
  const list = await fs.readdir(currentDir, { withFileTypes: true })
108
108
  for (const item of list) {
109
109
  if (totalChars >= maxCharBudget) break
110
- if (item.name.startsWith('.') && item.name !== '.env') continue
110
+ // Skip all dotfiles, including .env and .env.* variants: their
111
+ // contents must never be shipped to third-party model providers.
112
+ if (item.name.startsWith('.')) continue
111
113
 
112
114
  const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
113
115
  const full = path.join(currentDir, item.name)
package/lib/history.js CHANGED
@@ -35,6 +35,7 @@ export function recordMoaRun(record, filePath = DEFAULT_HISTORY_FILE) {
35
35
  prompt: record.prompt || '',
36
36
  preset: record.preset || 'default',
37
37
  isRefinement: Boolean(record.isRefinement),
38
+ isFastMode: Boolean(record.isFastMode),
38
39
  candidates: Array.isArray(record.candidates)
39
40
  ? record.candidates.map((c) => ({
40
41
  provider: c.provider || '',
@@ -144,7 +145,9 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
144
145
  st.totalTokens += inTok + outTok
145
146
  st.totalCostUsd += Number(cand.costUsd || 0)
146
147
 
147
- if (winner && (winner === key || winner === cand.model || winner.includes(cand.model))) {
148
+ // Exact match only: loose substring matching combined with empty
149
+ // candidate fields credited wins to every model in the run.
150
+ if (winner && cand.model && (winner === key || winner === cand.model)) {
148
151
  st.wins += 1
149
152
  }
150
153
  }