@goodandready/dsh-moa 0.2.3 → 0.2.5

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 CHANGED
@@ -37,6 +37,10 @@ Single-model AI generation often suffers from blind spots, single-perspective bi
37
37
  4. **Instant Live Canvas Previewing**: When web applications or UI components are generated, `dsh-moa` integrates seamlessly with `@goodandready/dsh-live-canvas`, automatically spawning sandboxes for 1-click browser previewing.
38
38
  5. **Token-Saving Chat Summarization**: Replaces massive code dumps in chat bubbles with compact file listings and clean architectural summaries.
39
39
  6. **One-Shot Session Model Restoration**: Executes cleanly as a one-shot turn modifier, automatically reverting back to the user's primary session model immediately after completion.
40
+ 7. **Dynamic Model Pricing Catalog & Token Estimation**: Real-time rate resolution for 300+ models fetched automatically in the background from OpenRouter's public catalog (cached locally in `~/.dsh/storages/dsh-moa-catalog.json` for 24h), plus support for direct vendor rates and custom `prices` overrides in `settings.yaml`.
41
+ 8. **Refinement Mode (Incremental Edits)**: Automatically detects existing codebase context to generate precise delta modifications instead of destructive full-file rewrites.
42
+ 9. **Fast Mode & Custom Judge Criteria**: Ultra-fast single-model preset for quick tasks and customizable evaluation guidelines for the judge.
43
+ 10. **Run History & Win-Rate Leaderboard**: Persistent logging with built-in REST endpoints (`/dsh-moa/history` and `/dsh-moa/leaderboard`).
40
44
 
41
45
  ---
42
46
 
@@ -152,6 +156,13 @@ dsh-moa:
152
156
  aggregator:
153
157
  provider: "your-reasoning-provider"
154
158
  model: "your-judge-model"
159
+ prices:
160
+ "my-provider/my-model":
161
+ input: 0.20
162
+ output: 0.80
163
+ "ollama/*":
164
+ input: 0
165
+ output: 0
155
166
  code-review:
156
167
  references:
157
168
  - provider: "your-fast-provider"
@@ -175,6 +186,16 @@ dsh-moa:
175
186
 
176
187
  ---
177
188
 
189
+ ## 📊 REST API & Endpoints
190
+
191
+ | Endpoint | Method | Description |
192
+ |:---|:---|:---|
193
+ | `/dsh-moa/presets` | `GET` | Returns list of configured MoA presets |
194
+ | `/dsh-moa/history?limit=20&offset=0` | `GET` | Returns recent MoA runs with candidates, winner, cost, and tokens |
195
+ | `/dsh-moa/leaderboard` | `GET` | Computes model win-rate leaderboard and average execution costs |
196
+
197
+ ---
198
+
178
199
  ## 🧪 Testing
179
200
 
180
201
  Run the automated test suite:
package/docs/README.ru.md CHANGED
@@ -37,6 +37,10 @@
37
37
  4. **Мгновенный предпросмотр через Live Canvas**: При создании веб-приложений и UI-компонентов `dsh-moa` бесшовно интегрируется с `@goodandready/dsh-live-canvas`, автоматически инициализируя контейнер для предпросмотра в браузере в 1 клик.
38
38
  5. **Экономия токенов в чате**: Вместо вывода огромных листингов кода в чат формируется компактный отчет с перечнем созданных файлов и архитектурным резюме.
39
39
  6. **Одноразовый модификатор сессии**: Команда выполняется в рамках одного такта и автоматически возвращает исходную модель сессии пользователя сразу после завершения.
40
+ 7. **Динамический каталог тарифов и подсчет токенов**: Актуальные цены на 300+ моделей автоматически подтягиваются из публичного каталога OpenRouter (без ключей и авторизации), кешируются в `~/.dsh/storages/dsh-moa-catalog.json` на 24 часа, а также поддерживают прямые вендорские тарифы и пользовательские оверрайды `prices` в `settings.yaml`.
41
+ 8. **Режим доработки (Refinement Mode)**: Автоматически считывает контекст существующего проекта и генерирует точечные дельта-правки без перетирания всей кодовой базы.
42
+ 9. **Быстрый режим (Fast Mode) и критерии судьи**: Режим для одиночных быстрых задач без судьи и гибкая настройка фокуса оценки (безопасность, производительность, минимализм).
43
+ 10. **История запусков и лидерборд моделей**: Персистентное логирование запусков и REST-эндпоинты (`/dsh-moa/history` и `/dsh-moa/leaderboard`).
40
44
 
41
45
  ---
42
46
 
@@ -152,6 +156,13 @@ dsh-moa:
152
156
  aggregator:
153
157
  provider: "your-reasoning-provider"
154
158
  model: "your-judge-model"
159
+ prices:
160
+ "my-provider/my-model":
161
+ input: 0.20
162
+ output: 0.80
163
+ "ollama/*":
164
+ input: 0
165
+ output: 0
155
166
  code-review:
156
167
  references:
157
168
  - provider: "your-fast-provider"
@@ -175,6 +186,16 @@ dsh-moa:
175
186
 
176
187
  ---
177
188
 
189
+ ## 📊 REST API эндпоинты
190
+
191
+ | Эндпоинт | Метод | Описание |
192
+ |:---|:---|:---|
193
+ | `/dsh-moa/presets` | `GET` | Возвращает список настроенных пресетов MoA |
194
+ | `/dsh-moa/history?limit=20&offset=0` | `GET` | История запусков с кандидатами, победителем, токенами и ценой |
195
+ | `/dsh-moa/leaderboard` | `GET` | Лидерборд побед моделей и средняя стоимость генерации |
196
+
197
+ ---
198
+
178
199
  ## 🧪 Тестирование
179
200
 
180
201
  Запуск автоматического набора тестов:
package/lib/client.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  let React = require('react')
7
7
 
8
8
  const NS = 'dsh-moa'
9
+ let localeRegistered = false
9
10
 
10
11
  let ChevronIcon = null
11
12
  try {
@@ -378,6 +379,10 @@ window.__ModuleLoader__.load({
378
379
  updateCurrentPreset((p) => ({ ...p, reference_temperature: val }))
379
380
  }
380
381
 
382
+ const setJudgeCriteria = (criteria) => {
383
+ updateCurrentPreset((p) => ({ ...p, judge_criteria: criteria }))
384
+ }
385
+
381
386
  const setReferenceModel = (idx, prov, mod) => {
382
387
  updateCurrentPreset((p) => {
383
388
  const refs = [...(p.reference_models || [])]
@@ -468,6 +473,7 @@ window.__ModuleLoader__.load({
468
473
  const currentRefs = currentPreset.reference_models || []
469
474
  const aggTemp = currentPreset.aggregator_temperature ?? 0.4
470
475
  const refTemp = currentPreset.reference_temperature ?? 0.6
476
+ const judgeCriteria = currentPreset.judge_criteria || ''
471
477
 
472
478
  return React.createElement(
473
479
  React.Fragment,
@@ -621,6 +627,30 @@ window.__ModuleLoader__.load({
621
627
  })
622
628
  ),
623
629
  React.createElement('div', { className: 'moa-hint' }, t('aggTempHint') || 'Рекомендуется 0.2 – 0.4 для точной проверки и синтеза без галлюцинаций.')
630
+ ),
631
+ React.createElement(
632
+ 'div',
633
+ { className: 'moa-field', style: { marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
634
+ React.createElement('label', { className: 'moa-label', style: { fontSize: 12 } }, t('judgeCriteriaLabel') || 'Критерии оценки судьи (опционально):'),
635
+ React.createElement('textarea', {
636
+ className: 'moa-textarea',
637
+ rows: 2,
638
+ style: {
639
+ width: '100%',
640
+ boxSizing: 'border-box',
641
+ padding: '8px 10px',
642
+ borderRadius: 8,
643
+ border: '1px solid var(--dsw-alias-border-l2)',
644
+ background: 'var(--dsw-alias-bg-layer-3)',
645
+ color: 'var(--dsw-alias-label-primary)',
646
+ fontSize: 13,
647
+ resize: 'vertical',
648
+ outline: 'none',
649
+ },
650
+ placeholder: t('judgeCriteriaPlaceholder') || 'например: Приоритет — производительность, чистый код, минимум зависимостей...',
651
+ value: judgeCriteria,
652
+ onChange: (e) => setJudgeCriteria(e.target.value),
653
+ })
624
654
  )
625
655
  ),
626
656
  /* Proposers / Advisors Box with Temperature Slider */
@@ -744,8 +774,120 @@ window.__ModuleLoader__.load({
744
774
  )
745
775
  }
746
776
 
777
+ let rootCtx = null
778
+
779
+ const en = {
780
+ title: 'Mixture of Agents (MoA)',
781
+ description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
782
+ slashMoa: 'Run task through Mixture of Agents (/moa <prompt>)',
783
+ defaultPreset: 'Default Preset (used on /moa without arguments)',
784
+ isDefault: 'default',
785
+ presetSelectLabel: 'Active Preset to Configure:',
786
+ createNewPreset: 'Create new preset...',
787
+ makeDefault: 'Make Default',
788
+ makeDefaultHint: 'Use this preset for /moa when no preset argument is given',
789
+ deletePreset: 'Delete preset',
790
+ delete: 'Delete',
791
+ confirmDeletePreset: 'Are you sure you want to delete preset',
792
+ newPresetPlaceholder: 'New preset name (e.g. fast-audit)',
793
+ create: 'Create',
794
+ cancel: 'Cancel',
795
+ aggregatorTitle: 'Judge / Aggregator Model (who reviews and synthesizes)',
796
+ aggregatorDesc: 'Evaluates candidate proposals, corrects hallucinations, and outputs the final unified solution.',
797
+ temperature: 'Temperature',
798
+ aggTempHint: 'Recommended 0.2 - 0.4 for rigorous verification and factual synthesis.',
799
+ proposersTitle: 'Candidate Models (Advisors)',
800
+ proposersDesc: 'Generates independent candidate answers in parallel to provide diverse perspectives.',
801
+ refTempLabel: 'Candidates Temperature (solution diversity):',
802
+ refTempHint: 'Recommended 0.6 - 0.8 for diverse creative hypotheses.',
803
+ parallelCount: 'Parallel workers',
804
+ candidate: 'Candidate',
805
+ addCandidate: 'Add parallel model slot',
806
+ remove: 'Remove',
807
+ save: 'Save Changes',
808
+ saved: 'Saved successfully',
809
+ loading: 'Saving…',
810
+ retry: 'Retry connection',
811
+ unavailable: 'MoA service temporarily unavailable (reconnecting...)',
812
+ searchPlaceholder: 'Search by model name or provider...',
813
+ modelsFound: 'Found',
814
+ of: 'of',
815
+ clear: 'Clear',
816
+ noModelsMatch: 'No models found matching query.',
817
+ useCustom: 'Use custom',
818
+ selectModel: 'Select model...',
819
+ judgeCriteriaLabel: 'Custom Judge Evaluation Criteria (optional):',
820
+ judgeCriteriaPlaceholder: 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
821
+ }
822
+
823
+ const ru = {
824
+ title: 'Mixture of Agents (MoA)',
825
+ description: 'Запуск задач через ансамбль параллельных моделей-советников и модель-судью',
826
+ slashMoa: 'Запустить задачу через архитектуру Mixture of Agents (/moa <prompt>)',
827
+ defaultPreset: 'Основной пресет (при вызове /moa без параметров)',
828
+ isDefault: 'основной',
829
+ presetSelectLabel: 'Текущий пресет для настройки:',
830
+ createNewPreset: 'Создать новый пресет...',
831
+ makeDefault: 'Сделать основным',
832
+ makeDefaultHint: 'Использовать этот пресет для /moa при вызове без параметров',
833
+ deletePreset: 'Удалить пресет',
834
+ delete: 'Удалить',
835
+ confirmDeletePreset: 'Вы уверены, что хотите удалить пресет',
836
+ newPresetPlaceholder: 'Название нового пресета (например: fast-audit)',
837
+ create: 'Создать',
838
+ cancel: 'Отмена',
839
+ aggregatorTitle: 'Ведущая модель / Судья (кто проверяет и объединяет)',
840
+ aggregatorDesc: 'Сопоставляет варианты кандидатов, устраняет ошибки и синтезирует итоговое решение.',
841
+ temperature: 'Температура',
842
+ aggTempHint: 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
843
+ proposersTitle: 'Модели-кандидаты (советники)',
844
+ proposersDesc: 'Параллельно формируют независимые варианты решения одной задачи.',
845
+ refTempLabel: 'Температура кандидатов (разнообразие решений):',
846
+ refTempHint: 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.',
847
+ parallelCount: 'Параллельных моделей',
848
+ candidate: 'Кандидат',
849
+ addCandidate: 'Добавить модель в параллель',
850
+ remove: 'Удалить',
851
+ save: 'Сохранить',
852
+ saved: 'Успешно сохранено',
853
+ loading: 'Сохранение…',
854
+ retry: 'Повторить подключение',
855
+ unavailable: 'Служба MoA временно недоступна (переподключение...)',
856
+ searchPlaceholder: 'Поиск модели по названию или провайдеру...',
857
+ modelsFound: 'Найдено',
858
+ of: 'из',
859
+ clear: 'Сбросить',
860
+ noModelsMatch: 'Модели не найдены.',
861
+ useCustom: 'Использовать кастомное',
862
+ selectModel: 'Выберите модель...',
863
+ judgeCriteriaLabel: 'Критерии оценки судьи (опционально):',
864
+ judgeCriteriaPlaceholder: 'например: Приоритет — производительность, минимум зависимостей, обработка граничных случаев...',
865
+ }
866
+
867
+ function makeT(ctx, props) {
868
+ return (key) => {
869
+ if (props && typeof props.t === 'function') {
870
+ try {
871
+ const res = props.t(key)
872
+ if (res && res !== key) return res
873
+ } catch (_) {}
874
+ }
875
+ const effectiveCtx = (props && props.ctx) || ctx || rootCtx
876
+ if (effectiveCtx && effectiveCtx.locale && typeof effectiveCtx.locale.bind === 'function') {
877
+ try {
878
+ const bound = effectiveCtx.locale.bind(NS)
879
+ const res = bound(key)
880
+ if (res && res !== key) return res
881
+ } catch (_) {}
882
+ }
883
+ const curLocale = (effectiveCtx && effectiveCtx.locale && effectiveCtx.locale.getSnapshot && effectiveCtx.locale.getSnapshot().active) || 'ru'
884
+ const dict = curLocale === 'en' ? en : ru
885
+ return dict[key] || ru[key] || en[key] || key
886
+ }
887
+ }
888
+
747
889
  function useMoASettings(props) {
748
- const t = props.t || ((k) => k)
890
+ const t = makeT(rootCtx, props)
749
891
  const [status, setStatus] = React.useState('loading')
750
892
  const [presets, setPresets] = React.useState([])
751
893
  const [defaultPreset, setDefaultPreset] = React.useState('default')
@@ -858,6 +1000,64 @@ window.__ModuleLoader__.load({
858
1000
  )
859
1001
  }
860
1002
 
1003
+
1004
+ // ── ERROR BOUNDARY ──────────────────────────────────────────────
1005
+ class MoAErrorBoundary extends React.Component {
1006
+ constructor(props) {
1007
+ super(props)
1008
+ this.state = { hasError: false, error: null, errorInfo: null }
1009
+ }
1010
+ static getDerivedStateFromError(error) {
1011
+ return { hasError: true, error }
1012
+ }
1013
+ componentDidCatch(error, errorInfo) {
1014
+ console.error('[dsh-moa] MoAErrorBoundary caught error:', error, errorInfo)
1015
+ this.setState({ errorInfo })
1016
+ }
1017
+ render() {
1018
+ if (this.state.hasError) {
1019
+ return React.createElement(
1020
+ 'div',
1021
+ {
1022
+ style: {
1023
+ padding: '16px',
1024
+ borderRadius: '8px',
1025
+ border: '1px solid #ef4444',
1026
+ background: 'rgba(239,68,68,0.08)',
1027
+ color: '#ef4444',
1028
+ fontSize: '13px',
1029
+ fontFamily: 'monospace',
1030
+ whiteSpace: 'pre-wrap',
1031
+ wordBreak: 'break-word',
1032
+ },
1033
+ },
1034
+ React.createElement('div', { style: { fontWeight: 700, marginBottom: 8 } }, '⚠ MoA Settings Error'),
1035
+ React.createElement('div', null, String(this.state.error)),
1036
+ this.state.errorInfo && React.createElement(
1037
+ 'details',
1038
+ { style: { marginTop: 8 } },
1039
+ React.createElement('summary', null, 'Component stack'),
1040
+ React.createElement('pre', { style: { fontSize: 11 } }, this.state.errorInfo.componentStack || '')
1041
+ ),
1042
+ React.createElement(
1043
+ 'button',
1044
+ {
1045
+ type: 'button',
1046
+ style: {
1047
+ marginTop: 12, padding: '6px 16px', border: '1px solid #ef4444',
1048
+ background: 'transparent', color: '#ef4444', borderRadius: 6,
1049
+ cursor: 'pointer', fontSize: 12,
1050
+ },
1051
+ onClick: () => this.setState({ hasError: false, error: null, errorInfo: null }),
1052
+ },
1053
+ 'Retry'
1054
+ )
1055
+ )
1056
+ }
1057
+ return this.props.children
1058
+ }
1059
+ }
1060
+
861
1061
  function MoACard(props) {
862
1062
  ensureStyles()
863
1063
  const state = useMoASettings(props)
@@ -913,134 +1113,39 @@ window.__ModuleLoader__.load({
913
1113
 
914
1114
  exports.inject = ['slots', 'locale', 'inputTriggers']
915
1115
  exports.apply = function apply(ctx) {
916
- const registerLocale = () => {
1116
+ rootCtx = ctx
1117
+ const t = makeT(ctx)
1118
+ const addLocale = (locale, dictionary) => {
917
1119
  try {
918
1120
  if (ctx.locale && typeof ctx.locale.register === 'function') {
919
- return ctx.locale.register(NS, {
920
- en: {
921
- title: 'Mixture of Agents (MoA)',
922
- description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
923
- slashMoa: 'Run task through Mixture of Agents (/moa <prompt>)',
924
- defaultPreset: 'Default Preset (used on /moa without arguments)',
925
- isDefault: 'default',
926
- presetSelectLabel: 'Active Preset to Configure:',
927
- createNewPreset: 'Create new preset...',
928
- makeDefault: 'Make Default',
929
- makeDefaultHint: 'Use this preset for /moa when no preset argument is given',
930
- deletePreset: 'Delete preset',
931
- delete: 'Delete',
932
- confirmDeletePreset: 'Are you sure you want to delete preset',
933
- newPresetPlaceholder: 'New preset name (e.g. fast-audit)',
934
- create: 'Create',
935
- cancel: 'Cancel',
936
- aggregatorTitle: 'Judge / Aggregator Model (who reviews and synthesizes)',
937
- aggregatorDesc: 'Evaluates candidate proposals, corrects hallucinations, and outputs the final unified solution.',
938
- temperature: 'Temperature',
939
- aggTempHint: 'Recommended 0.2 - 0.4 for rigorous verification and factual synthesis.',
940
- proposersTitle: 'Candidate Models (Advisors)',
941
- proposersDesc: 'Generates independent candidate answers in parallel to provide diverse perspectives.',
942
- refTempLabel: 'Candidates Temperature (solution diversity):',
943
- refTempHint: 'Recommended 0.6 - 0.8 for diverse creative hypotheses.',
944
- parallelCount: 'Parallel workers',
945
- candidate: 'Candidate',
946
- addCandidate: 'Add parallel model slot',
947
- remove: 'Remove',
948
- save: 'Save Changes',
949
- saved: 'Saved successfully',
950
- loading: 'Saving…',
951
- retry: 'Retry connection',
952
- unavailable: 'MoA service temporarily unavailable (reconnecting...)',
953
- searchPlaceholder: 'Search by model name or provider...',
954
- modelsFound: 'Found',
955
- of: 'of',
956
- clear: 'Clear',
957
- noModelsMatch: 'No models found matching query.',
958
- useCustom: 'Use custom',
959
- selectModel: 'Select model...',
960
- },
961
- ru: {
962
- title: 'Mixture of Agents (MoA)',
963
- description: 'Запуск задач через ансамбль параллельных моделей-советников и модель-судью',
964
- slashMoa: 'Запустить задачу через архитектуру Mixture of Agents (/moa <prompt>)',
965
- defaultPreset: 'Основной пресет (при вызове /moa без параметров)',
966
- isDefault: 'основной',
967
- presetSelectLabel: 'Текущий пресет для настройки:',
968
- createNewPreset: 'Создать новый пресет...',
969
- makeDefault: 'Сделать основным',
970
- makeDefaultHint: 'Использовать этот пресет для /moa при вызове без параметров',
971
- deletePreset: 'Удалить пресет',
972
- delete: 'Удалить',
973
- confirmDeletePreset: 'Вы уверены, что хотите удалить пресет',
974
- newPresetPlaceholder: 'Название нового пресета (например: fast-audit)',
975
- create: 'Создать',
976
- cancel: 'Отмена',
977
- aggregatorTitle: 'Ведущая модель / Судья (кто проверяет и объединяет)',
978
- aggregatorDesc: 'Сопоставляет варианты кандидатов, устраняет ошибки и синтезирует итоговое решение.',
979
- temperature: 'Температура',
980
- aggTempHint: 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
981
- proposersTitle: 'Модели-кандидаты (советники)',
982
- proposersDesc: 'Параллельно формируют независимые варианты решения одной задачи.',
983
- refTempLabel: 'Температура кандидатов (разнообразие решений):',
984
- refTempHint: 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.',
985
- parallelCount: 'Параллельных моделей',
986
- candidate: 'Кандидат',
987
- addCandidate: 'Добавить модель в параллель',
988
- remove: 'Удалить',
989
- save: 'Сохранить',
990
- saved: 'Успешно сохранено',
991
- loading: 'Сохранение…',
992
- retry: 'Повторить подключение',
993
- unavailable: 'Служба MoA временно недоступна (переподключение...)',
994
- searchPlaceholder: 'Поиск модели по названию или провайдеру...',
995
- modelsFound: 'Найдено',
996
- of: 'из',
997
- clear: 'Сбросить',
998
- noModelsMatch: 'Модели не найдены.',
999
- useCustom: 'Использовать кастомное',
1000
- selectModel: 'Выберите модель...',
1001
- },
1002
- })
1121
+ return ctx.locale.register(NS, locale, dictionary)
1003
1122
  }
1004
- } catch (err) {
1005
- console.warn('[dsh-moa] locale registration error:', err)
1123
+ } catch {
1124
+ // Если язык уже занят (например, dsh-russian-lang) или зарегистрирован ранее — уступаем без ошибки
1125
+ return () => {}
1006
1126
  }
1127
+ return () => {}
1007
1128
  }
1008
1129
 
1009
1130
  if (typeof ctx.effect === 'function') {
1010
- ctx.effect(() => registerLocale(), 'dsh-moa: dictionaries')
1131
+ ctx.effect(() => {
1132
+ const undo = [addLocale('en', en), addLocale('ru', ru)]
1133
+ return () => {
1134
+ for (const off of undo) {
1135
+ if (typeof off === 'function') {
1136
+ try { off() } catch {}
1137
+ }
1138
+ }
1139
+ }
1140
+ }, 'dsh-moa: dictionaries')
1011
1141
  } else {
1012
- registerLocale()
1142
+ addLocale('en', en)
1143
+ addLocale('ru', ru)
1013
1144
  }
1014
1145
 
1015
- const t = (key) => {
1016
- try {
1017
- if (ctx.locale && typeof ctx.locale.bind === 'function') {
1018
- return ctx.locale.bind(NS)(key) || key
1019
- }
1020
- } catch {}
1021
- return key
1022
- }
1023
1146
 
1024
1147
  if (ctx.slots) {
1025
- const registerDirectSection = () => {
1026
- try {
1027
- ctx.slots.register(
1028
- {
1029
- name: 'settings.section',
1030
- id: NS,
1031
- order: 35,
1032
- locale: NS,
1033
- label: () => t('title'),
1034
- inject: () => ({ ctx, t }),
1035
- },
1036
- MoASection
1037
- )
1038
- } catch (err) {
1039
- console.warn('[dsh-moa] Failed to register settings.section:', err)
1040
- }
1041
- }
1042
-
1043
- const registerPluginCard = () => {
1148
+ const registerCard = () => {
1044
1149
  try {
1045
1150
  ctx.slots.register(
1046
1151
  {
@@ -1048,10 +1153,9 @@ window.__ModuleLoader__.load({
1048
1153
  key: NS,
1049
1154
  order: 35,
1050
1155
  locale: NS,
1051
- label: () => t('title'),
1052
- inject: () => ({ ctx, t }),
1156
+ inject: () => ({ ctx }),
1053
1157
  },
1054
- MoACard
1158
+ (props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
1055
1159
  )
1056
1160
  } catch (err) {
1057
1161
  console.warn('[dsh-moa] Failed to register settings.plugin.item:', err)
@@ -1059,11 +1163,9 @@ window.__ModuleLoader__.load({
1059
1163
  }
1060
1164
 
1061
1165
  if (typeof ctx.slots.inject === 'function') {
1062
- ctx.slots.inject('settings.section', registerDirectSection)
1063
- ctx.slots.inject('settings.plugin.item', registerPluginCard)
1166
+ ctx.slots.inject('settings.plugin.item', registerCard)
1064
1167
  } else {
1065
- registerDirectSection()
1066
- registerPluginCard()
1168
+ registerCard()
1067
1169
  }
1068
1170
  }
1069
1171
 
@@ -85,6 +85,89 @@ function sanitizePath(filePath) {
85
85
  return normalized.replace(/^[\\\/]+/, '')
86
86
  }
87
87
 
88
+ /**
89
+ * Collects readable text files from current workspace to feed as context for candidate models (#6).
90
+ * Ignores node_modules, .git, .moa, binary files, large bundles.
91
+ */
92
+ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
93
+ if (!baseDir) return { files: [], totalChars: 0 }
94
+
95
+ const ignoreDirs = new Set(['node_modules', '.git', '.moa', '.worktrees', 'dist', 'build', '.dsh'])
96
+ const codeExts = new Set([
97
+ '.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx',
98
+ '.vue', '.svelte', '.json', '.py', '.md', '.txt', '.sh', '.yaml', '.yml',
99
+ ])
100
+
101
+ const collected = []
102
+ let totalChars = 0
103
+
104
+ async function scan(currentDir, relPrefix = '') {
105
+ if (totalChars >= maxCharBudget) return
106
+ try {
107
+ const list = await fs.readdir(currentDir, { withFileTypes: true })
108
+ for (const item of list) {
109
+ if (totalChars >= maxCharBudget) break
110
+ if (item.name.startsWith('.') && item.name !== '.env') continue
111
+
112
+ const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
113
+ const full = path.join(currentDir, item.name)
114
+
115
+ if (item.isDirectory()) {
116
+ if (!ignoreDirs.has(item.name)) {
117
+ await scan(full, rel)
118
+ }
119
+ } else if (item.isFile()) {
120
+ const ext = path.extname(item.name).toLowerCase()
121
+ if (codeExts.has(ext)) {
122
+ try {
123
+ const stat = await fs.stat(full)
124
+ if (stat.size > 100000) continue // skip files > 100kb
125
+ const content = await fs.readFile(full, 'utf8')
126
+ const budgetRemaining = maxCharBudget - totalChars
127
+ const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
128
+ totalChars += truncated.length
129
+ collected.push({ relativePath: rel, content: truncated, size: stat.size })
130
+ } catch {}
131
+ }
132
+ }
133
+ }
134
+ } catch {}
135
+ }
136
+
137
+ await scan(baseDir)
138
+ return { files: collected, totalChars }
139
+ }
140
+
141
+ /**
142
+ * Checks if a task prompt is an iterative refinement / modification
143
+ * of an existing project rather than a greenfield project creation (#4).
144
+ */
145
+ export function isRefinementTask(userPrompt = '', projectFiles = []) {
146
+ if (!Array.isArray(projectFiles) || projectFiles.length === 0) return false
147
+ const p = userPrompt.toLowerCase().trim()
148
+ if (!p) return false
149
+
150
+ // Greenfield creation phrases - ALWAYS fresh task
151
+ const freshPhrases = [
152
+ 'с нуля', 'from scratch', 'новый проект', 'новое приложение', 'new project', 'new app',
153
+ 'создай проект', 'создай приложение', 'создай игру', 'создай сервис',
154
+ 'сделай проект', 'сделай приложение', 'сделай игру',
155
+ 'create a new', 'build a new', 'generate a new',
156
+ ]
157
+ if (freshPhrases.some((phrase) => p.includes(phrase))) {
158
+ return false
159
+ }
160
+
161
+ // Modification keywords
162
+ const modKeywords = [
163
+ 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали',
164
+ 'add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust',
165
+ 'style', 'css', 'темн', 'светл', 'color', 'цвет', 'кнопк', 'theme', 'bug', 'баг',
166
+ ]
167
+
168
+ return modKeywords.some((kw) => p.includes(kw))
169
+ }
170
+
88
171
  /**
89
172
  * Writes candidate files into an isolated candidate workspace:
90
173
  * <baseDir>/.moa/candidate-<index>/
@@ -114,7 +197,7 @@ export async function writeCandidateWorkspace(baseDir, candidateIndex, files = [
114
197
  * Promotes winning candidate files to baseDir root,
115
198
  * then cleans up the temporary .moa folder.
116
199
  */
117
- export async function promoteCandidateWorkspace(baseDir, winningIndex) {
200
+ export async function promoteCandidateWorkspace(baseDir, winningIndex, options = { keepMoa: false }) {
118
201
  if (!baseDir || winningIndex === null || winningIndex === undefined) return []
119
202
 
120
203
  const winningDir = path.join(baseDir, '.moa', `candidate-${winningIndex}`)
@@ -123,7 +206,9 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex) {
123
206
  try {
124
207
  const entries = await getFilesRecursively(winningDir)
125
208
  if (entries.length === 0) {
126
- try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
209
+ if (!options.keepMoa) {
210
+ try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
211
+ }
127
212
  return []
128
213
  }
129
214
 
@@ -137,12 +222,16 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex) {
137
222
  promoted.push(rel)
138
223
  }
139
224
 
140
- // Clean up temporary .moa folder
141
- await fs.rm(moaRoot, { recursive: true, force: true })
225
+ if (!options.keepMoa) {
226
+ // Clean up temporary .moa folder
227
+ await fs.rm(moaRoot, { recursive: true, force: true })
228
+ }
142
229
 
143
230
  return promoted
144
231
  } catch (err) {
145
- try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
232
+ if (!options.keepMoa) {
233
+ try { await fs.rm(moaRoot, { recursive: true, force: true }) } catch {}
234
+ }
146
235
  return []
147
236
  }
148
237
  }