@goodandready/dsh-model-sync 0.3.10 → 0.3.12

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
@@ -107,6 +107,15 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
107
107
 
108
108
  ---
109
109
 
110
+ ## 🛠️ Enhancements & Fixes in v0.3.11
111
+
112
+ * 🗂️ **Streamlined Settings UI Surface**:
113
+ * Removed the redundant top-level settings sidebar section fallback (`settings.section`), keeping configuration strictly inside the standard DSH plugin settings card (`settings.plugin.item`).
114
+ * 🛡️ **Hardened Cordis Service Access**:
115
+ * Service access for `settings` and `llm` is now performed via `ctx.get(...)` with safe fallback to direct property inspection, preventing silent resolution failures on Cordis proxy implementations.
116
+
117
+ ---
118
+
110
119
  ## 🛠️ Enhancements & Fixes in v0.3.8
111
120
 
112
121
  * ⚙️ **Reactive Settings Card Binding (`settingsScope`)**:
package/README.ru.md CHANGED
@@ -84,6 +84,15 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
84
84
 
85
85
  ---
86
86
 
87
+ ## 🛠️ Улучшения и исправления в v0.3.11
88
+
89
+ * 🗂️ **Чистый интерфейс настроек (DSH Design Contract)**:
90
+ * Удалена избыточная регистрация fallback-раздела верхнего уровня бокового меню (`settings.section`), настройки доступны только через стандартную карточку в «Настройки → Плагины → Настройки плагинов» (`settings.plugin.item`).
91
+ * 🛡️ **Надёжный доступ к сервисам Cordis**:
92
+ * Доступ к сервисам ядра (`settings`, `llm`) переведён на `ctx.get(...)` с безопасным fallback к прямым свойствам контекста для исключения ошибок проксирования.
93
+
94
+ ---
95
+
87
96
  ## 🛠️ Улучшения и исправления в v0.3.8
88
97
 
89
98
  * ⚙️ **Реактивное управление настройками через `settingsScope`**:
package/README.zh.md CHANGED
@@ -73,3 +73,13 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
73
73
  ## 📄 开源协议
74
74
 
75
75
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
76
+
77
+ ## 🛠️ v0.3.11 改进与修复
78
+
79
+ * 🗂️ **规范化设置界面**:
80
+ * 移除侧边栏顶层多余的 `settings.section` fallback,统一使用标准插件设置卡片 `settings.plugin.item`。
81
+ * 🛡️ **增强 Cordis 服务访问稳定性**:
82
+ * 使用 `ctx.get(...)` 安全获取 `settings` 与 `llm` 服务,避免代理对象属性解析异常。
83
+
84
+ ---
85
+
package/lib/client.js CHANGED
@@ -28,7 +28,46 @@ window.__ModuleLoader__.load({
28
28
  '.dms-editor label{display:block;gap:5px;margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px}' +
29
29
  '.dms-editor label input,.dms-editor label textarea{width:100%}' +
30
30
  '.dms-editor input[type="checkbox"]{width:auto;accent-color:var(--dsw-alias-brand-primary)}' +
31
- '@media (max-width:540px){.dms-provider{grid-template-columns:1fr!important}.dms-provider .dms-btn{justify-self:start}.dms-toolbar>div:first-child{align-items:stretch;flex-direction:column}.dms-toolbar>div:first-child select{width:100%}.dms-toolbar,.dms-section,.dms-editor{padding:11px 12px}}'
31
+ '.dms-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px;box-sizing:border-box}' + '.dms-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;line-height:1.4}' + '.dms-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}' + '.dms-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}' + '.dms-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}' + '.dms-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' + '.dms-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' + '.dms-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' + '.dms-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' + '.dms-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;box-sizing:border-box}' + '.dms-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' + '.dms-btn-primary{background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;border-color:transparent!important;font-weight:600}' + '.dms-btn-primary:hover:not(:disabled){opacity:0.88}' + '.dms-btn-danger{color:var(--dsw-alias-state-error-primary)!important;border-color:rgba(239,68,68,0.3)!important}' + '.dms-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important;border-color:rgba(239,68,68,0.5)!important}' + '.dms-alert{padding:10px 14px;border-radius:8px;font-size:13px}' + '.dms-alert-ok{background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary)}' + '.dms-alert-bad{background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary)}' + '.dms-probe-result{padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px}' + '@media (max-width:540px){.dms-provider{grid-template-columns:1fr!important}.dms-provider .dms-btn{justify-self:start}.dms-toolbar>div:first-child{align-items:stretch;flex-direction:column}.dms-toolbar>div:first-child select{width:100%}.dms-toolbar,.dms-section,.dms-section-card,.dms-editor{padding:11px 12px}}'
32
+
33
+ function createErrorBoundary() {
34
+ if (!React || typeof React.Component !== 'function') {
35
+ return function NoopBoundary(props) { return props?.children || null }
36
+ }
37
+ return class ErrorBoundary extends React.Component {
38
+ constructor(props) {
39
+ super(props)
40
+ this.state = { hasError: false, error: null }
41
+ }
42
+ static getDerivedStateFromError(error) {
43
+ return { hasError: true, error }
44
+ }
45
+ componentDidCatch(error, errorInfo) {
46
+ console.error('[dsh-model-sync] React Error:', error, errorInfo)
47
+ }
48
+ render() {
49
+ if (this.state.hasError) {
50
+ return React.createElement(
51
+ 'div',
52
+ {
53
+ className: 'dms-alert dms-alert-bad',
54
+ style: { margin: '12px 0', padding: '14px', borderRadius: '8px', border: '1px solid var(--dsw-alias-state-error-primary)' },
55
+ },
56
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Model Sync UI Error:'),
57
+ React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
58
+ React.createElement('button', {
59
+ type: 'button',
60
+ className: 'dms-btn',
61
+ style: { marginTop: '8px' },
62
+ onClick: () => this.setState({ hasError: false, error: null })
63
+ }, 'Retry')
64
+ )
65
+ }
66
+ return this.props.children
67
+ }
68
+ }
69
+ }
70
+ const ErrorBoundary = createErrorBoundary()
32
71
 
33
72
  function ensureStyles() {
34
73
  if (typeof document === 'undefined' || !document.head) return
@@ -49,7 +88,6 @@ window.__ModuleLoader__.load({
49
88
  }
50
89
  const API = '/dsh-model-sync'
51
90
  const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
52
- function capabilityKeys(models) { return [...new Set((models ?? []).flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort() }
53
91
  function filterModelsByCapabilities(models, selected) { const required = [...new Set((selected ?? []).filter((key) => typeof key === 'string' && key))]; return (models ?? []).filter((model) => required.every((key) => model?.capabilities?.[key] === true)) }
54
92
  const NS = 'dsh-model-sync'
55
93
  function useLocale(ctx) {
@@ -86,7 +124,7 @@ window.__ModuleLoader__.load({
86
124
  const doTry = (provider, modelId) => { setTryState((s)=>({ ...s, [provider+':'+modelId]: { loading: true } })); fetch(`${API}/try`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider, model: modelId }) }).then(async r=>{ const b=await r.json(); if(!r.ok) throw new Error(b?.error?.message||`HTTP ${r.status}`); return b }).then(b=>setTryState((s)=>({ ...s, [provider+':'+modelId]: { ok: b.ok, latency: b.latencyMs, status: b.status, error: b.error } }))).catch(e=>setTryState((s)=>({ ...s, [provider+':'+modelId]: { error: String(e.message||e) } }))) }
87
125
  const load = React.useCallback(() => { setError(''); return fetch(`${API}/status`).then((r) => r.json()).then(setStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
88
126
  const loadCredentials = React.useCallback(() => { setError(''); return fetch(`${API}/credentials`).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(setCredentialStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
89
- React.useEffect(() => { ensureStyles(); load(); loadCredentials(); const timer = setInterval(load, 15000); return () => clearInterval(timer) }, [load, loadCredentials])
127
+ React.useEffect(() => { ensureStyles(); load(); loadCredentials(); const timer = setInterval(() => { if (typeof document !== 'undefined' && document.hidden) return; load() }, 15000); return () => clearInterval(timer) }, [load, loadCredentials])
90
128
  const execute = (targetProvider, dryRunOverride = dryRun) => { if (!dryRunOverride && removeMissing && !window.confirm(t.removeMissing)) return; setBusy(true); setError(''); fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider || undefined, dryRun: dryRunOverride, removeMissing }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setStatus(body); if (dryRunOverride) setPreview({ ...body, provider: targetProvider || '' }); else setPreview(null) }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
91
129
  const checkHealth = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/health`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(targetProvider ? { provider: targetProvider } : {}) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(setStatus).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
92
130
  const checkCredentials = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/credentials/check`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(targetProvider ? { provider: targetProvider } : {}) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setCredentialStatus(body.credentials); setStatus((current) => ({ ...(current ?? {}), lastHealth: body.health })) }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
@@ -114,7 +152,7 @@ window.__ModuleLoader__.load({
114
152
  const tokens = searchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
115
153
  const searchedChoices = tokens.length ? filteredChoicesBase.filter((m) => { const haystack = (m.id + ' ' + (m.name||'') + ' ' + (m.aliases||[]).join(' ') + ' ' + (m.tags||[]).join(' ') + ' ' + (m.description||'')).toLowerCase(); return tokens.every((tok) => haystack.includes(tok)); }) : filteredChoicesBase;
116
154
  const quickFilteredChoices = searchedChoices.filter((m) => { if (quickFilter.context100k && (!m.contextWindow || m.contextWindow < 100000)) return false; if (quickFilter.cheap && (!m.pricing?.inputPerToken || (m.pricing.inputPerToken * 1000000) > 1.0)) return false; return true; });
117
- const filteredChoices = [...quickFilteredChoices].sort((a,b) => { if (sortBy==='price') { const pa=a.pricing?.inputPerToken??Infinity; const pb=b.pricing?.inputPerToken??Infinity; return pa-pb; } if (sortBy==='context') return (b.contextWindow??0)-(a.contextWindow??0); if (sortBy==='date') return (b.firstSeenAt??0)-(a.firstSeenAt??0); return (a.name || a.id || '').localeCompare(b.name || b.id || ''); }); const maxContext = filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0); const minPrice = filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity); const availableCapabilities = [...new Set(choices.flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort(); const visibleChoices = filteredChoices.slice(0, pageSize); const button = (label, onClick) => React.createElement('button', { type: 'button', disabled: busy, onClick, className: 'dms-btn' }, label)
155
+ const filteredChoices = [...quickFilteredChoices].sort((a,b) => { if (sortBy==='price') { const pa=a.pricing?.inputPerToken??Infinity; const pb=b.pricing?.inputPerToken??Infinity; return pa-pb; } if (sortBy==='context') return (b.contextWindow??0)-(a.contextWindow??0); if (sortBy==='date') return (b.firstSeenAt??0)-(a.firstSeenAt??0); return (a.name || a.id || '').localeCompare(b.name || b.id || ''); }); const maxContext = filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0); const minPrice = filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity); const availableCapabilities = [...new Set(choices.flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort(); const visibleChoices = filteredChoices.slice(0, pageSize); const button = (label, onClick, variant) => React.createElement('button', { type: 'button', disabled: busy, onClick, className: 'dms-btn' + (variant ? ' dms-btn-' + variant : '') }, label)
118
156
  const renderNotifications = () => {
119
157
  const entries = status?.notifications ?? []
120
158
  if (entries.length === 0) return React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noNotifications)
@@ -133,7 +171,7 @@ window.__ModuleLoader__.load({
133
171
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12, cursor: 'pointer' } }, React.createElement('input', { type: 'checkbox', checked: !!schedDraft.scheduleEnabled, onChange: (e) => setSchedDraft((d) => ({ ...d, scheduleEnabled: e.target.checked })), disabled: busy }), t.scheduleEnabled),
134
172
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12 } }, React.createElement('span', { style: { minWidth: 140 } }, t.intervalMinutes + ':'), React.createElement('input', { type: 'number', min: 1, max: 1440, value: schedDraft.intervalMinutes, onChange: (e) => setSchedDraft((d) => ({ ...d, intervalMinutes: Math.max(1, Number(e.target.value) || 1) })), disabled: busy, style: { width: 90, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'inherit' } })),
135
173
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12, cursor: 'pointer' } }, React.createElement('input', { type: 'checkbox', checked: !!schedDraft.autoApply, onChange: (e) => setSchedDraft((d) => ({ ...d, autoApply: e.target.checked })), disabled: busy }), t.autoApplySchedule),
136
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 } }, button(t.saveScheduler, saveScheduler), schedMsg ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, schedMsg) : null, schedErr ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, schedErr) : null)
174
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 } }, button(t.saveScheduler, saveScheduler, 'primary'), schedMsg ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, schedMsg) : null, schedErr ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, schedErr) : null)
137
175
  ) : null
138
176
  )
139
177
  }
@@ -148,15 +186,15 @@ window.__ModuleLoader__.load({
148
186
  const details = `+ ${names(item.diff?.added)}; - ${names(item.diff?.removed)}; ⇄ ${(item.diff?.renamed ?? []).map((pair) => `${pair.before?.id} → ${pair.after?.id}`).join(', ')}; ~ ${(item.diff?.changed ?? []).map((pair) => pair.after?.id).join(', ')}`
149
187
  return React.createElement('div', { key: item.provider, style: { margin: '6px 0 6px 12px' } },
150
188
  React.createElement('span', null, `${item.provider}: ${item.status} · +${counts.added ?? 0} ${t.diffAdded}, -${counts.removed ?? 0} ${t.diffRemoved}, ${counts.renamed ?? 0} ${t.diffRenamed}, ${counts.changed ?? 0} ${t.diffChanged}`),
151
- item.hasRollback ? button(t.rollback, () => rollback(entry, item.provider)) : null,
189
+ item.hasRollback ? button(t.rollback, () => rollback(entry, item.provider), 'danger') : null,
152
190
  React.createElement('small', { style: { display: 'block', marginTop: 4 } }, details))
153
191
  })))
154
192
  }
155
193
  return React.createElement('div', { className: 'dms-wrap' },
156
194
  includeIntro ? React.createElement('p', null, t.desc) : null,
157
195
  React.createElement('div', { className: 'dms-toolbar' }, React.createElement('select', { value: provider, onChange: (e) => setProvider(e.target.value) }, React.createElement('option', { value: '' }, t.all), rows.map((row) => React.createElement('option', { key: row.provider, value: row.provider }, row.provider))), React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: dryRun, onChange: (e) => setDryRun(e.target.checked) }), ` ${t.dryRun}`), React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: removeMissing, onChange: (e) => setRemoveMissing(e.target.checked) }), ` ${t.removeMissing}`)),
158
- React.createElement('div', { className: 'dms-actions' }, button(t.refresh, load), button(t.refreshAll, () => execute(undefined)), button(dryRun ? t.run : t.apply, () => execute(provider || undefined)), button(t.health, () => checkHealth(provider || undefined)), button(t.checkCredentials, () => checkCredentials(provider || undefined))),
159
- status?.lastRun ? React.createElement('small', null, t.lastRun + ': ' + new Date(status.lastRun.finishedAt).toLocaleString()) : null, preview ? React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.previewTitle), (preview.results ?? []).filter((row) => row.status === 'ok').map((row) => React.createElement('div', { key: row.provider, style: { marginTop: 8 } }, React.createElement('strong', null, row.provider), React.createElement('div', null, t.previewAdded + ': ' + ((row.diff?.added ?? []).map((model) => model.name ?? model.id).join(', ') || '—')), React.createElement('div', null, t.previewRemoved + ': ' + ((row.diff?.removed ?? []).map((model) => model.name ?? model.id).join(', ') || '—')), React.createElement('div', null, t.previewChanged + ': ' + ((row.diff?.changed ?? []).map((change) => change.after?.name ?? change.after?.id).join(', ') || '—')))), (preview.results ?? []).every((row) => row.status !== 'ok' || !row.diff?.hasChanges) ? React.createElement('small', { style: { display: 'block', marginTop: 8 } }, t.previewNone) : null, React.createElement('div', { className: 'dms-actions', style: { marginTop: 8 } }, button(t.previewApply, () => execute(preview.provider || undefined, false)), button(t.previewDismiss, () => setPreview(null)))) : null, status?.lastReport ? React.createElement('div', { style: { marginTop: 8 } }, React.createElement('strong', null, t.report), React.createElement('small', { style: { display: 'block' } }, (t[status.lastReport.outcome] ?? status.lastReport.outcome) + ' · ' + (status.lastReport.counts?.succeeded ?? 0) + '/' + (status.lastReport.counts?.providers ?? 0) + ' providers · ' + (status.lastReport.counts?.models?.added ?? 0) + ' ' + t.diffAdded)) : null, status?.scheduler ? React.createElement('small', null, `${t.scheduler}: ${status.scheduler.active ? t.schedulerOn : t.schedulerOff}${status.scheduler.nextRunAt ? ` · ${t.nextRun}: ${new Date(status.scheduler.nextRunAt).toLocaleString()}` : ''}`) : null, status?.running || busy ? React.createElement('small', null, t.running) : null, error ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, error) : null, status === null ? React.createElement('small', null, t.loading) : null, rows.length === 0 && status ? React.createElement('small', null, t.noProviders) : null,
196
+ React.createElement('div', { className: 'dms-actions' }, button(t.refresh, load), button(t.refreshAll, () => execute(undefined)), button(dryRun ? t.run : t.apply, () => execute(provider || undefined), dryRun ? undefined : 'primary'), button(t.health, () => checkHealth(provider || undefined)), button(t.checkCredentials, () => checkCredentials(provider || undefined))),
197
+ status?.lastRun ? React.createElement('small', null, t.lastRun + ': ' + new Date(status.lastRun.finishedAt).toLocaleString()) : null, preview ? React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.previewTitle), (preview.results ?? []).filter((row) => row.status === 'ok').map((row) => React.createElement('div', { key: row.provider, style: { marginTop: 8 } }, React.createElement('strong', null, row.provider), React.createElement('div', null, t.previewAdded + ': ' + ((row.diff?.added ?? []).map((model) => model.name ?? model.id).join(', ') || '—')), React.createElement('div', null, t.previewRemoved + ': ' + ((row.diff?.removed ?? []).map((model) => model.name ?? model.id).join(', ') || '—')), React.createElement('div', null, t.previewChanged + ': ' + ((row.diff?.changed ?? []).map((change) => change.after?.name ?? change.after?.id).join(', ') || '—')))), (preview.results ?? []).every((row) => row.status !== 'ok' || !row.diff?.hasChanges) ? React.createElement('small', { style: { display: 'block', marginTop: 8 } }, t.previewNone) : null, React.createElement('div', { className: 'dms-actions', style: { marginTop: 8 } }, button(t.previewApply, () => execute(preview.provider || undefined, false), 'primary'), button(t.previewDismiss, () => setPreview(null)))) : null, status?.lastReport ? React.createElement('div', { style: { marginTop: 8 } }, React.createElement('strong', null, t.report), React.createElement('small', { style: { display: 'block' } }, (t[status.lastReport.outcome] ?? status.lastReport.outcome) + ' · ' + (status.lastReport.counts?.succeeded ?? 0) + '/' + (status.lastReport.counts?.providers ?? 0) + ' providers · ' + (status.lastReport.counts?.models?.added ?? 0) + ' ' + t.diffAdded)) : null, status?.scheduler ? React.createElement('small', null, `${t.scheduler}: ${status.scheduler.active ? t.schedulerOn : t.schedulerOff}${status.scheduler.nextRunAt ? ` · ${t.nextRun}: ${new Date(status.scheduler.nextRunAt).toLocaleString()}` : ''}`) : null, status?.running || busy ? React.createElement('small', null, t.running) : null, error ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, error) : null, status === null ? React.createElement('small', null, t.loading) : null, rows.length === 0 && status ? React.createElement('small', null, t.noProviders) : null,
160
198
  rows.map((row) => {
161
199
  const health = status?.lastHealth?.results?.find((item) => item.provider === row.provider)
162
200
  const healthText = health ? ` · ${health.status === 'ok' ? t.healthOk : health.status === 'dormant' ? t.healthDormant : t.healthError}` : ''
@@ -170,43 +208,50 @@ window.__ModuleLoader__.load({
170
208
  const latencyText = healthEntry?.latencyMs ? ` · ${healthEntry.latencyMs}ms` : ''
171
209
  const isOffline = latestRun?.status !== 'ok' && modelCount > 0
172
210
  const offlineText = isOffline ? ' · ' + t.cacheHoursAgo.replace('%d', Math.max(0, Math.round((Date.now() - (status?.lastRun?.finishedAt ? new Date(status.lastRun.finishedAt).getTime() : Date.now()))/3600000))) : ''
173
- const providerCard = React.createElement('div', { className: 'dms-provider' }, React.createElement('span', null, `${row.provider}: ${row.configured ? t.ok : t.dormant} · ${modelSummary}${lifecycleText}${healthText}${latencyText}${offlineText}`), button(t.selectModels, () => openSelection(row)), button(t.policy, () => openPolicy(row)))
174
- const selectionEditor = selectionProvider === row.provider ? React.createElement('div', { className: 'dms-editor' }, React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`), React.createElement('div', { style: { display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' } }, React.createElement('input', { placeholder: t.searchPlaceholder, value: searchQuery, onChange: (e)=>setSearchQuery(e.target.value), style: { flex: '1 1 160px', padding: '6px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)' } }), React.createElement('select', { value: sortBy, onChange: (e)=>setSortBy(e.target.value), style: { padding: '6px 8px', borderRadius: 6 } }, React.createElement('option', {value:'name'}, t.sortName), React.createElement('option', {value:'price'}, t.sortPrice), React.createElement('option', {value:'context'}, t.sortContext), React.createElement('option', {value:'date'}, t.sortDate))), React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } }, React.createElement('button', { type: 'button', className: 'dms-btn', style: quickFilter.context100k ? { borderColor: 'var(--dsw-alias-brand-primary)', background: 'var(--dsw-alias-bg-layer-2)' } : {}, onClick: () => setQuickFilter((f) => ({ ...f, context100k: !f.context100k })) }, (quickFilter.context100k ? '✓ ' : '') + t.filterContext100k), React.createElement('button', { type: 'button', className: 'dms-btn', style: quickFilter.cheap ? { borderColor: 'var(--dsw-alias-brand-primary)', background: 'var(--dsw-alias-bg-layer-2)' } : {}, onClick: () => setQuickFilter((f) => ({ ...f, cheap: !f.cheap })) }, (quickFilter.cheap ? '✓ ' : '') + t.filterCheap)), React.createElement('div', { style: { margin: '8px 0' } }, choices.length === 0 ? React.createElement('small', null, t.noModels) : React.createElement(React.Fragment, null, availableCapabilities.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('strong', null, t.capabilityFilter), availableCapabilities.map((capability) => React.createElement('label', { key: capability, style: { display: 'inline-block', marginLeft: 8 } }, React.createElement('input', { type: 'checkbox', checked: capabilityFilter.includes(capability), onChange: (e) => setCapabilityFilter((current) => e.target.checked ? [...current, capability] : current.filter((item) => item !== capability)) }), ' ' + (CAPABILITY_LABELS[capability]?.[locale] ?? capability)))) : React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, t.noCapabilities), React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } }, button(t.selectAllFiltered, () => { const ids = new Set(selectionDraft); for (const m of filteredChoices) ids.add(m.id); setSelectionDraft([...ids]); }), button(t.deselectAllFiltered, () => { const filterSet = new Set(filteredChoices.map((m) => m.id)); setSelectionDraft(selectionDraft.filter((id) => !filterSet.has(id))); }), button(t.invertFiltered, () => { const current = new Set(selectionDraft); for (const m of filteredChoices) { if (current.has(m.id)) current.delete(m.id); else current.add(m.id); } setSelectionDraft([...current]); })), React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, filteredChoices.length + '/' + choices.length + ' ' + t.capabilityCount), visibleChoices.map((model) => { const deprecationDays = (() => { const d = model.lifecycle?.deprecationDate; if (!d) return null; const diff = new Date(d).getTime() - Date.now(); return diff > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : 0; })(); return React.createElement('label', { key: model.id, style: { display: 'block', margin: '4px 0', padding: '4px 6px', borderRadius: 6, background: isNewModel(model,row) ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' } }, React.createElement('input', { type: 'checkbox', checked: selectionDraft.includes(model.id), onChange: (e) => setSelectionDraft((current) => e.target.checked ? [...current, model.id] : current.filter((id) => id !== model.id)) }), ` ${model.name ?? model.id} (${model.id})`, isNewModel(model,row) ? React.createElement('span', { style: { marginLeft: 6, fontSize: 11, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-business-primary)', color: 'white' } }, 'NEW') : null, deprecationDays !== null ? React.createElement('span', { style: { marginLeft: 6, fontSize: 10, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-warning-primary)', color: 'white' } }, deprecationDays > 0 ? t.deprecatesIn.replace('%d', deprecationDays) : t.deprecated) : null, (model.contextWindow && maxContext > 0 && model.contextWindow >= maxContext) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-warning-primary)', color: 'white' } }, '★') : null, (model.pricing && minPrice !== Infinity && model.pricing.inputPerToken === minPrice) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-success-primary)', color: 'white' } }, '$') : null, model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null, model.pricing ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, `in $${(model.pricing.inputPerToken*1000000).toFixed(2)}/1M` + (model.pricing.cacheReadPerToken!==undefined ? ` / cache $${(model.pricing.cacheReadPerToken*1000000).toFixed(2)}` : '') + (model.pricing.outputPerToken!==undefined ? ` / out $${(model.pricing.outputPerToken*1000000).toFixed(2)}` : '')) : null, model.description ? React.createElement('small', { title: model.description, style: { display: 'block', marginLeft: 22, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, model.description.slice(0,120)) : null, row.baseURL || row.api ? React.createElement('a', { href: (row.baseURL||'') + '/models/'+encodeURIComponent(model.id), target: '_blank', rel: 'noopener', style: { marginLeft: 8, fontSize: 11 } }, 'docs') : null, React.createElement('button', { type: 'button', onClick: (e)=>{e.preventDefault(); doTry(row.provider, model.id);}, style: { marginLeft: 8, fontSize: 11, padding: '1px 6px', borderRadius: 4 } }, (tryState[row.provider+':'+model.id] && tryState[row.provider+':'+model.id].loading) ? '…' : '▶'), tryState[row.provider+':'+model.id] && !tryState[row.provider+':'+model.id].loading ? React.createElement('small', { style: { marginLeft: 4, color: tryState[row.provider+':'+model.id].ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: 11 } }, tryState[row.provider+':'+model.id].ok ? `${tryState[row.provider+':'+model.id].latency}ms` : (tryState[row.provider+':'+model.id].error||tryState[row.provider+':'+model.id].status)) : null); }), filteredChoices.length > pageSize ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } }, button(t.showMore.replace('%d', Math.min(50, filteredChoices.length - pageSize)), () => setPageSize((s) => s + 50)), React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.showingCount.replace('%d', visibleChoices.length).replace('%d', filteredChoices.length))) : null)), React.createElement('small', null, t.pickerNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.allModels, () => setSelectionDraft(choices.map((model) => model.id))), button(t.saveSelection, saveSelection), button(t.cancel, closeSelection))) : null
175
- const policyEditor = policyProvider === row.provider ? React.createElement('div', { className: 'dms-editor' }, React.createElement('strong', null, `${t.policy}: ${policyProvider}`), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyInclude, React.createElement('textarea', { rows: 3, value: policyDraft.include, onChange: (e) => setPolicyDraft((current) => ({ ...current, include: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyExclude, React.createElement('textarea', { rows: 3, value: policyDraft.exclude, onChange: (e) => setPolicyDraft((current) => ({ ...current, exclude: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyRequire, React.createElement('input', { value: policyDraft.requireCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, requireCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyDeny, React.createElement('input', { value: policyDraft.denyCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, denyCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('small', null, t.policyNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.savePolicy, savePolicy), button(t.cancel, closePolicy))) : null
211
+ const statusBadgeClass = row.configured ? 'dms-badge dms-badge-ok' : 'dms-badge dms-badge-warn'
212
+ const healthBadgeClass = health?.status === 'ok' ? 'dms-badge dms-badge-ok' : health?.status === 'dormant' ? 'dms-badge' : 'dms-badge dms-badge-bad'
213
+ const providerCard = React.createElement('div', { className: 'dms-provider' },
214
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
215
+ React.createElement('strong', { style: { fontSize: 13, marginRight: 2 } }, row.provider),
216
+ React.createElement('span', { className: statusBadgeClass }, row.configured ? t.ok : t.dormant),
217
+ React.createElement('span', { className: 'dms-badge' }, modelSummary),
218
+ health ? React.createElement('span', { className: healthBadgeClass }, `${health.status === 'ok' ? t.healthOk : health.status === 'dormant' ? t.healthDormant : t.healthError}${latencyText}`) : null,
219
+ lifecycleText ? React.createElement('span', { className: 'dms-badge dms-badge-warn' }, lifecycleText.replace(/^ · /, '')) : null,
220
+ offlineText ? React.createElement('span', { className: 'dms-badge dms-badge-warn' }, offlineText.replace(/^ · /, '')) : null
221
+ ),
222
+ button(t.selectModels, () => openSelection(row)),
223
+ button(t.policy, () => openPolicy(row))
224
+ )
225
+ const selectionEditor = selectionProvider === row.provider ? React.createElement('div', { className: 'dms-editor' }, React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`), React.createElement('div', { style: { display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' } }, React.createElement('input', { placeholder: t.searchPlaceholder, value: searchQuery, onChange: (e)=>setSearchQuery(e.target.value), style: { flex: '1 1 160px', padding: '6px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)' } }), React.createElement('select', { value: sortBy, onChange: (e)=>setSortBy(e.target.value), style: { padding: '6px 8px', borderRadius: 6 } }, React.createElement('option', {value:'name'}, t.sortName), React.createElement('option', {value:'price'}, t.sortPrice), React.createElement('option', {value:'context'}, t.sortContext), React.createElement('option', {value:'date'}, t.sortDate))), React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } }, React.createElement('button', { type: 'button', className: 'dms-btn', style: quickFilter.context100k ? { borderColor: 'var(--dsw-alias-brand-primary)', background: 'var(--dsw-alias-bg-layer-2)' } : {}, onClick: () => setQuickFilter((f) => ({ ...f, context100k: !f.context100k })) }, (quickFilter.context100k ? '✓ ' : '') + t.filterContext100k), React.createElement('button', { type: 'button', className: 'dms-btn', style: quickFilter.cheap ? { borderColor: 'var(--dsw-alias-brand-primary)', background: 'var(--dsw-alias-bg-layer-2)' } : {}, onClick: () => setQuickFilter((f) => ({ ...f, cheap: !f.cheap })) }, (quickFilter.cheap ? '✓ ' : '') + t.filterCheap)), React.createElement('div', { style: { margin: '8px 0' } }, choices.length === 0 ? React.createElement('small', null, t.noModels) : React.createElement(React.Fragment, null, availableCapabilities.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('strong', null, t.capabilityFilter), availableCapabilities.map((capability) => React.createElement('label', { key: capability, style: { display: 'inline-block', marginLeft: 8 } }, React.createElement('input', { type: 'checkbox', checked: capabilityFilter.includes(capability), onChange: (e) => setCapabilityFilter((current) => e.target.checked ? [...current, capability] : current.filter((item) => item !== capability)) }), ' ' + (CAPABILITY_LABELS[capability]?.[locale] ?? capability)))) : React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, t.noCapabilities), React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } }, button(t.selectAllFiltered, () => { const ids = new Set(selectionDraft); for (const m of filteredChoices) ids.add(m.id); setSelectionDraft([...ids]); }), button(t.deselectAllFiltered, () => { const filterSet = new Set(filteredChoices.map((m) => m.id)); setSelectionDraft(selectionDraft.filter((id) => !filterSet.has(id))); }), button(t.invertFiltered, () => { const current = new Set(selectionDraft); for (const m of filteredChoices) { if (current.has(m.id)) current.delete(m.id); else current.add(m.id); } setSelectionDraft([...current]); })), React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, filteredChoices.length + '/' + choices.length + ' ' + t.capabilityCount), visibleChoices.map((model) => { const deprecationDays = (() => { const d = model.lifecycle?.deprecationDate; if (!d) return null; const diff = new Date(d).getTime() - Date.now(); return diff > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : 0; })(); return React.createElement('label', { key: model.id, style: { display: 'block', margin: '4px 0', padding: '4px 6px', borderRadius: 6, background: isNewModel(model,row) ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' } }, React.createElement('input', { type: 'checkbox', checked: selectionDraft.includes(model.id), onChange: (e) => setSelectionDraft((current) => e.target.checked ? [...current, model.id] : current.filter((id) => id !== model.id)) }), ` ${model.name ?? model.id} (${model.id})`, isNewModel(model,row) ? React.createElement('span', { style: { marginLeft: 6, fontSize: 11, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-business-primary)', color: 'white' } }, 'NEW') : null, deprecationDays !== null ? React.createElement('span', { style: { marginLeft: 6, fontSize: 10, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-warning-primary)', color: 'white' } }, deprecationDays > 0 ? t.deprecatesIn.replace('%d', deprecationDays) : t.deprecated) : null, (model.contextWindow && maxContext > 0 && model.contextWindow >= maxContext) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-warning-primary)', color: 'white' } }, '★') : null, (model.pricing && minPrice !== Infinity && model.pricing.inputPerToken === minPrice) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-success-primary)', color: 'white' } }, '$') : null, model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null, model.pricing ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, `in $${(model.pricing.inputPerToken*1000000).toFixed(2)}/1M` + (model.pricing.cacheReadPerToken!==undefined ? ` / cache $${(model.pricing.cacheReadPerToken*1000000).toFixed(2)}` : '') + (model.pricing.outputPerToken!==undefined ? ` / out $${(model.pricing.outputPerToken*1000000).toFixed(2)}` : '')) : null, model.description ? React.createElement('small', { title: model.description, style: { display: 'block', marginLeft: 22, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, model.description.slice(0,120)) : null, row.baseURL || row.api ? React.createElement('a', { href: (row.baseURL||'') + '/models/'+encodeURIComponent(model.id), target: '_blank', rel: 'noopener', style: { marginLeft: 8, fontSize: 11 } }, 'docs') : null, React.createElement('button', { type: 'button', onClick: (e)=>{e.preventDefault(); doTry(row.provider, model.id);}, style: { marginLeft: 8, fontSize: 11, padding: '1px 6px', borderRadius: 4 } }, (tryState[row.provider+':'+model.id] && tryState[row.provider+':'+model.id].loading) ? '…' : '▶'), tryState[row.provider+':'+model.id] && !tryState[row.provider+':'+model.id].loading ? React.createElement('small', { style: { marginLeft: 4, color: tryState[row.provider+':'+model.id].ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: 11 } }, tryState[row.provider+':'+model.id].ok ? `${tryState[row.provider+':'+model.id].latency}ms` : (tryState[row.provider+':'+model.id].error||tryState[row.provider+':'+model.id].status)) : null); }), filteredChoices.length > pageSize ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } }, button(t.showMore.replace('%d', Math.min(50, filteredChoices.length - pageSize)), () => setPageSize((s) => s + 50)), React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.showingCount.replace('%d', visibleChoices.length).replace('%d', filteredChoices.length))) : null)), React.createElement('small', null, t.pickerNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.allModels, () => setSelectionDraft(choices.map((model) => model.id))), button(t.saveSelection, saveSelection, 'primary'), button(t.cancel, closeSelection))) : null
226
+ const policyEditor = policyProvider === row.provider ? React.createElement('div', { className: 'dms-editor' }, React.createElement('strong', null, `${t.policy}: ${policyProvider}`), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyInclude, React.createElement('textarea', { rows: 3, value: policyDraft.include, onChange: (e) => setPolicyDraft((current) => ({ ...current, include: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyExclude, React.createElement('textarea', { rows: 3, value: policyDraft.exclude, onChange: (e) => setPolicyDraft((current) => ({ ...current, exclude: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyRequire, React.createElement('input', { value: policyDraft.requireCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, requireCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyDeny, React.createElement('input', { value: policyDraft.denyCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, denyCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('small', null, t.policyNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.savePolicy, savePolicy, 'primary'), button(t.cancel, closePolicy))) : null
176
227
  return React.createElement('div', { key: row.provider, className: 'dms-provider-wrap' }, providerCard, selectionEditor, policyEditor)
177
228
  }),
178
- React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.notifications), renderNotifications()),
179
- React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.credentials), credentialStatus === null ? React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.loading) : (credentialStatus.results ?? []).length === 0 ? React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noProviders) : (credentialStatus.results ?? []).map((item) => React.createElement('div', { key: item.provider, style: { marginTop: 6 } }, React.createElement('span', null, `${item.provider}: ${item.rotation?.configured ? `${item.rotation.keyCount} refs` : 'single ref'}${item.lastRequest ? ` · ${item.lastRequest.status}` : ''}`), (item.refs ?? []).map((ref) => React.createElement('small', { key: ref.ref || ref.label, style: { display: 'block', marginLeft: 12 } }, `${ref.order}. ${ref.label} · ${ref.configured === true ? t.credentialReady : t.missingCredential}${ref.source && ref.source !== 'unknown' ? ` · ${ref.source}` : ''}${ref.lastResolution?.status ? ` · ${ref.lastResolution.status}` : ''}`))))),
180
- React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.schedulerSettings), renderScheduler()),
181
- React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.history), renderHistory()),
229
+ React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.notifications)), renderNotifications()),
230
+ React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.credentials)), credentialStatus === null ? React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.loading) : (credentialStatus.results ?? []).length === 0 ? React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noProviders) : (credentialStatus.results ?? []).map((item) => React.createElement('div', { key: item.provider, style: { marginTop: 6 } }, React.createElement('span', null, `${item.provider}: ${item.rotation?.configured ? `${item.rotation.keyCount} refs` : 'single ref'}${item.lastRequest ? ` · ${item.lastRequest.status}` : ''}`), (item.refs ?? []).map((ref) => React.createElement('small', { key: ref.ref || ref.label, style: { display: 'block', marginLeft: 12 } }, `${ref.order}. ${ref.label} · ${ref.configured === true ? t.credentialReady : t.missingCredential}${ref.source && ref.source !== 'unknown' ? ` · ${ref.source}` : ''}${ref.lastResolution?.status ? ` · ${ref.lastResolution.status}` : ''}`))))),
231
+ React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.schedulerSettings)), renderScheduler()),
232
+ React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.history)), renderHistory()),
182
233
  )
183
234
  }
184
235
  function ModelSyncCard({ ctx }) {
185
236
  const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en; const [open, setOpen] = React.useState(false)
186
237
  const [cardStatus, setCardStatus] = React.useState(null)
187
- React.useEffect(() => { ensureStyles(); const load = () => fetch(`${API}/status`).then(r=>r.json()).then(setCardStatus).catch(()=>{}); load(); const id=setInterval(load,15000); return ()=>clearInterval(id)}, [])
238
+ React.useEffect(() => { ensureStyles(); const load = () => { if (typeof document !== 'undefined' && document.hidden) return; fetch(`${API}/status`).then(r=>r.json()).then(setCardStatus).catch(()=>{}) }; load(); if (open) return; const id = setInterval(load, 15000); return () => clearInterval(id) }, [open])
188
239
  const cardBadge = (cardStatus?.lastRun?.results?.reduce((s,r)=>s+(r.diff?.added?.length||0),0) || 0)
189
240
  return React.createElement('li', { className: 'dms-card' + (open ? ' dms-card-open' : '') },
190
241
  React.createElement('button', { type: 'button', className: 'dms-card-header', 'aria-expanded': open, 'aria-label': `${t[open ? 'hideSettings' : 'showSettings']}: ${t.title}`, onClick: () => setOpen((value) => !value) },
191
242
  React.createElement('span', { className: 'dms-card-head-text' }, React.createElement('span', { className: 'dms-card-name' }, t.title), React.createElement('span', { className: 'dms-card-description' }, t.desc)),
192
243
  cardBadge ? React.createElement('span', { style: { background: 'var(--dsw-alias-state-business-primary)', color: 'white', padding: '2px 6px', borderRadius: 6, fontSize: 11, marginLeft: 8 } }, `+${cardBadge}`) : null,
193
244
  React.createElement('svg', { width: 14, height: 14, className: 'dms-card-chevron' + (open ? ' dms-card-chevron-open' : ''), viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': 'true' }, React.createElement('path', { d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5', fill: 'currentColor' }))),
194
- open ? React.createElement('div', { className: 'dms-card-body' }, React.createElement(ModelSyncSection, { ctx, includeIntro: false })) : null)
245
+ open ? React.createElement('div', { className: 'dms-card-body' }, React.createElement(ErrorBoundary, null, React.createElement(ModelSyncSection, { ctx, includeIntro: false }))) : null)
195
246
  }
196
247
  function apply(ctx) {
197
248
  ctx.effect(() => {
198
249
  try { ctx.locale?.register?.(NS, { en, ru }) } catch {}
199
250
  return () => {}
200
251
  }, 'dsh-model-sync: dictionaries')
201
- const registerPluginCard = () => {
202
- try {
203
- ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ModelSyncCard, { ...props, ctx })))
204
- return true
205
- } catch { return false }
206
- }
207
- if (!registerPluginCard()) {
208
- ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-model-sync', order: 25, label: () => 'Model Sync' }, (props) => React.createElement(ModelSyncSection, { ...props, ctx })))
209
- }
252
+ try {
253
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ErrorBoundary, null, React.createElement(ModelSyncCard, { ...props, ctx }))))
254
+ } catch {}
210
255
  }
211
256
  module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], filterModelsByCapabilities }; return module.exports
212
257
  },
package/lib/index.js CHANGED
@@ -137,7 +137,7 @@ export function apply(ctx, config) {
137
137
  try { settings = ctx.get('settings') } catch {}
138
138
  if (!bindSettings(settings, ctx)) {
139
139
  ctx.inject(['settings'], (sctx) => {
140
- bindSettings(sctx.settings, sctx)
140
+ bindSettings((typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? sctx.settings, sctx)
141
141
  })
142
142
  }
143
143
 
@@ -3,13 +3,22 @@ import { listApiKeyProviders } from './inventory.js'
3
3
  export function createProviderService(ctx) {
4
4
  const readSettings = () => {
5
5
  try {
6
- return ctx.get('settings')
6
+ return (typeof ctx.get === 'function' ? (ctx.get('settings') ?? ctx.settings) : ctx.settings)
7
7
  } catch {
8
- return undefined
8
+ return ctx.settings
9
9
  }
10
10
  }
11
11
 
12
- const listProviders = () => listApiKeyProviders(ctx.llm, readSettings())
12
+ const readLlm = () => {
13
+ try {
14
+ const llm = typeof ctx.get === 'function' ? ctx.get('llm') : null
15
+ return (llm && typeof llm === 'object' && (llm.listConfigurableProviders || llm.listProviders)) ? llm : ctx.llm
16
+ } catch {
17
+ return ctx.llm
18
+ }
19
+ }
20
+
21
+ const listProviders = () => listApiKeyProviders(readLlm(), readSettings())
13
22
 
14
23
  return Object.freeze({
15
24
  listProviders,