dsh-model-params 0.1.5 → 0.1.7

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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/lib/client.js +242 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  ## English
4
4
 
5
- **Current release: 0.1.5** — models.dev parameter assistant inside the official DeepSeek Harness Models settings page. This release adds a documented install path: `dsh plugin --profile web add dsh-model-params`.
5
+ **Current release: 0.1.6** — Portable settings transport: the plugin now resolves the official pi-ai settings scope on both the `settingsScope` (≤ 0.1.5) and `configForms` (≥ 0.1.7-rc.1) hosts, so the provider card keeps working across the rename.
6
6
 
7
7
  For every configured `llm-pi-ai` provider card (custom OpenAI-compatible gateways such as the ones you add under Settings → Models), a `models.dev 参数` control fetches the official models.dev records for the provider's configured model ids and proposes the metadata pi-ai needs: context window, max output tokens, and reasoning-effort levels. One click writes the merged `models` array back to that provider profile through the official revision-aware Settings API.
8
8
 
9
+ On hosts that declare the root-scope `settings.section` list seat, that same card is also registered as a first-class settings page — identity `YOTK · Model Params`, id `yotk-model-params`, order `63` — additive beside the Models-page extension and never an activation gate. The page needs nothing selected elsewhere: it reads the `llm-pi-ai` providers you have configured through the same settings scope, addresses the first one (with a picker whenever several exist), and states plainly when the namespace is loading, unserved, or has no provider yet.
10
+
9
11
  Safety boundaries:
10
12
 
11
13
  - The control never auto-writes. You open the panel, review the per-model diff, then click **应用并写入该 provider** — it writes only that provider's `models` array in the `llm-pi-ai` namespace.
@@ -20,6 +22,8 @@ models.dev model ids may collide across catalog providers; the matcher prefers t
20
22
 
21
23
  面向 DeepSeek Harness Web 官方 **设置 → 模型** 页的 models.dev 参数助手。每个已配置的 `llm-pi-ai` provider 卡片内提供 `models.dev 参数` 入口:按该 provider 的模型 id 在 models.dev 官方目录中匹配,预览上下文窗口 / max 输出 / 推理档位,确认后一次性写回该 provider 的 `models` 配置(官方 revision-aware Settings API,仅写 `llm-pi-ai` 命名空间下当前 provider 的数组)。
22
24
 
25
+ 在声明根级 `settings.section` 列表座的宿主上,同一张卡片还会注册为设置页的一级入口 —— 标识 `YOTK · Model Params`、id `yotk-model-params`、order `63` —— 与「设置 → 模型」页的扩展并存,且从不成为激活门槛。该页不依赖别处选中的 provider:它通过同一设置作用域读取已配置的 `llm-pi-ai` provider,默认处理第一个(配置多个时可在页内切换),并在命名空间仍在读取、宿主未提供或尚未配置 provider 时明确说明。
26
+
23
27
  - 默认仅补缺失参数;勾选「覆盖已有值」才替换差异值。
24
28
  - 窗口顶部可启用一个所有 provider 共用的 models.dev 请求代理并编辑地址;它只影响下一次参数查询,不改变任何 LLM provider 的 `baseURL`。
25
29
  - Host 只负责拉取并缓存 models.dev 目录(6 小时,支持公共代理或 `HTTPS_PROXY` 回退),不接触任何 LLM 配置与凭据。
@@ -31,7 +35,7 @@ models.dev model ids may collide across catalog providers; the matcher prefers t
31
35
  dsh plugin --profile web add dsh-model-params
32
36
  ```
33
37
 
34
- Restart the existing DSH Web process afterwards: the Host scans the browser plugin roster at startup, so the provider-card control appears only after that restart. Then open **Settings → Models** and look for `models.dev 参数` on a configured provider card.
38
+ Restart the existing DSH Web process afterwards: the Host scans the browser plugin roster at startup, so the provider-card control appears only after that restart. Then open **Settings → Models** and look for `models.dev 参数` on a configured provider card. Where the host also exposes the settings page seat, the same control is one click deep in **Settings** as **YOTK · Model Params**.
35
39
 
36
40
  Local development, from this package directory:
37
41
 
package/lib/client.js CHANGED
@@ -3,12 +3,18 @@
3
3
  // Renders a compact "models.dev 参数" affordance inside every official
4
4
  // llm-pi-ai provider card on the Models settings page
5
5
  // (settings.models.provider-card, keyed by the pi-ai settings namespace).
6
- // It reads the current provider profile from the official llm-pi-ai Settings
7
- // scope, asks the Host for the matching models.dev records, previews the
8
- // proposed context / max-output / reasoning-effort values per model, and —
9
- // only on an explicit "应用" click — writes the merged models array back
10
- // through the revision-aware official Settings API. Default policy fills
11
- // missing parameters only; "覆盖已有值" also replaces differing ones.
6
+ // The same card is ALSO registered on the root-scope `settings.section` list
7
+ // seat (id `yotk-model-params`), which makes it a first-class settings page of
8
+ // its own on hosts that declare that seat. That seat's owner share is
9
+ // `{ close }` alone, so the page is handed no provider: it resolves one of the
10
+ // routes the llm-pi-ai namespace itself declares, from the same scope and
11
+ // never through a second read path.
12
+ // Either way it reads the current provider profile from the official llm-pi-ai
13
+ // Settings scope, asks the Host for the matching models.dev records, previews the
14
+ // proposed context / max-output / reasoning-effort values per model, and — only
15
+ // on an explicit "应用" click — writes the merged models array back through the
16
+ // revision-aware official Settings API. Default policy fills missing parameters
17
+ // only; "覆盖已有值" also replaces differing ones.
12
18
  // The plugin never auto-writes, never touches other providers/namespaces,
13
19
  // and never sends credentials anywhere.
14
20
 
@@ -22,6 +28,30 @@ window.__ModuleLoader__.load({
22
28
 
23
29
  const PI_NS = 'llm-pi-ai'
24
30
  const API = '/api/model-params/lookup'
31
+
32
+ // ── settings-scope portability (DSH 0.1.5 ↔ 0.1.7-rc.1) ──────────────────
33
+ //
34
+ // This plugin owns no settings namespace: it extends the OFFICIAL pi-ai
35
+ // Models page and writes that provider's own `llm-pi-ai` section. The two
36
+ // supported hosts expose the same scope CONTRACT under different names:
37
+ //
38
+ // ≤ 0.1.5 settingsScope.bind({ namespace }) → SettingsScope<T>
39
+ // ≥ 0.1.7 configForms.get(entryId) → ConfigForm<T>
40
+ //
41
+ // Both return getSnapshot()/subscribe()/set()/unset()/mutate() over the same
42
+ // { status, value, base, user, revision, writable, mode } snapshot, so the
43
+ // resolved object is used unchanged by the provider card. On 0.1.7-rc.1 a
44
+ // form's namespace IS the profile entry id, and the pi-ai LLM entry is
45
+ // mounted as `llm-pi-ai`, so the same key resolves on either host.
46
+ // `ctx.get` (never a direct property read) keeps the lookup safe when only
47
+ // one service is mounted: an uninjected property read throws.
48
+ function resolveSettingsScopeFrom(ctx, namespace) {
49
+ const binder = ctx.get('settingsScope')
50
+ if (binder && typeof binder.bind === 'function') return binder.bind({ namespace })
51
+ const forms = ctx.get('configForms')
52
+ if (forms && typeof forms.get === 'function') return forms.get(namespace)
53
+ return undefined
54
+ }
25
55
  const FETCH_TIMEOUT_MS = 20000
26
56
  const CSS_ID = 'dsh-model-params/style'
27
57
  const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
@@ -40,6 +70,8 @@ window.__ModuleLoader__.load({
40
70
  '.dpmProxy{display:flex;flex-direction:column;gap:6px;padding:7px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}',
41
71
  '.dpmProxy label{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);cursor:pointer}',
42
72
  '.dpmProxy input[type=text]{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:11px}',
73
+ '.dpmRoute{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary)}',
74
+ '.dpmRoute select{font:inherit;font-size:11px;max-width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
43
75
  '.dpmHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}',
44
76
  '.dpmErr{color:var(--dsw-alias-state-error-primary);margin:0;font-size:11px;word-break:break-all}',
45
77
  '.dpmList{display:flex;flex-direction:column;gap:6px}',
@@ -71,6 +103,48 @@ window.__ModuleLoader__.load({
71
103
  const clean = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined
72
104
  const intersectLevels = (values) => (values || []).filter((value) => LEVELS.includes(value))
73
105
 
106
+ // ── the section page's own provider (the `settings.section` seat) ─────────
107
+ //
108
+ // The root-scope section seat's owner share is `{ close }` ALONE
109
+ // (SettingsSectionOwnerProps), so that page is handed NO provider while its
110
+ // Models-page sibling receives one per card. It therefore resolves its
111
+ // provider from the ONE settings transport this package already reads: the
112
+ // `llm-pi-ai` section itself, whose `providers` dict IS keyed by provider
113
+ // route (PiAiProviderProfile). Every route this page addresses is one the
114
+ // namespace really declares — never a synthesized id, never a second read or
115
+ // write path — and the profile supplies the real display name when it has one.
116
+ function piAiValue(snapshot) {
117
+ return snapshot && snapshot.status === 'ready' && snapshot.value && typeof snapshot.value === 'object' ? snapshot.value : undefined
118
+ }
119
+ function piAiRoutes(snapshot) {
120
+ const value = piAiValue(snapshot)
121
+ const providers = value && value.providers && typeof value.providers === 'object' && !Array.isArray(value.providers) ? value.providers : undefined
122
+ return providers ? Object.keys(providers).filter((route) => typeof route === 'string' && route.length > 0) : []
123
+ }
124
+ function piAiProfile(snapshot, route) {
125
+ const value = piAiValue(snapshot)
126
+ const profile = value && value.providers && typeof value.providers === 'object' ? value.providers[route] : undefined
127
+ return profile && typeof profile === 'object' ? profile : undefined
128
+ }
129
+ function piAiModels(snapshot, route) {
130
+ const profile = piAiProfile(snapshot, route)
131
+ return profile && Array.isArray(profile.models) ? profile.models : []
132
+ }
133
+ function piAiDisplayName(snapshot, route) {
134
+ const profile = piAiProfile(snapshot, route)
135
+ return (profile && clean(profile.displayName)) || route
136
+ }
137
+ // What the page says when it has no card to draw. A namespace still loading, a
138
+ // namespace this host does not serve, and a namespace with no provider
139
+ // configured are three different facts, and none may look like a blank page.
140
+ function sectionPageHint(snapshot, scope) {
141
+ if (!scope) return '设置传输不可用:本机未挂载 settingsScope / configForms,无法读写 llm-pi-ai 配置。'
142
+ const status = snapshot && snapshot.status
143
+ if (status === 'loading') return '正在读取 llm-pi-ai 设置…'
144
+ if (status !== 'ready') return '当前 Host 未提供 llm-pi-ai 设置,无法读写模型参数。'
145
+ return 'llm-pi-ai 命名空间下还没有已配置的 provider;请先在 设置 → 模型 中添加一个自定义 provider。'
146
+ }
147
+
74
148
  function paramLine(label, value) {
75
149
  if (value === undefined || value === null || value === '') return null
76
150
  return e('span', { key: label }, label + ': ' + value)
@@ -79,11 +153,23 @@ window.__ModuleLoader__.load({
79
153
  function ModelParamsExtension(props) {
80
154
  const row = props.provider
81
155
  const entry = row && row.entry && typeof row.entry === 'object' ? row.entry : row
82
- const route = entry && (entry.provider || row && row.provider)
83
- const settingsNs = entry && entry.settingsNs
156
+ // The provider the Models page hands this card through the seat's own owner
157
+ // props. The section seat hands NONE (`{ close }` is its whole owner share),
158
+ // so there this card IS the page and picks its own provider from the routes
159
+ // the namespace declares — see the resolvers above.
160
+ const seatRoute = entry && (entry.provider || row && row.provider)
84
161
  const api = props.api
85
162
  const scope = props.scope
86
- const [open, setOpen] = React.useState(false)
163
+ // The panel starts COLLAPSED on the Models-page provider card, which renders
164
+ // one control per provider beside the others; it starts EXPANDED wherever the
165
+ // card is the whole page (`settings.section` passes `defaultOpen: true`), so
166
+ // the single-card seat opens with its content already visible. The trigger
167
+ // button below keeps folding it either way.
168
+ const [open, setOpen] = React.useState(props.defaultOpen === true)
169
+ // Which real route this card addresses on the page seat. Ignored while a
170
+ // provider arrives from the host, because a provider CARD is bound to the
171
+ // row it extends and must never follow a module-level selection.
172
+ const [picked, setPicked] = React.useState('')
87
173
  const [busy, setBusy] = React.useState(false)
88
174
  const [failed, setFailed] = React.useState('')
89
175
  const [saved, setSaved] = React.useState('')
@@ -94,7 +180,22 @@ window.__ModuleLoader__.load({
94
180
  proxyState.listeners.add(rerender)
95
181
  return () => proxyState.listeners.delete(rerender)
96
182
  }, [])
97
- const [result, setResult] = React.useState(null) // { matched: [], unmatched: [] }
183
+ const [, refreshScope] = React.useState(0)
184
+ // The page's only reason to re-render beyond its own clicks: the namespace
185
+ // may still be loading at first paint, and a provider added in 设置 → 模型
186
+ // must appear here without a reload. ONE subscription on the same transport,
187
+ // disposed with the card.
188
+ React.useEffect(() => {
189
+ if (!scope || typeof scope.subscribe !== 'function') return undefined
190
+ const unsubscribe = scope.subscribe(() => refreshScope((value) => value + 1))
191
+ return () => { if (typeof unsubscribe === 'function') unsubscribe() }
192
+ }, [scope])
193
+ // A lookup answer is stamped with the route it was requested for: this card
194
+ // can change its addressed provider (the page seat's chooser, or the
195
+ // namespace dropping the picked route) while a request is in flight, and the
196
+ // previous provider's matches must never be previewed beside this one's
197
+ // models.
198
+ const [rawResult, setRawResult] = React.useState(null) // { route, matched: [], unmatched: [] }
98
199
  const ref = React.useRef(null)
99
200
 
100
201
  React.useEffect(() => {
@@ -109,29 +210,47 @@ window.__ModuleLoader__.load({
109
210
  }
110
211
  }, [open])
111
212
 
112
- if (!route || settingsNs !== PI_NS) return null
113
-
114
213
  const snap = scope && typeof scope.getSnapshot === 'function' ? scope.getSnapshot() : { status: 'unavailable' }
115
- const value = snap.status === 'ready' && snap.value && typeof snap.value === 'object' ? snap.value : {}
116
- const profile = value.providers && value.providers[route] && typeof value.providers[route] === 'object' ? value.providers[route] : null
117
- const models = Array.isArray(profile && profile.models) ? profile.models : []
214
+ // Two seats, ONE card. Where the host supplies a provider, this is the
215
+ // per-provider control it always was. Where none arrives (the
216
+ // `settings.section` page), this card is the page: it addresses the first
217
+ // route the llm-pi-ai namespace declares, or the one the user picked from the
218
+ // namespace's real route list.
219
+ const sectionSeat = !seatRoute
220
+ const routes = sectionSeat ? piAiRoutes(snap) : []
221
+ const route = sectionSeat ? (routes.includes(picked) ? picked : routes[0]) : seatRoute
222
+ const settingsNs = sectionSeat ? PI_NS : entry && entry.settingsNs
223
+ if (!route || settingsNs !== PI_NS) {
224
+ // A provider CARD whose namespace is not ours renders nothing (the seat
225
+ // dispatches by settingsNs, so this is defensive). The page seat owns the
226
+ // whole surface instead, so it always explains what it cannot draw rather
227
+ // than showing an empty page.
228
+ if (!sectionSeat) return null
229
+ return e('div', { className: 'dpmWrap' }, e('p', { className: 'dpmHint' }, sectionPageHint(snap, scope)))
230
+ }
231
+
232
+ const models = piAiModels(snap, route)
233
+ // Only this route's answer is this route's preview.
234
+ const result = rawResult && rawResult.route === route ? rawResult : null
118
235
  const writable = snap.status === 'ready' && snap.writable === true && !!api && !!api.settings
119
236
 
120
- async function load() {
121
- if (!models.length || busy) return
237
+ async function load(routeOverride) {
238
+ const targetRoute = routeOverride || route
239
+ const targetModels = routeOverride ? piAiModels(snap, routeOverride) : models
240
+ if (!targetModels.length || busy) return
122
241
  setBusy(true)
123
242
  setFailed('')
124
243
  setSaved('')
125
- setResult(null)
244
+ setRawResult(null)
126
245
  const controller = new AbortController()
127
246
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
128
247
  try {
129
- const ids = models.map((model) => model.id).filter(Boolean)
248
+ const ids = targetModels.map((model) => model.id).filter(Boolean)
130
249
  const query = '?ids=' + encodeURIComponent(ids.join(',')) + (proxyState.enabled ? '&proxy=' + encodeURIComponent((proxyState.url.trim() || PROXY_DEFAULT)) : '')
131
250
  const response = await fetch(API + query, { cache: 'no-store', signal: controller.signal })
132
251
  const data = await response.json()
133
252
  if (!data || data.ok !== true) throw new Error((data && data.error) || 'lookup failed')
134
- setResult({ matched: data.matched || [], unmatched: data.unmatched || [] })
253
+ setRawResult({ route: targetRoute, matched: data.matched || [], unmatched: data.unmatched || [] })
135
254
  } catch (error) {
136
255
  setFailed(String((error && error.name === 'AbortError') ? '请求超时' : (error && error.message) || error))
137
256
  } finally {
@@ -197,7 +316,7 @@ window.__ModuleLoader__.load({
197
316
  }
198
317
  setSaved('已写入 provider "' + route + '" 的模型参数。')
199
318
  setOpen(false)
200
- setResult(null)
319
+ setRawResult(null)
201
320
  } catch (error) {
202
321
  setFailed(String((error && error.message) || error))
203
322
  } finally {
@@ -205,7 +324,10 @@ window.__ModuleLoader__.load({
205
324
  }
206
325
  }
207
326
 
208
- const displayName = entry && entry.displayName ? entry.displayName : route
327
+ // The Models page joins the official directory, so its `displayName` is the
328
+ // richest one. The page seat has only the namespace's own profile, which may
329
+ // declare one; otherwise the route itself is the honest label.
330
+ const displayName = (entry && entry.displayName) || piAiDisplayName(snap, route)
209
331
  const hasModels = models.length > 0
210
332
  const summary = result && result.matched ? result.matched.length + '/' + models.length + ' 匹配' : ''
211
333
 
@@ -214,6 +336,24 @@ window.__ModuleLoader__.load({
214
336
  busy ? '查询中…' : ('models.dev 参数' + (summary ? ' · ' + summary : ''))),
215
337
  open && e('div', { className: 'dpmPanel', role: 'dialog', 'aria-label': displayName + ' models.dev 参数' },
216
338
  e('h4', null, displayName + ' · models.dev 官方参数'),
339
+ // A namespace may declare SEVERAL llm-pi-ai providers. This page holds ONE
340
+ // card, so it offers the namespace's real routes instead of silently
341
+ // addressing whichever one is declared first; a single-route namespace gets
342
+ // no chooser at all, because there is nothing to choose.
343
+ routes.length > 1 && e('label', { className: 'dpmRoute' }, 'provider',
344
+ e('select', {
345
+ value: route,
346
+ 'aria-label': '选择要处理的 llm-pi-ai provider',
347
+ onChange: (event) => {
348
+ const next = event.target.value
349
+ setPicked(next)
350
+ setRawResult(null)
351
+ setSaved('')
352
+ setFailed('')
353
+ void load(next)
354
+ },
355
+ }, routes.map((item) => e('option', { key: item, value: item }, piAiDisplayName(snap, item)))),
356
+ ),
217
357
  e('div', { className: 'dpmProxy' },
218
358
  e('label', null,
219
359
  e('input', { type: 'checkbox', checked: proxyState.enabled, onChange: (ev) => { proxyState.enabled = ev.target.checked; notifyProxy() } }),
@@ -274,15 +414,89 @@ window.__ModuleLoader__.load({
274
414
  const api = remote && remote.settings
275
415
  ? { settings: { mutate: (payload) => remote.settings.mutate(payload.ns, payload.ops, payload.expectedRevision).then((result) => ({ result })) } }
276
416
  : undefined
277
- const scope = ctx.settingsScope.bind({ namespace: PI_NS })
278
- return ctx.slots.inject('settings.models.provider-card', () => ctx.slots.register(
279
- { name: 'settings.models.provider-card', key: PI_NS },
280
- (props) => e(ModelParamsExtension, { ...props, api, scope }),
281
- ))
417
+ // Version-portable scope on the official pi-ai namespace. Both transport
418
+ // names are awaited WITHOUT gating activation, and whichever this host
419
+ // mounts resolves; see resolveSettingsScopeFrom.
420
+ let scope
421
+ let cardRegistered = false
422
+ // Slot-registration disposers, released from apply's own disposer on stop /
423
+ // update / HMR so no seat entry outlives the plugin.
424
+ const disposeSlots = []
425
+ // The provider card MUST register from inside a context that has the
426
+ // settings transport, i.e. from the `ctx.inject([...], cb)` callback and on
427
+ // the CHILD context it hands us. Registering
428
+ // `settings.models.provider-card` from the bare apply context puts the
429
+ // entry where the Models page's ledger never sees it, so the extension
430
+ // never renders even though `register` returned normally. (dshmarket, an
431
+ // out-of-repo bundle that does render its card, uses this nested shape.)
432
+ const registerCard = (sctx) => {
433
+ if (cardRegistered) return
434
+ const resolved = resolveSettingsScopeFrom(sctx, PI_NS)
435
+ if (resolved === undefined) return
436
+ scope = resolved
437
+ cardRegistered = true
438
+ disposeSlots.push(sctx.slots.inject('settings.models.provider-card', () => sctx.slots.register(
439
+ { name: 'settings.models.provider-card', key: PI_NS },
440
+ (props) => e(ModelParamsExtension, { ...props, api, scope }),
441
+ )))
442
+ }
443
+ // The first wait fires only when settingsScope exists; the second covers a
444
+ // host that renamed the transport, and its guard keeps the card from
445
+ // registering twice when both names exist.
446
+ ctx.inject(['settingsScope'], registerCard)
447
+ ctx.inject(['configForms'], (sctx) => { if (!cardRegistered) registerCard(sctx) })
448
+
449
+ // ── additive seat: the `settings.section` page ─────────────────────────
450
+ //
451
+ // 0.1.7-rc.2 also declares the root-scope LIST slot `settings.section`
452
+ // ("one settings page per list entry"), which puts this card one click deep
453
+ // inside 设置 as a first-class page of its own. It is purely ADDITIVE: the
454
+ // provider-card extension above stays the Models-page affordance, and both
455
+ // render the SAME `ModelParamsExtension` through the SAME resolved transport
456
+ // — no second settings UI and no second read/write path. This package owns
457
+ // no bundle row, so there is no `plugins.row.config` occupant to mirror:
458
+ // its card is the official Models-page extension.
459
+ //
460
+ // That seat's owner share is `{ close }` alone, so this page is handed NO
461
+ // provider. It addresses one of the routes the llm-pi-ai namespace itself
462
+ // declares — read from the SAME resolved scope, so no second read path and no
463
+ // synthesized route — and explains a loading, unavailable or provider-less
464
+ // namespace instead of rendering a blank page. The page passes no `provider`
465
+ // prop, which is exactly how the card knows it is the whole page.
466
+ //
467
+ // The wait on `slots` is NON-GATING (the seat is host-version dependent: a
468
+ // host that does not declare it simply never fires the inner `slots.inject`,
469
+ // so this registration can never gate the plugin), the callback RETURNS the
470
+ // registration disposer, and that disposer is pushed onto disposeSlots so
471
+ // the entry is owned by the registering fiber and released with the rest on
472
+ // stop / update / HMR.
473
+ const registerSettingsSection = (sctx) => {
474
+ disposeSlots.push(sctx.slots.inject('settings.section', () => sctx.slots.register({
475
+ name: 'settings.section',
476
+ id: 'yotk-model-params',
477
+ order: 63,
478
+ label: () => 'YOTK · Model Params',
479
+ }, function ModelParamsSettingsSection() {
480
+ // The seat owner shares `close` and nothing else; this card needs
481
+ // neither it nor the host-owned optional `form` prop, because values
482
+ // keep flowing through the one resolved transport — so the page is the
483
+ // same card component the provider-card seat renders, with the panel
484
+ // expanded: one card alone on the page. The trigger button still folds
485
+ // it back up.
486
+ return e(ModelParamsExtension, { api, scope, defaultOpen: true })
487
+ })))
488
+ }
489
+ ctx.inject(['slots'], registerSettingsSection)
490
+
491
+ return () => { for (const dispose of disposeSlots) { try { dispose() } catch (_) {} } }
282
492
  }
283
493
 
284
494
  exports.name = 'dsh-model-params'
285
- exports.inject = ['slots', 'settingsScope', 'remote', 'remote.settings']
495
+ // 'settingsScope' and 'configForms' are alternatives, not both-required:
496
+ // cordis resolves each inject name as its own gate, so activation succeeds on
497
+ // whichever settings transport the host mounts (0.1.5 vs 0.1.7-rc.1). The
498
+ // dotted remote names stay declared — dropping them fails the loader fiber.
499
+ exports.inject = ['slots', 'remote', 'remote.settings']
286
500
  exports.apply = apply
287
501
  return module.exports
288
502
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-params",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "EN: models.dev parameter assistant for the official DeepSeek Harness Models page: per-provider one-click context / max-output / reasoning-effort fill. ZH: 官方模型设置页的 models.dev 参数助手:按 provider 一键补全上下文、max 输出与推理档位。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",