@goodandready/dsh-moa 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1090 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@goodandready/dsh-moa',
3
+ factory: (require) => {
4
+ var module = { exports: {} }
5
+ var exports = module.exports
6
+ let React = require('react')
7
+
8
+ const NS = 'dsh-moa'
9
+
10
+ let ChevronIcon = null
11
+ try {
12
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
13
+ ChevronIcon = primitives && primitives.IconChevronDownOutline14
14
+ } catch {
15
+ ChevronIcon = null
16
+ }
17
+
18
+ function FallbackChevron({ className }) {
19
+ return React.createElement(
20
+ 'svg',
21
+ {
22
+ className,
23
+ width: 14,
24
+ height: 14,
25
+ viewBox: '0 0 14 14',
26
+ fill: 'none',
27
+ stroke: 'currentColor',
28
+ strokeWidth: 1.5,
29
+ strokeLinecap: 'round',
30
+ strokeLinejoin: 'round',
31
+ },
32
+ React.createElement('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' })
33
+ )
34
+ }
35
+
36
+ const Chevron = ChevronIcon || FallbackChevron
37
+
38
+ const STYLES = `
39
+ .moa-section { display:flex; flex-direction:column; gap:16px; padding:4px 0; max-width:780px; }
40
+ .moa-section-title { font-size:18px; font-weight:700; color:var(--dsw-alias-label-primary); }
41
+ .moa-section-sub { font-size:13px; color:var(--dsw-alias-label-secondary); margin-top:2px; margin-bottom:6px; }
42
+ .moa-card { border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); border-radius:12px; list-style:none; margin-bottom:12px; }
43
+ .moa-head { appearance:none; width:100%; font:inherit; color:inherit; text-align:left; cursor:pointer; background:0 0; border:0; border-radius:12px; display:flex; align-items:center; gap:12px; padding:14px 16px; }
44
+ .moa-head:focus-visible { outline:2px solid var(--dsw-alias-label-primary); outline-offset:2px; }
45
+ .moa-title { color:var(--dsw-alias-label-primary); font-size:15px; font-weight:600; line-height:1.4; }
46
+ .moa-sub { color:var(--dsw-alias-label-secondary); font-size:13px; margin-top:2px; }
47
+ .moa-chev { margin-left:auto; flex:none; color:var(--dsw-alias-label-tertiary); transition:transform .16s ease; }
48
+ .moa-chev-open { transform:rotate(180deg); }
49
+ .moa-body { border-top:1px solid var(--dsw-alias-border-l2); margin:0 16px; padding:12px 0 16px; display:flex; flex-direction:column; gap:14px; }
50
+ .moa-box { border:1px solid var(--dsw-alias-border-l2); border-radius:10px; padding:14px 16px; background:var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.02)); display:flex; flex-direction:column; gap:12px; }
51
+ .moa-box-title { font-size:14px; font-weight:600; color:var(--dsw-alias-label-primary); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px; }
52
+ .moa-field { display:flex; flex-direction:column; gap:6px; }
53
+ .moa-label { font-size:13px; font-weight:500; color:var(--dsw-alias-label-primary); }
54
+ .moa-hint { font-size:12px; color:var(--dsw-alias-label-secondary); line-height:1.4; }
55
+ .moa-select { height:36px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:8px; padding:0 10px; font-size:13px; }
56
+ .moa-select:focus { outline:1px solid var(--dsw-alias-label-primary); }
57
+ .moa-count-bar { display:flex; align-items:center; gap:8px; }
58
+ .moa-count-btn { appearance:none; font:inherit; padding:4px 12px; border-radius:6px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); font-size:12px; cursor:pointer; }
59
+ .moa-count-btn-active { background:var(--dsw-alias-label-primary); color:var(--dsw-alias-bg-layer-3); border-color:transparent; font-weight:600; }
60
+ .moa-refs-list { display:flex; flex-direction:column; gap:10px; }
61
+ .moa-ref-row { display:flex; align-items:center; gap:8px; }
62
+ .moa-ref-badge { font-size:12px; font-weight:600; color:var(--dsw-alias-label-secondary); width:85px; flex-shrink:0; }
63
+ .moa-ref-picker { flex:1; min-width:0; }
64
+ .moa-btn-del { appearance:none; font:inherit; border:none; background:transparent; color:var(--dsw-alias-label-tertiary); cursor:pointer; padding:6px 10px; font-size:14px; border-radius:6px; }
65
+ .moa-btn-del:hover { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-3); }
66
+ .moa-btn-add { appearance:none; font:inherit; border:1px dashed var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); border-radius:8px; padding:8px 12px; font-size:13px; font-weight:500; cursor:pointer; text-align:center; transition:border-color .15s; }
67
+ .moa-btn-add:hover { border-color:var(--dsw-alias-label-primary); }
68
+ .moa-foot { border-top:1px solid var(--dsw-alias-border-l2); display:flex; justify-content:flex-end; align-items:center; gap:10px; padding:14px 0 4px; }
69
+ .moa-save { appearance:none; font:inherit; cursor:pointer; border:1px solid transparent; border-radius:8px; padding:8px 20px; font-size:13px; font-weight:600; background:var(--dsw-alias-label-primary); color:var(--dsw-alias-bg-layer-3); transition:opacity .15s; }
70
+ .moa-save:hover { opacity:0.9; }
71
+ .moa-status-text { font-size:12px; color:var(--dsw-alias-label-secondary); }
72
+
73
+ /* Preset Toolbar & Actions */
74
+ .moa-preset-bar { display:flex; flex-direction:column; gap:8px; margin-bottom:4px; }
75
+ .moa-preset-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
76
+ .moa-btn-sub { appearance:none; font:inherit; padding:7px 12px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); font-size:12px; font-weight:500; cursor:pointer; white-space:nowrap; }
77
+ .moa-btn-sub:hover { border-color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-3); }
78
+ .moa-btn-del-preset { appearance:none; font:inherit; padding:7px 12px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:#ef4444; font-size:12px; font-weight:500; cursor:pointer; white-space:nowrap; }
79
+ .moa-btn-del-preset:hover { border-color:#ef4444; background:rgba(239,68,68,0.08); }
80
+ .moa-new-preset-box { display:flex; align-items:center; gap:8px; padding:10px 12px; border:1px dashed var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)); border-radius:8px; background:var(--dsw-alias-bg-layer-2); margin-top:4px; }
81
+ .moa-new-input { flex:1; height:32px; padding:0 10px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:6px; font-size:13px; outline:none; }
82
+ .moa-new-input:focus { border-color:var(--dsw-alias-label-primary); }
83
+
84
+ /* Temperature Sliders */
85
+ .moa-temp-ctrl { display:flex; align-items:center; gap:12px; margin-top:2px; }
86
+ .moa-temp-slider { flex:1; accent-color:var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)); cursor:pointer; height:4px; }
87
+ .moa-temp-input { width:62px; height:30px; text-align:center; padding:0 6px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:6px; font-size:13px; font-weight:600; outline:none; }
88
+ .moa-temp-input:focus { border-color:var(--dsw-alias-label-primary); }
89
+
90
+ /* Searchable Model Picker */
91
+ .moa-picker-container { position:relative; width:100%; }
92
+ .moa-picker-btn { width:100%; min-height:36px; padding:5px 12px; background:var(--dsw-alias-bg-layer-3); 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; }
93
+ .moa-picker-btn:hover { border-color:var(--dsw-alias-label-secondary); }
94
+ .moa-picker-btn:focus-visible { outline:2px solid var(--dsw-alias-label-primary); outline-offset:1px; }
95
+ .moa-picker-summary { display:flex; align-items:center; gap:8px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
96
+ .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; }
97
+ .moa-model-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:500; }
98
+ .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 10px 30px rgba(0,0,0,0.4); z-index:1000; max-height:360px; display:flex; flex-direction:column; overflow:hidden; }
99
+ .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; }
100
+ .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; }
101
+ .moa-search-input:focus { border-color:var(--dsw-alias-label-primary); }
102
+ .moa-search-info { font-size:11px; color:var(--dsw-alias-label-tertiary); padding:0 2px; display:flex; justify-content:space-between; }
103
+ .moa-picker-list { overflow-y:auto; max-height:280px; padding:6px; display:flex; flex-direction:column; gap:2px; }
104
+ .moa-group-title { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--dsw-alias-label-tertiary); padding:8px 8px 4px; }
105
+ .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; }
106
+ .moa-picker-item:hover, .moa-picker-item-selected { background:var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); }
107
+ .moa-picker-check { color:var(--dsw-alias-brand-primary, #10b981); font-weight:bold; font-size:14px; flex-shrink:0; }
108
+ .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; }
109
+ .moa-custom-opt:hover { background:var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); }
110
+ `
111
+
112
+ let stylesInjected = false
113
+ function ensureStyles() {
114
+ if (stylesInjected || typeof document === 'undefined') return
115
+ stylesInjected = true
116
+ const style = document.createElement('style')
117
+ style.textContent = STYLES
118
+ document.head.appendChild(style)
119
+ }
120
+
121
+ const PROVIDER_TITLES = {
122
+ codex: 'ChatGPT Codex (Subscription)',
123
+ claude: 'Claude (Anthropic Subscription)',
124
+ grok: 'Grok (xAI Subscription)',
125
+ antigravity: 'Antigravity (Google)',
126
+ gemini: 'Google Gemini',
127
+ commandcode: 'CommandCode Provider',
128
+ 'opencode-go': 'OpenCode-Go',
129
+ 'qwen-token-plan': 'Qwen (Token Plan)',
130
+ groq: 'Groq Cloud',
131
+ zai: 'Zhipu GLM (Zai)',
132
+ ollama: 'Ollama (Local / Cloud)',
133
+ 'kimi-coding': 'Kimi Coding',
134
+ minimax: 'MiniMax',
135
+ 'xiaomi-token-plan-sgp': 'Xiaomi Token Plan',
136
+ 'deepseek-official': 'DeepSeek Official',
137
+ }
138
+
139
+ function SearchableModelPicker({
140
+ provider,
141
+ model,
142
+ onChange,
143
+ availableModels,
144
+ t,
145
+ }) {
146
+ const [open, setOpen] = React.useState(false)
147
+ const [query, setQuery] = React.useState('')
148
+ const rootRef = React.useRef(null)
149
+ const inputRef = React.useRef(null)
150
+
151
+ const selectedLabel = React.useMemo(() => {
152
+ if (!provider && !model) return t('selectModel') || 'Выберите модель...'
153
+ const found = availableModels.find((m) => m.provider === provider && m.model === model)
154
+ return found ? (found.label || `${provider}: ${found.model}`) : `${provider}:${model}`
155
+ }, [availableModels, provider, model, t])
156
+
157
+ const filtered = React.useMemo(() => {
158
+ const q = query.trim().toLowerCase()
159
+ if (!q) return availableModels
160
+ return availableModels.filter((m) => {
161
+ const prov = (m.provider || '').toLowerCase()
162
+ const mod = (m.model || '').toLowerCase()
163
+ const lbl = (m.label || '').toLowerCase()
164
+ return prov.includes(q) || mod.includes(q) || lbl.includes(q)
165
+ })
166
+ }, [availableModels, query])
167
+
168
+ const grouped = React.useMemo(() => {
169
+ const map = new Map()
170
+ for (const m of filtered) {
171
+ const p = m.provider || 'other'
172
+ if (!map.has(p)) map.set(p, [])
173
+ map.get(p).push(m)
174
+ }
175
+ const groups = []
176
+ for (const [prov, items] of map.entries()) {
177
+ const title = PROVIDER_TITLES[prov] || prov.toUpperCase()
178
+ groups.push({ provider: prov, title, items })
179
+ }
180
+ groups.sort((a, b) => a.title.localeCompare(b.title))
181
+ return groups
182
+ }, [filtered])
183
+
184
+ React.useEffect(() => {
185
+ if (!open) return
186
+ const onDown = (e) => {
187
+ if (rootRef.current && !rootRef.current.contains(e.target)) {
188
+ setOpen(false)
189
+ }
190
+ }
191
+ document.addEventListener('mousedown', onDown)
192
+ return () => document.removeEventListener('mousedown', onDown)
193
+ }, [open])
194
+
195
+ React.useEffect(() => {
196
+ if (open && inputRef.current) {
197
+ inputRef.current.focus()
198
+ }
199
+ }, [open])
200
+
201
+ const handleSelect = (item) => {
202
+ onChange(item.provider, item.model)
203
+ setOpen(false)
204
+ setQuery('')
205
+ }
206
+
207
+ const handleCustom = () => {
208
+ const q = query.trim()
209
+ if (!q) return
210
+ const parts = q.split(':')
211
+ if (parts.length >= 2) {
212
+ onChange(parts[0].trim(), parts.slice(1).join(':').trim())
213
+ } else {
214
+ onChange(provider || 'custom', q)
215
+ }
216
+ setOpen(false)
217
+ setQuery('')
218
+ }
219
+
220
+ return React.createElement(
221
+ 'div',
222
+ { ref: rootRef, className: 'moa-picker-container' },
223
+ React.createElement(
224
+ 'button',
225
+ {
226
+ type: 'button',
227
+ className: 'moa-picker-btn',
228
+ onClick: () => setOpen(!open),
229
+ },
230
+ React.createElement(
231
+ 'div',
232
+ { className: 'moa-picker-summary' },
233
+ provider && React.createElement('span', { className: 'moa-prov-chip' }, provider),
234
+ React.createElement('span', { className: 'moa-model-name' }, selectedLabel)
235
+ ),
236
+ React.createElement(Chevron, { className: open ? 'moa-chev moa-chev-open' : 'moa-chev' })
237
+ ),
238
+ open &&
239
+ React.createElement(
240
+ 'div',
241
+ { className: 'moa-picker-popover' },
242
+ React.createElement(
243
+ 'div',
244
+ { className: 'moa-picker-search-bar' },
245
+ React.createElement('input', {
246
+ ref: inputRef,
247
+ type: 'text',
248
+ className: 'moa-search-input',
249
+ placeholder: t('searchPlaceholder') || 'Поиск по названию или провайдеру...',
250
+ value: query,
251
+ onChange: (e) => setQuery(e.target.value),
252
+ onKeyDown: (e) => {
253
+ if (e.key === 'Escape') setOpen(false)
254
+ if (e.key === 'Enter' && query.trim()) handleCustom()
255
+ },
256
+ }),
257
+ React.createElement(
258
+ 'div',
259
+ { className: 'moa-search-info' },
260
+ React.createElement(
261
+ 'span',
262
+ null,
263
+ `${t('modelsFound') || 'Найдено'}: ${filtered.length} ${t('of') || 'из'} ${availableModels.length}`
264
+ ),
265
+ query &&
266
+ React.createElement(
267
+ 'span',
268
+ { style: { cursor: 'pointer' }, onClick: () => setQuery('') },
269
+ t('clear') || 'Сбросить'
270
+ )
271
+ )
272
+ ),
273
+ React.createElement(
274
+ 'div',
275
+ { className: 'moa-picker-list' },
276
+ grouped.map((g) =>
277
+ React.createElement(
278
+ React.Fragment,
279
+ { key: g.provider },
280
+ React.createElement('div', { className: 'moa-group-title' }, g.title),
281
+ g.items.map((item) => {
282
+ const isCur = item.provider === provider && item.model === model
283
+ return React.createElement(
284
+ 'button',
285
+ {
286
+ key: `${item.provider}:${item.model}`,
287
+ type: 'button',
288
+ className: isCur ? 'moa-picker-item moa-picker-item-selected' : 'moa-picker-item',
289
+ onClick: () => handleSelect(item),
290
+ },
291
+ React.createElement(
292
+ 'div',
293
+ { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 } },
294
+ React.createElement('span', { className: 'moa-prov-chip' }, item.provider),
295
+ React.createElement('span', { className: 'moa-model-name' }, item.label || item.model)
296
+ ),
297
+ isCur && React.createElement('span', { className: 'moa-picker-check' }, '✓')
298
+ )
299
+ })
300
+ )
301
+ ),
302
+ filtered.length === 0 &&
303
+ React.createElement(
304
+ 'div',
305
+ { style: { padding: '12px 10px', fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } },
306
+ t('noModelsMatch') || 'Модели не найдены.'
307
+ )
308
+ ),
309
+ query.trim() &&
310
+ React.createElement(
311
+ 'div',
312
+ { className: 'moa-custom-opt', onClick: handleCustom },
313
+ React.createElement('span', null, '➕'),
314
+ React.createElement(
315
+ 'span',
316
+ null,
317
+ `${t('useCustom') || 'Использовать'}: "${query.trim()}"`
318
+ )
319
+ )
320
+ )
321
+ )
322
+ }
323
+
324
+ function MoAEditor({
325
+ t,
326
+ presets,
327
+ setPresets,
328
+ defaultPreset,
329
+ setDefaultPreset,
330
+ availableModels,
331
+ handleSave,
332
+ saveStatus,
333
+ }) {
334
+ const [selectedPresetName, setSelectedPresetName] = React.useState(defaultPreset || (presets[0] && presets[0].name) || 'default')
335
+ const [isCreating, setIsCreating] = React.useState(false)
336
+ const [newPresetName, setNewPresetName] = React.useState('')
337
+
338
+ // Sync active preset if selected one is removed
339
+ React.useEffect(() => {
340
+ if (!presets.some((p) => p.name === selectedPresetName)) {
341
+ setSelectedPresetName(defaultPreset || (presets[0] && presets[0].name) || 'default')
342
+ }
343
+ }, [presets, selectedPresetName, defaultPreset])
344
+
345
+ const currentPreset = presets.find((p) => p.name === selectedPresetName) || presets[0] || {
346
+ name: 'default',
347
+ reference_models: [],
348
+ aggregator: { provider: '', model: '' },
349
+ aggregator_temperature: 0.4,
350
+ reference_temperature: 0.6,
351
+ }
352
+
353
+ const updateCurrentPreset = (modifier) => {
354
+ setPresets((prev) => {
355
+ const copy = structuredClone(prev || [])
356
+ const idx = copy.findIndex((p) => p.name === currentPreset.name)
357
+ if (idx !== -1) {
358
+ copy[idx] = modifier(copy[idx])
359
+ }
360
+ return copy
361
+ })
362
+ }
363
+
364
+ const setAggregatorModel = (prov, mod) => {
365
+ updateCurrentPreset((p) => ({
366
+ ...p,
367
+ aggregator: { provider: prov, model: mod },
368
+ }))
369
+ }
370
+
371
+ const setAggregatorTemp = (temp) => {
372
+ const val = Math.round(Number(temp) * 100) / 100
373
+ updateCurrentPreset((p) => ({ ...p, aggregator_temperature: val }))
374
+ }
375
+
376
+ const setReferenceTemp = (temp) => {
377
+ const val = Math.round(Number(temp) * 100) / 100
378
+ updateCurrentPreset((p) => ({ ...p, reference_temperature: val }))
379
+ }
380
+
381
+ const setReferenceModel = (idx, prov, mod) => {
382
+ updateCurrentPreset((p) => {
383
+ const refs = [...(p.reference_models || [])]
384
+ refs[idx] = { provider: prov, model: mod }
385
+ return { ...p, reference_models: refs }
386
+ })
387
+ }
388
+
389
+ const setReferenceCount = (targetCount) => {
390
+ updateCurrentPreset((p) => {
391
+ let refs = [...(p.reference_models || [])]
392
+ if (refs.length > targetCount) {
393
+ refs = refs.slice(0, targetCount)
394
+ } else {
395
+ while (refs.length < targetCount) {
396
+ const fallback = availableModels[refs.length % (availableModels.length || 1)] || {
397
+ provider: 'opencode-go',
398
+ model: 'deepseek-v4-flash',
399
+ }
400
+ refs.push({ provider: fallback.provider, model: fallback.model })
401
+ }
402
+ }
403
+ return { ...p, reference_models: refs }
404
+ })
405
+ }
406
+
407
+ const removeReference = (idx) => {
408
+ updateCurrentPreset((p) => {
409
+ const refs = [...(p.reference_models || [])]
410
+ refs.splice(idx, 1)
411
+ return { ...p, reference_models: refs }
412
+ })
413
+ }
414
+
415
+ const addReference = () => {
416
+ updateCurrentPreset((p) => {
417
+ const refs = [...(p.reference_models || [])]
418
+ const fallback = availableModels[refs.length % (availableModels.length || 1)] || {
419
+ provider: 'opencode-go',
420
+ model: 'deepseek-v4-flash',
421
+ }
422
+ refs.push({ provider: fallback.provider, model: fallback.model })
423
+ return { ...p, reference_models: refs }
424
+ })
425
+ }
426
+
427
+ const handleCreatePreset = () => {
428
+ const name = newPresetName.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-')
429
+ if (!name) return
430
+ if (presets.some((p) => p.name === name)) {
431
+ setSelectedPresetName(name)
432
+ setIsCreating(false)
433
+ setNewPresetName('')
434
+ return
435
+ }
436
+ const created = {
437
+ name,
438
+ enabled: true,
439
+ reference_models: currentPreset?.reference_models?.length ? structuredClone(currentPreset.reference_models) : [
440
+ { provider: 'opencode-go', model: 'deepseek-v4-flash' },
441
+ { provider: 'codex', model: 'gpt-5.6-sol' },
442
+ ],
443
+ aggregator: currentPreset?.aggregator?.provider ? structuredClone(currentPreset.aggregator) : {
444
+ provider: 'antigravity',
445
+ model: 'gemini-3-flash',
446
+ },
447
+ reference_temperature: currentPreset?.reference_temperature ?? 0.6,
448
+ aggregator_temperature: currentPreset?.aggregator_temperature ?? 0.4,
449
+ max_tokens: currentPreset?.max_tokens ?? 4096,
450
+ }
451
+ setPresets((prev) => [...prev, created])
452
+ setSelectedPresetName(name)
453
+ setIsCreating(false)
454
+ setNewPresetName('')
455
+ }
456
+
457
+ const handleDeletePreset = () => {
458
+ if (presets.length <= 1) return
459
+ if (!confirm(`${t('confirmDeletePreset') || 'Удалить пресет'} "${currentPreset.name}"?`)) return
460
+ const remaining = presets.filter((p) => p.name !== currentPreset.name)
461
+ if (defaultPreset === currentPreset.name) {
462
+ setDefaultPreset(remaining[0].name)
463
+ }
464
+ setPresets(remaining)
465
+ setSelectedPresetName(remaining[0].name)
466
+ }
467
+
468
+ const currentRefs = currentPreset.reference_models || []
469
+ const aggTemp = currentPreset.aggregator_temperature ?? 0.4
470
+ const refTemp = currentPreset.reference_temperature ?? 0.6
471
+
472
+ return React.createElement(
473
+ React.Fragment,
474
+ null,
475
+ /* Preset Selector Toolbar (No top tab buttons!) */
476
+ React.createElement(
477
+ 'div',
478
+ { className: 'moa-preset-bar' },
479
+ React.createElement(
480
+ 'div',
481
+ { className: 'moa-field' },
482
+ React.createElement('label', { className: 'moa-label' }, t('presetSelectLabel') || 'Текущий пресет для настройки:'),
483
+ React.createElement(
484
+ 'div',
485
+ { className: 'moa-preset-row' },
486
+ React.createElement(
487
+ 'select',
488
+ {
489
+ className: 'moa-select',
490
+ style: { flex: 1, minWidth: 200 },
491
+ value: isCreating ? '__new__' : currentPreset.name,
492
+ onChange: (e) => {
493
+ if (e.target.value === '__new__') {
494
+ setIsCreating(true)
495
+ } else {
496
+ setIsCreating(false)
497
+ setSelectedPresetName(e.target.value)
498
+ }
499
+ },
500
+ },
501
+ presets.map((pr) =>
502
+ React.createElement(
503
+ 'option',
504
+ { key: pr.name, value: pr.name },
505
+ pr.name + (pr.name === defaultPreset ? ` ⭐ (${t('isDefault') || 'основной'})` : '')
506
+ )
507
+ ),
508
+ React.createElement('option', { disabled: true }, '──────────────────'),
509
+ React.createElement(
510
+ 'option',
511
+ { value: '__new__' },
512
+ '+ ' + (t('createNewPreset') || 'Создать новый пресет...')
513
+ )
514
+ ),
515
+ currentPreset.name !== defaultPreset &&
516
+ React.createElement(
517
+ 'button',
518
+ {
519
+ type: 'button',
520
+ className: 'moa-btn-sub',
521
+ onClick: () => setDefaultPreset(currentPreset.name),
522
+ title: t('makeDefaultHint') || 'Использовать этот пресет при вызове /moa без аргументов',
523
+ },
524
+ '⭐ ' + (t('makeDefault') || 'Сделать основным')
525
+ ),
526
+ presets.length > 1 &&
527
+ React.createElement(
528
+ 'button',
529
+ {
530
+ type: 'button',
531
+ className: 'moa-btn-del-preset',
532
+ onClick: handleDeletePreset,
533
+ title: t('deletePreset') || 'Удалить пресет',
534
+ },
535
+ '🗑️ ' + (t('delete') || 'Удалить')
536
+ )
537
+ )
538
+ ),
539
+ isCreating &&
540
+ React.createElement(
541
+ 'div',
542
+ { className: 'moa-new-preset-box' },
543
+ React.createElement('input', {
544
+ type: 'text',
545
+ className: 'moa-new-input',
546
+ placeholder: t('newPresetPlaceholder') || 'Название нового пресета (например: fast-audit)',
547
+ value: newPresetName,
548
+ onChange: (e) => setNewPresetName(e.target.value),
549
+ onKeyDown: (e) => {
550
+ if (e.key === 'Enter') handleCreatePreset()
551
+ if (e.key === 'Escape') setIsCreating(false)
552
+ },
553
+ }),
554
+ React.createElement(
555
+ 'button',
556
+ {
557
+ type: 'button',
558
+ className: 'moa-btn-sub',
559
+ onClick: handleCreatePreset,
560
+ style: { background: 'var(--dsw-alias-label-primary)', color: 'var(--dsw-alias-bg-layer-3)' },
561
+ },
562
+ t('create') || 'Создать'
563
+ ),
564
+ React.createElement(
565
+ 'button',
566
+ {
567
+ type: 'button',
568
+ className: 'moa-btn-sub',
569
+ onClick: () => setIsCreating(false),
570
+ },
571
+ t('cancel') || 'Отмена'
572
+ )
573
+ )
574
+ ),
575
+ /* Aggregator / Judge Box with Temperature Slider */
576
+ React.createElement(
577
+ 'div',
578
+ { className: 'moa-box' },
579
+ React.createElement(
580
+ 'div',
581
+ { className: 'moa-box-title' },
582
+ React.createElement('span', null, t('aggregatorTitle')),
583
+ React.createElement(
584
+ 'div',
585
+ { style: { display: 'flex', alignItems: 'center', gap: 6 } },
586
+ React.createElement('span', { className: 'moa-hint' }, `${t('temperature') || 'Температура'}:`),
587
+ React.createElement('span', { style: { fontWeight: 600, fontSize: 13 } }, aggTemp)
588
+ )
589
+ ),
590
+ React.createElement('div', { className: 'moa-hint' }, t('aggregatorDesc')),
591
+ React.createElement(SearchableModelPicker, {
592
+ provider: currentPreset.aggregator?.provider || '',
593
+ model: currentPreset.aggregator?.model || '',
594
+ onChange: setAggregatorModel,
595
+ availableModels,
596
+ t,
597
+ }),
598
+ React.createElement(
599
+ 'div',
600
+ { className: 'moa-field', style: { marginTop: 4 } },
601
+ React.createElement(
602
+ 'div',
603
+ { className: 'moa-temp-ctrl' },
604
+ React.createElement('input', {
605
+ type: 'range',
606
+ className: 'moa-temp-slider',
607
+ min: '0',
608
+ max: '2',
609
+ step: '0.05',
610
+ value: aggTemp,
611
+ onChange: (e) => setAggregatorTemp(e.target.value),
612
+ }),
613
+ React.createElement('input', {
614
+ type: 'number',
615
+ className: 'moa-temp-input',
616
+ min: '0',
617
+ max: '2',
618
+ step: '0.05',
619
+ value: aggTemp,
620
+ onChange: (e) => setAggregatorTemp(e.target.value),
621
+ })
622
+ ),
623
+ React.createElement('div', { className: 'moa-hint' }, t('aggTempHint') || 'Рекомендуется 0.2 – 0.4 для точной проверки и синтеза без галлюцинаций.')
624
+ )
625
+ ),
626
+ /* Proposers / Advisors Box with Temperature Slider */
627
+ React.createElement(
628
+ 'div',
629
+ { className: 'moa-box' },
630
+ React.createElement(
631
+ 'div',
632
+ { className: 'moa-box-title' },
633
+ React.createElement('span', null, t('proposersTitle')),
634
+ React.createElement(
635
+ 'div',
636
+ { className: 'moa-count-bar' },
637
+ React.createElement('span', { className: 'moa-hint' }, t('parallelCount') + ':'),
638
+ [2, 3, 4, 5].map((cnt) =>
639
+ React.createElement(
640
+ 'button',
641
+ {
642
+ key: cnt,
643
+ type: 'button',
644
+ className: currentRefs.length === cnt ? 'moa-count-btn moa-count-btn-active' : 'moa-count-btn',
645
+ onClick: () => setReferenceCount(cnt),
646
+ },
647
+ cnt
648
+ )
649
+ )
650
+ )
651
+ ),
652
+ React.createElement('div', { className: 'moa-hint' }, t('proposersDesc')),
653
+ React.createElement(
654
+ 'div',
655
+ { className: 'moa-refs-list' },
656
+ currentRefs.map((ref, idx) =>
657
+ React.createElement(
658
+ 'div',
659
+ { key: idx, className: 'moa-ref-row' },
660
+ React.createElement('span', { className: 'moa-ref-badge' }, `${t('candidate')} #${idx + 1}:`),
661
+ React.createElement(
662
+ 'div',
663
+ { className: 'moa-ref-picker' },
664
+ React.createElement(SearchableModelPicker, {
665
+ provider: ref.provider || '',
666
+ model: ref.model || '',
667
+ onChange: (p, m) => setReferenceModel(idx, p, m),
668
+ availableModels,
669
+ t,
670
+ })
671
+ ),
672
+ currentRefs.length > 1 &&
673
+ React.createElement(
674
+ 'button',
675
+ {
676
+ type: 'button',
677
+ className: 'moa-btn-del',
678
+ title: t('remove'),
679
+ onClick: () => removeReference(idx),
680
+ },
681
+ '✕'
682
+ )
683
+ )
684
+ )
685
+ ),
686
+ React.createElement(
687
+ 'button',
688
+ {
689
+ type: 'button',
690
+ className: 'moa-btn-add',
691
+ onClick: addReference,
692
+ },
693
+ '+ ' + t('addCandidate')
694
+ ),
695
+ React.createElement(
696
+ 'div',
697
+ { className: 'moa-field', style: { marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 10 } },
698
+ React.createElement(
699
+ 'div',
700
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
701
+ React.createElement('span', { className: 'moa-label', style: { fontSize: 12 } }, t('refTempLabel') || 'Температура кандидатов (разнообразие решений):'),
702
+ React.createElement('span', { style: { fontWeight: 600, fontSize: 13 } }, refTemp)
703
+ ),
704
+ React.createElement(
705
+ 'div',
706
+ { className: 'moa-temp-ctrl' },
707
+ React.createElement('input', {
708
+ type: 'range',
709
+ className: 'moa-temp-slider',
710
+ min: '0',
711
+ max: '2',
712
+ step: '0.05',
713
+ value: refTemp,
714
+ onChange: (e) => setReferenceTemp(e.target.value),
715
+ }),
716
+ React.createElement('input', {
717
+ type: 'number',
718
+ className: 'moa-temp-input',
719
+ min: '0',
720
+ max: '2',
721
+ step: '0.05',
722
+ value: refTemp,
723
+ onChange: (e) => setReferenceTemp(e.target.value),
724
+ })
725
+ ),
726
+ React.createElement('div', { className: 'moa-hint' }, t('refTempHint') || 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.')
727
+ )
728
+ ),
729
+ /* Save Footer */
730
+ React.createElement(
731
+ 'div',
732
+ { className: 'moa-foot' },
733
+ saveStatus && React.createElement('span', { className: 'moa-status-text' }, saveStatus),
734
+ React.createElement(
735
+ 'button',
736
+ {
737
+ type: 'button',
738
+ className: 'moa-save',
739
+ onClick: handleSave,
740
+ },
741
+ t('save')
742
+ )
743
+ )
744
+ )
745
+ }
746
+
747
+ function useMoASettings(props) {
748
+ const t = props.t || ((k) => k)
749
+ const [status, setStatus] = React.useState('loading')
750
+ const [presets, setPresets] = React.useState([])
751
+ const [defaultPreset, setDefaultPreset] = React.useState('default')
752
+ const [availableModels, setAvailableModels] = React.useState([])
753
+ const [saveStatus, setSaveStatus] = React.useState('')
754
+
755
+ const reload = React.useCallback(() => {
756
+ setStatus('loading')
757
+
758
+ fetch('/dsh-moa/presets', { cache: 'no-store' })
759
+ .then((r) => {
760
+ if (!r.ok) throw new Error(`HTTP ${r.status}`)
761
+ return r.json()
762
+ })
763
+ .then((data) => {
764
+ if (data && data.ok) {
765
+ setPresets(data.presets || [])
766
+ setDefaultPreset(data.defaultPreset || 'default')
767
+ setStatus('ready')
768
+ } else {
769
+ setStatus('unavailable')
770
+ }
771
+ })
772
+ .catch((err) => {
773
+ console.warn('[dsh-moa] Presets fetch failed:', err)
774
+ setStatus('unavailable')
775
+ })
776
+
777
+ fetch('/dsh-moa/models', { cache: 'no-store' })
778
+ .then((r) => r.json())
779
+ .then((data) => {
780
+ if (data && data.ok && Array.isArray(data.models)) {
781
+ setAvailableModels(data.models)
782
+ }
783
+ })
784
+ .catch((err) => {
785
+ console.warn('[dsh-moa] Models fetch failed:', err)
786
+ })
787
+ }, [])
788
+
789
+ React.useEffect(() => {
790
+ reload()
791
+ }, [reload])
792
+
793
+ const handleSave = () => {
794
+ setSaveStatus(t('loading'))
795
+ fetch('/dsh-moa/presets', {
796
+ method: 'POST',
797
+ headers: { 'Content-Type': 'application/json' },
798
+ body: JSON.stringify({ presets, default_preset: defaultPreset }),
799
+ })
800
+ .then((r) => r.json())
801
+ .then((res) => {
802
+ if (res && res.ok) {
803
+ setSaveStatus(t('saved'))
804
+ if (Array.isArray(res.presets)) setPresets(res.presets)
805
+ if (res.defaultPreset) setDefaultPreset(res.defaultPreset)
806
+ setTimeout(() => setSaveStatus(''), 2500)
807
+ } else {
808
+ setSaveStatus('Error saving: ' + (res?.error || 'failed'))
809
+ }
810
+ })
811
+ .catch((err) => {
812
+ setSaveStatus(`Error: ${err.message || err}`)
813
+ })
814
+ }
815
+
816
+ return {
817
+ t,
818
+ status,
819
+ presets,
820
+ setPresets,
821
+ defaultPreset,
822
+ setDefaultPreset,
823
+ availableModels,
824
+ handleSave,
825
+ saveStatus,
826
+ reload,
827
+ }
828
+ }
829
+
830
+ function MoASection(props) {
831
+ ensureStyles()
832
+ const state = useMoASettings(props)
833
+
834
+ return React.createElement(
835
+ 'div',
836
+ { className: 'moa-section' },
837
+ React.createElement('div', { className: 'moa-section-title' }, state.t('title')),
838
+ React.createElement('div', { className: 'moa-section-sub' }, state.t('description')),
839
+ state.status === 'loading'
840
+ ? React.createElement('div', { className: 'moa-hint' }, state.t('loading'))
841
+ : state.status === 'unavailable'
842
+ ? React.createElement(
843
+ 'div',
844
+ { className: 'moa-field' },
845
+ React.createElement('div', { className: 'moa-hint' }, state.t('unavailable')),
846
+ React.createElement(
847
+ 'button',
848
+ {
849
+ type: 'button',
850
+ className: 'moa-count-btn',
851
+ style: { width: 'fit-content', marginTop: 8 },
852
+ onClick: state.reload,
853
+ },
854
+ state.t('retry')
855
+ )
856
+ )
857
+ : React.createElement(MoAEditor, state)
858
+ )
859
+ }
860
+
861
+ function MoACard(props) {
862
+ ensureStyles()
863
+ const state = useMoASettings(props)
864
+ const [open, setOpen] = React.useState(false)
865
+
866
+ return React.createElement(
867
+ 'li',
868
+ { className: 'moa-card' },
869
+ React.createElement(
870
+ 'button',
871
+ {
872
+ type: 'button',
873
+ className: 'moa-head',
874
+ 'aria-expanded': open,
875
+ onClick: () => setOpen(!open),
876
+ },
877
+ React.createElement(
878
+ 'div',
879
+ null,
880
+ React.createElement('div', { className: 'moa-title' }, state.t('title')),
881
+ React.createElement('div', { className: 'moa-sub' }, state.t('description'))
882
+ ),
883
+ React.createElement(Chevron, {
884
+ className: open ? 'moa-chev moa-chev-open' : 'moa-chev',
885
+ })
886
+ ),
887
+ open &&
888
+ React.createElement(
889
+ 'div',
890
+ { className: 'moa-body' },
891
+ state.status === 'loading'
892
+ ? React.createElement('div', { className: 'moa-hint' }, state.t('loading'))
893
+ : state.status === 'unavailable'
894
+ ? React.createElement(
895
+ 'div',
896
+ { className: 'moa-field' },
897
+ React.createElement('div', { className: 'moa-hint' }, state.t('unavailable')),
898
+ React.createElement(
899
+ 'button',
900
+ {
901
+ type: 'button',
902
+ className: 'moa-count-btn',
903
+ style: { width: 'fit-content', marginTop: 8 },
904
+ onClick: state.reload,
905
+ },
906
+ state.t('retry')
907
+ )
908
+ )
909
+ : React.createElement(MoAEditor, state)
910
+ )
911
+ )
912
+ }
913
+
914
+ exports.inject = ['slots', 'locale', 'sessions', 'inputTriggers']
915
+ exports.apply = function apply(ctx) {
916
+ ctx.locale.register(NS, {
917
+ en: {
918
+ title: 'Mixture of Agents (MoA)',
919
+ description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
920
+ slashMoa: 'Run task through Mixture of Agents (/moa <prompt>)',
921
+ defaultPreset: 'Default Preset (used on /moa without arguments)',
922
+ isDefault: 'default',
923
+ presetSelectLabel: 'Active Preset to Configure:',
924
+ createNewPreset: 'Create new preset...',
925
+ makeDefault: 'Make Default',
926
+ makeDefaultHint: 'Use this preset for /moa when no preset argument is given',
927
+ deletePreset: 'Delete preset',
928
+ delete: 'Delete',
929
+ confirmDeletePreset: 'Are you sure you want to delete preset',
930
+ newPresetPlaceholder: 'New preset name (e.g. fast-audit)',
931
+ create: 'Create',
932
+ cancel: 'Cancel',
933
+ aggregatorTitle: 'Judge / Aggregator Model (who reviews and synthesizes)',
934
+ aggregatorDesc: 'Evaluates candidate proposals, corrects hallucinations, and outputs the final unified solution.',
935
+ temperature: 'Temperature',
936
+ aggTempHint: 'Recommended 0.2 - 0.4 for rigorous verification and factual synthesis.',
937
+ proposersTitle: 'Candidate Models (Advisors)',
938
+ proposersDesc: 'Generates independent candidate answers in parallel to provide diverse perspectives.',
939
+ refTempLabel: 'Candidates Temperature (solution diversity):',
940
+ refTempHint: 'Recommended 0.6 - 0.8 for diverse creative hypotheses.',
941
+ parallelCount: 'Parallel workers',
942
+ candidate: 'Candidate',
943
+ addCandidate: 'Add parallel model slot',
944
+ remove: 'Remove',
945
+ save: 'Save Changes',
946
+ saved: 'Saved successfully',
947
+ loading: 'Saving…',
948
+ retry: 'Retry connection',
949
+ unavailable: 'MoA service temporarily unavailable (reconnecting...)',
950
+ searchPlaceholder: 'Search by model name or provider...',
951
+ modelsFound: 'Found',
952
+ of: 'of',
953
+ clear: 'Clear',
954
+ noModelsMatch: 'No models found matching query.',
955
+ useCustom: 'Use custom',
956
+ selectModel: 'Select model...',
957
+ },
958
+ ru: {
959
+ title: 'Mixture of Agents (MoA)',
960
+ description: 'Запуск задач через ансамбль параллельных моделей-советников и модель-судью',
961
+ slashMoa: 'Запустить задачу через архитектуру Mixture of Agents (/moa <prompt>)',
962
+ defaultPreset: 'Основной пресет (при вызове /moa без параметров)',
963
+ isDefault: 'основной',
964
+ presetSelectLabel: 'Текущий пресет для настройки:',
965
+ createNewPreset: 'Создать новый пресет...',
966
+ makeDefault: 'Сделать основным',
967
+ makeDefaultHint: 'Использовать этот пресет для /moa при вызове без параметров',
968
+ deletePreset: 'Удалить пресет',
969
+ delete: 'Удалить',
970
+ confirmDeletePreset: 'Вы уверены, что хотите удалить пресет',
971
+ newPresetPlaceholder: 'Название нового пресета (например: fast-audit)',
972
+ create: 'Создать',
973
+ cancel: 'Отмена',
974
+ aggregatorTitle: 'Ведущая модель / Судья (кто проверяет и объединяет)',
975
+ aggregatorDesc: 'Сопоставляет варианты кандидатов, устраняет ошибки и синтезирует итоговое решение.',
976
+ temperature: 'Температура',
977
+ aggTempHint: 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
978
+ proposersTitle: 'Модели-кандидаты (советники)',
979
+ proposersDesc: 'Параллельно формируют независимые варианты решения одной задачи.',
980
+ refTempLabel: 'Температура кандидатов (разнообразие решений):',
981
+ refTempHint: 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.',
982
+ parallelCount: 'Параллельных моделей',
983
+ candidate: 'Кандидат',
984
+ addCandidate: 'Добавить модель в параллель',
985
+ remove: 'Удалить',
986
+ save: 'Сохранить',
987
+ saved: 'Успешно сохранено',
988
+ loading: 'Сохранение…',
989
+ retry: 'Повторить подключение',
990
+ unavailable: 'Служба MoA временно недоступна (переподключение...)',
991
+ searchPlaceholder: 'Поиск модели по названию или провайдеру...',
992
+ modelsFound: 'Найдено',
993
+ of: 'из',
994
+ clear: 'Сбросить',
995
+ noModelsMatch: 'Модели не найдены.',
996
+ useCustom: 'Использовать кастомное',
997
+ selectModel: 'Выберите модель...',
998
+ },
999
+ })
1000
+
1001
+ const t = ctx.locale.bind(NS)
1002
+
1003
+ if (ctx.slots) {
1004
+ const registerDirectSection = () => {
1005
+ try {
1006
+ ctx.slots.register(
1007
+ {
1008
+ name: 'settings.section',
1009
+ id: NS,
1010
+ order: 35,
1011
+ locale: NS,
1012
+ label: () => t('title'),
1013
+ inject: () => ({ ctx, t }),
1014
+ },
1015
+ MoASection
1016
+ )
1017
+ } catch (err) {
1018
+ console.warn('[dsh-moa] Failed to register settings.section:', err)
1019
+ }
1020
+ }
1021
+
1022
+ const registerPluginCard = () => {
1023
+ try {
1024
+ ctx.slots.register(
1025
+ {
1026
+ name: 'settings.plugin.item',
1027
+ key: NS,
1028
+ order: 35,
1029
+ locale: NS,
1030
+ label: () => t('title'),
1031
+ inject: () => ({ ctx, t }),
1032
+ },
1033
+ MoACard
1034
+ )
1035
+ } catch (err) {
1036
+ console.warn('[dsh-moa] Failed to register settings.plugin.item:', err)
1037
+ }
1038
+ }
1039
+
1040
+ if (typeof ctx.slots.inject === 'function') {
1041
+ ctx.slots.inject('settings.section', registerDirectSection)
1042
+ ctx.slots.inject('settings.plugin.item', registerPluginCard)
1043
+ } else {
1044
+ registerDirectSection()
1045
+ registerPluginCard()
1046
+ }
1047
+ }
1048
+
1049
+ // Register slash command in composer
1050
+ ctx.effect(() => {
1051
+ const triggers = ctx.get ? ctx.get('inputTriggers') : ctx.inputTriggers
1052
+ if (!triggers || typeof triggers.registerSource !== 'function') return () => {}
1053
+
1054
+ const dispose = triggers.registerSource({
1055
+ trigger: '/',
1056
+ name: 'moa',
1057
+ order: 35,
1058
+ description: t('slashMoa'),
1059
+ candidates: (_session, req) => {
1060
+ if (req.position !== 'leading') return Promise.resolve([])
1061
+ const q = req.query.trim().toLowerCase()
1062
+ if (q !== '' && !'moa'.startsWith(q)) return Promise.resolve([])
1063
+ return Promise.resolve([
1064
+ {
1065
+ name: 'moa',
1066
+ description: t('slashMoa'),
1067
+ },
1068
+ ])
1069
+ },
1070
+ onPick: ({ candidate }) => ({
1071
+ text: '/' + candidate.name + ' ',
1072
+ }),
1073
+ matchSpace: (_session, token) => {
1074
+ if (token === '/moa') return { text: '/moa ' }
1075
+ return undefined
1076
+ },
1077
+ matchEnter: (_session, _line) => {
1078
+ return Promise.resolve(undefined)
1079
+ },
1080
+ })
1081
+
1082
+ return () => {
1083
+ if (typeof dispose === 'function') dispose()
1084
+ }
1085
+ }, 'dsh-moa: slash command trigger')
1086
+ }
1087
+
1088
+ return module.exports
1089
+ },
1090
+ })