@dsh-plus/subagent-model 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.
package/lib/index.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import "@deepseek-ai/dsh-settings";
3
2
  import { Context, Service } from "@deepseek-ai/cordis";
4
-
5
3
  //#region src/config.d.ts
6
-
7
4
  /** 一条条目的规范输出形态。 */
8
5
  interface EntryConfig {
9
6
  enabled: boolean;
package/lib/index.js CHANGED
@@ -1,31 +1,33 @@
1
- import z from "@deepseek-ai/schemastery";
2
1
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import z from "@deepseek-ai/schemastery";
3
3
  import { Context, Service } from "@deepseek-ai/cordis";
4
-
5
- //#region src/ns.ts
6
- /**
7
- * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
8
- * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
9
- * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
10
- * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
11
- * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
12
- * @module subagent-model/ns
13
- */
14
- const SETTINGS_NS$1 = "dsh-plus-subagent-model";
15
-
16
4
  //#endregion
17
5
  //#region src/config.ts
6
+ /**
7
+ * 配置单一事实源:cordis 行级 Config(组合默认值,dev/prod patch 层可覆盖)
8
+ * 与 settings namespace(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)
9
+ * 共用同一 schemastery schema。条目按【子代理 provider 名】键控
10
+ * (standard preset 事实映射:subagent↔spawn、subagent_fork↔fork)。
11
+ *
12
+ * 运行时语义(详见 delegation.ts):
13
+ * - provider/model 显式时注入子代理 `agentOptions`(覆盖"继承主代理"快照);
14
+ * - reasoningEffort 显式时经私有字段随 agentOptions 抵达子代理,再由
15
+ * `agent/request` 瀑布监听器应用(dsh 原生 AgentOptions 不含 effort 字段)。
16
+ *
17
+ * 类型说明:z.dict 的推断类型引用 cosmokit 的 Dict,直接导出会触发
18
+ * dts 的 TS2742(不可具名引用);按官方 dsh-permission-presets 模式,
19
+ * 显式声明输入/输出接口并注解 schema(输出为规范型,输入为可空宽松型)。
20
+ * @module subagent-model/config
21
+ */
18
22
  /** settings 命名空间;webui 配置卡片与插件运行期读取同一份(字面量见 ./ns.ts)。 */
19
- const SETTINGS_NS = settingsNamespace(SETTINGS_NS$1);
23
+ const SETTINGS_NS = settingsNamespace("dsh-plus-subagent-model");
20
24
  /** 思考程度哨兵值:继承主代理(不注入 effort 字段)。 */
21
25
  const EFFORT_INHERIT = "inherit";
22
- /** 思考程度哨兵值:跟随模型默认(显式剥离任何继承/推导出的 effort)。 */
23
- const EFFORT_DEFAULT = "default";
24
26
  const EntrySchema = z.object({
25
27
  enabled: z.boolean().description("该子代理 provider 行的开关").default(false),
26
28
  provider: z.string().description("LLM 提供商 id;留空 = 继承主代理").default(""),
27
29
  model: z.string().description("模型 id;留空 = 继承主代理").default(""),
28
- reasoningEffort: z.string().description(`思考程度:${EFFORT_INHERIT}(继承主代理)/ ${EFFORT_DEFAULT}(跟随模型默认)/ 提供商目录档位 id`).default(EFFORT_INHERIT)
30
+ reasoningEffort: z.string().description(`思考程度:${EFFORT_INHERIT}(继承主代理)/ default(跟随模型默认)/ 提供商目录档位 id`).default(EFFORT_INHERIT)
29
31
  });
30
32
  const Config = z.object({
31
33
  enabled: z.boolean().description("总开关").default(false),
@@ -40,7 +42,7 @@ function resolveEntry(entry) {
40
42
  const out = {};
41
43
  if (entry.provider.length > 0) out.provider = entry.provider;
42
44
  if (entry.model.length > 0) out.model = entry.model;
43
- if (entry.reasoningEffort !== EFFORT_INHERIT) out.reasoningEffort = entry.reasoningEffort;
45
+ if (entry.reasoningEffort !== "inherit") out.reasoningEffort = entry.reasoningEffort;
44
46
  return out.provider !== void 0 || out.model !== void 0 || out.reasoningEffort !== void 0 ? out : void 0;
45
47
  }
46
48
  /**
@@ -61,8 +63,8 @@ function mergeAgentOptions(injected, existing) {
61
63
  */
62
64
  function applyEffort(resolved, effort) {
63
65
  if (effort === void 0) return resolved;
64
- if (effort === EFFORT_DEFAULT) {
65
- const { reasoningEffort: _dropped,...rest } = resolved;
66
+ if (effort === "default") {
67
+ const { reasoningEffort: _dropped, ...rest } = resolved;
66
68
  return rest;
67
69
  }
68
70
  return {
@@ -81,13 +83,12 @@ function validateEntry(entry) {
81
83
  }
82
84
  /** 校验整组条目(settings 写入的 validate 钩子用);返回首个错误消息,合法时返回 null。 */
83
85
  function validateEntries(entries) {
84
- for (const [name$1, entry] of Object.entries(entries)) {
86
+ for (const [name, entry] of Object.entries(entries)) {
85
87
  const error = validateEntry(entry);
86
- if (error !== null) return `条目 ${name$1}: ${error}`;
88
+ if (error !== null) return `条目 ${name}: ${error}`;
87
89
  }
88
90
  return null;
89
91
  }
90
-
91
92
  //#endregion
92
93
  //#region src/catalog.ts
93
94
  function toEffort(effort) {
@@ -154,7 +155,6 @@ function subagentProvidersOf(ctx) {
154
155
  const subagents = ctx.get("subagents");
155
156
  return subagents === void 0 ? [] : [...subagents.list()];
156
157
  }
157
-
158
158
  //#endregion
159
159
  //#region src/config-api.ts
160
160
  const ROUTE_CATALOG = "/dsh-plus/subagent-model/catalog";
@@ -192,13 +192,12 @@ function registerCatalogApi(ctx) {
192
192
  })
193
193
  });
194
194
  }
195
-
196
195
  //#endregion
197
196
  //#region src/delegation.ts
198
197
  const WRAPPED = Symbol("dsh-plus-subagent-model.wrapped");
199
- function entryFor(current, name$1) {
200
- if (name$1 === void 0) return void 0;
201
- const entry = current().entries[name$1];
198
+ function entryFor(current, name) {
199
+ if (name === void 0) return void 0;
200
+ const entry = current().entries[name];
202
201
  return entry === void 0 ? void 0 : resolveEntry(entry);
203
202
  }
204
203
  /**
@@ -220,10 +219,10 @@ function installDelegationHook(ctx, current) {
220
219
  }
221
220
  const originalStart = subagents.start.bind(subagents);
222
221
  const originalContinuable = subagents.startContinuable.bind(subagents);
223
- subagents.start = async (name$1, request) => {
224
- const injected = entryFor(current, name$1);
225
- if (injected === void 0) return originalStart(name$1, request);
226
- return originalStart(name$1, {
222
+ subagents.start = async (name, request) => {
223
+ const injected = entryFor(current, name);
224
+ if (injected === void 0) return originalStart(name, request);
225
+ return originalStart(name, {
227
226
  ...request,
228
227
  agentOptions: mergeAgentOptions(injected, request.agentOptions)
229
228
  });
@@ -268,9 +267,19 @@ function installChildRoute(ctx) {
268
267
  return applyEffort(resolved, effort);
269
268
  });
270
269
  }
271
-
272
270
  //#endregion
273
271
  //#region src/service.ts
272
+ /**
273
+ * SubagentModelService:子代理模型配置中枢。
274
+ * - 配置:installSettingsSection 接入 settings 用户层($DSH_HOME/settings.yaml,热生效),
275
+ * 无 settings provider 时退化为 cordis 行级 config;写入经 validate 钩子
276
+ * 走 validateEntries 规则(model 不能脱离 provider 等),非法即拒。
277
+ * - 委托挂钩:包装 ctx.subagents.start/startContinuable,按 provider 名注入
278
+ * agentOptions(provider/model/effort 私有标记),未命中条目完全直通。
279
+ * - 子代理路由:根 ctx 的 agent/request 瀑布监听,把条目 effort 应用到子代理请求。
280
+ * - 自定义端点:ctx.webServer 仅注册模型目录路由(配置读写已走官方 settings RPC)。
281
+ * @module @dsh-plus/subagent-model
282
+ */
274
283
  var SubagentModelService = class extends Service {
275
284
  static [Context.inject] = ["subagents", "llm"];
276
285
  current;
@@ -303,7 +312,6 @@ var SubagentModelService = class extends Service {
303
312
  return subagents === void 0 ? [] : [...subagents.list()];
304
313
  }
305
314
  };
306
-
307
315
  //#endregion
308
316
  //#region src/index.ts
309
317
  const name = "dsh-plus-subagent-model";
@@ -311,6 +319,5 @@ const inject = ["subagents", "llm"];
311
319
  function apply(ctx, config) {
312
320
  ctx.plugin(SubagentModelService, config);
313
321
  }
314
-
315
322
  //#endregion
316
- export { Config, SubagentModelService, apply, inject, name };
323
+ export { Config, SubagentModelService, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/subagent-model",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "dsh-plus service+ui plugin: 为 subagent/subagent_fork 等子代理按提供商配置模型与思考程度(或继承主代理)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -40,12 +40,12 @@
40
40
  "devDependencies": {
41
41
  "@deepseek-ai/cordis": "4.0.1",
42
42
  "@deepseek-ai/cosmokit": "1.8.2",
43
- "@deepseek-ai/dsh-agent": "0.1.0-rc.8",
44
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8",
45
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
46
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
47
- "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
48
- "@deepseek-ai/dsh-subagent": "0.1.0-rc.8",
43
+ "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
44
+ "@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
45
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
47
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
48
+ "@deepseek-ai/dsh-subagent": "0.1.1-rc.2",
49
49
  "@deepseek-ai/schemastery": "3.18.1",
50
50
  "@types/react": "~18.3.1",
51
51
  "react": "^18.2.0"
package/src/catalog.ts CHANGED
@@ -48,7 +48,7 @@ function toEffort(effort: LlmReasoningEffortInfo): CatalogEffort {
48
48
  return {
49
49
  id: String(effort.id),
50
50
  name: effort.name,
51
- ...effort.description === undefined ? {} : { description: effort.description },
51
+ ...(effort.description === undefined ? {} : { description: effort.description }),
52
52
  }
53
53
  }
54
54
 
@@ -59,49 +59,62 @@ function toEffort(effort: LlmReasoningEffortInfo): CatalogEffort {
59
59
  export async function buildModelCatalog(ctx: Context): Promise<ModelCatalog> {
60
60
  const llm = ctx.get('llm')
61
61
  if (llm === undefined) {
62
- return { providers: [], failures: [], subagentProviders: subagentProvidersOf(ctx) }
62
+ return {
63
+ providers: [],
64
+ failures: [],
65
+ subagentProviders: subagentProvidersOf(ctx),
66
+ }
63
67
  }
64
- const catalog = await Promise.all(llm.listProviders().map(async (provider) => {
65
- try {
66
- const models = await llm.listModels(provider.id)
67
- const entries = await Promise.all(models.map(async (model) => {
68
- const resolved = await llm.resolveModelInfo(provider.id, model.id)
69
- const reasoning = resolved.reasoning === undefined ? undefined : {
70
- efforts: resolved.reasoning.efforts.map(toEffort),
71
- ...resolved.reasoning.defaultEffort === undefined
72
- ? {}
73
- : { defaultEffort: String(resolved.reasoning.defaultEffort) },
68
+ const catalog = await Promise.all(
69
+ llm.listProviders().map(async (provider) => {
70
+ try {
71
+ const models = await llm.listModels(provider.id)
72
+ const entries = await Promise.all(
73
+ models.map(async (model) => {
74
+ const resolved = await llm.resolveModelInfo(provider.id, model.id)
75
+ const reasoning =
76
+ resolved.reasoning === undefined
77
+ ? undefined
78
+ : {
79
+ efforts: resolved.reasoning.efforts.map(toEffort),
80
+ ...(resolved.reasoning.defaultEffort === undefined
81
+ ? {}
82
+ : {
83
+ defaultEffort: String(resolved.reasoning.defaultEffort),
84
+ }),
85
+ }
86
+ return {
87
+ id: model.id,
88
+ name: model.name,
89
+ ...(model.description === undefined ? {} : { description: model.description }),
90
+ ...(reasoning === undefined ? {} : { reasoning }),
91
+ }
92
+ }),
93
+ )
94
+ return {
95
+ kind: 'group' as const,
96
+ group: { id: provider.id, name: provider.name, models: entries },
74
97
  }
98
+ } catch (error) {
75
99
  return {
76
- id: model.id,
77
- name: model.name,
78
- ...model.description === undefined ? {} : { description: model.description },
79
- ...reasoning === undefined ? {} : { reasoning },
100
+ kind: 'failure' as const,
101
+ failure: {
102
+ id: provider.id,
103
+ name: provider.name,
104
+ message: error instanceof Error ? error.message : String(error),
105
+ },
80
106
  }
81
- }))
82
- return {
83
- kind: 'group' as const,
84
- group: { id: provider.id, name: provider.name, models: entries },
85
107
  }
86
- } catch (error) {
87
- return {
88
- kind: 'failure' as const,
89
- failure: {
90
- id: provider.id,
91
- name: provider.name,
92
- message: error instanceof Error ? error.message : String(error),
93
- },
94
- }
95
- }
96
- }))
108
+ }),
109
+ )
97
110
  return {
98
111
  providers: catalog
99
112
  .filter((item) => item.kind === 'group' && item.group.models.length > 0)
100
- .map((item) => item.kind === 'group' ? item.group : undefined)
113
+ .map((item) => (item.kind === 'group' ? item.group : undefined))
101
114
  .filter((group): group is CatalogProvider => group !== undefined),
102
115
  failures: catalog
103
116
  .filter((item) => item.kind === 'failure')
104
- .map((item) => item.kind === 'failure' ? item.failure : undefined)
117
+ .map((item) => (item.kind === 'failure' ? item.failure : undefined))
105
118
  .filter((failure): failure is CatalogFailure => failure !== undefined),
106
119
  subagentProviders: subagentProvidersOf(ctx),
107
120
  }
@@ -7,12 +7,10 @@
7
7
  * 落盘,自文档化);「模型目录」为唯一保留的自定义端点。
8
8
  * @module subagent-model/client/card
9
9
  */
10
- import { useEffect, useMemo, useSyncExternalStore, useState, type ReactElement } from 'react'
10
+ import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
11
11
 
12
12
  import { SETTINGS_NS } from '../ns.ts'
13
- import {
14
- fetchCatalog, type CatalogProvider, type ModelCatalog,
15
- } from './api.ts'
13
+ import { type CatalogProvider, fetchCatalog, type ModelCatalog } from './api.ts'
16
14
  import { CheckRow, SelectField, type SelectOption } from './fields.tsx'
17
15
  import type { Scope, SettingsApi } from './scope.ts'
18
16
 
@@ -53,7 +51,12 @@ interface Status {
53
51
  }
54
52
 
55
53
  const IDLE_STATUS: Status = { kind: 'idle', text: '' }
56
- const EMPTY_ROW: DraftRow = { enabled: false, provider: '', model: '', reasoningEffort: 'inherit' }
54
+ const EMPTY_ROW: DraftRow = {
55
+ enabled: false,
56
+ provider: '',
57
+ model: '',
58
+ reasoningEffort: 'inherit',
59
+ }
57
60
  const EFFORT_INHERIT = 'inherit'
58
61
  const EFFORT_DEFAULT = 'default'
59
62
 
@@ -64,14 +67,15 @@ function draftFrom(value: ConfigValue, catalog: ModelCatalog | null): Draft {
64
67
  for (const name of Object.keys(value.entries).sort()) names.add(name)
65
68
  for (const name of names) {
66
69
  const entry = value.entries[name]
67
- rows[name] = entry === undefined
68
- ? { ...EMPTY_ROW }
69
- : {
70
- enabled: entry.enabled,
71
- provider: entry.provider,
72
- model: entry.model,
73
- reasoningEffort: entry.reasoningEffort,
74
- }
70
+ rows[name] =
71
+ entry === undefined
72
+ ? { ...EMPTY_ROW }
73
+ : {
74
+ enabled: entry.enabled,
75
+ provider: entry.provider,
76
+ model: entry.model,
77
+ reasoningEffort: entry.reasoningEffort,
78
+ }
75
79
  }
76
80
  return { enabled: value.enabled, rows }
77
81
  }
@@ -84,16 +88,25 @@ function unknownOption(value: string, label: string): SelectOption {
84
88
  return { value, label: `${value}(${label})` }
85
89
  }
86
90
 
87
- function providerOptions(catalog: ModelCatalog | null, row: DraftRow, t: (k: string) => string): SelectOption[] {
91
+ function providerOptions(
92
+ catalog: ModelCatalog | null,
93
+ row: DraftRow,
94
+ t: (k: string) => string,
95
+ ): SelectOption[] {
88
96
  const options: SelectOption[] = [{ value: '', label: t('inheritProvider') }]
89
- for (const provider of catalog?.providers ?? []) options.push({ value: provider.id, label: provider.name })
97
+ for (const provider of catalog?.providers ?? [])
98
+ options.push({ value: provider.id, label: provider.name })
90
99
  if (row.provider.length > 0 && !options.some((o) => o.value === row.provider)) {
91
100
  options.push(unknownOption(row.provider, t('unknownValue')))
92
101
  }
93
102
  return options
94
103
  }
95
104
 
96
- function modelOptions(catalog: ModelCatalog | null, row: DraftRow, t: (k: string) => string): SelectOption[] {
105
+ function modelOptions(
106
+ catalog: ModelCatalog | null,
107
+ row: DraftRow,
108
+ t: (k: string) => string,
109
+ ): SelectOption[] {
97
110
  const options: SelectOption[] = [{ value: '', label: t('inheritModel') }]
98
111
  const provider = catalog?.providers.find((p) => p.id === row.provider)
99
112
  for (const model of provider?.models ?? []) options.push({ value: model.id, label: model.name })
@@ -103,14 +116,19 @@ function modelOptions(catalog: ModelCatalog | null, row: DraftRow, t: (k: string
103
116
  return options
104
117
  }
105
118
 
106
- function effortOptions(catalog: ModelCatalog | null, row: DraftRow, t: (k: string) => string): SelectOption[] {
119
+ function effortOptions(
120
+ catalog: ModelCatalog | null,
121
+ row: DraftRow,
122
+ t: (k: string) => string,
123
+ ): SelectOption[] {
107
124
  const options: SelectOption[] = [
108
125
  { value: EFFORT_INHERIT, label: t('effortInherit') },
109
126
  { value: EFFORT_DEFAULT, label: t('effortDefault') },
110
127
  ]
111
128
  const provider = catalog?.providers.find((p) => p.id === row.provider)
112
129
  const model = provider?.models.find((m) => m.id === row.model)
113
- for (const effort of model?.reasoning?.efforts ?? []) options.push({ value: effort.id, label: effort.name })
130
+ for (const effort of model?.reasoning?.efforts ?? [])
131
+ options.push({ value: effort.id, label: effort.name })
114
132
  if (row.reasoningEffort.length > 0 && !options.some((o) => o.value === row.reasoningEffort)) {
115
133
  options.push(unknownOption(row.reasoningEffort, t('unknownValue')))
116
134
  }
@@ -133,20 +151,44 @@ function RowBlock(props: RowBlockProps): ReactElement {
133
151
  <div className="dsm-row">
134
152
  <div className="dsm-rowHead">
135
153
  <span className="dsm-rowName">{name}</span>
136
- <CheckRow id={`dsm-row-${name}`} label={t('rowEnabled')} checked={row.enabled}
137
- disabled={disabled} onEdit={(v) => onEdit({ ...row, enabled: v })} />
154
+ <CheckRow
155
+ id={`dsm-row-${name}`}
156
+ label={t('rowEnabled')}
157
+ checked={row.enabled}
158
+ disabled={disabled}
159
+ onEdit={(v) => onEdit({ ...row, enabled: v })}
160
+ />
138
161
  </div>
139
162
  <p className="dsm-rowHint">{t('rowHint')}</p>
140
- <SelectField id={`dsm-provider-${name}`} label={t('provider')} hint={t('providerHint')}
141
- value={row.provider} options={providerOptions(catalog, row, t)} disabled={disabled}
142
- onEdit={(v) => onEdit({ ...row, provider: v })} />
143
- <SelectField id={`dsm-model-${name}`} label={t('model')} hint={t('modelHint')}
144
- value={row.model} options={modelOptions(catalog, row, t)} disabled={disabled || row.provider.length === 0}
145
- invalid={invalidModel} invalidLabel={t('invalidModel')}
146
- onEdit={(v) => onEdit({ ...row, model: v })} />
147
- <SelectField id={`dsm-effort-${name}`} label={t('effort')} hint={t('effortHint')}
148
- value={row.reasoningEffort} options={effortOptions(catalog, row, t)} disabled={disabled}
149
- onEdit={(v) => onEdit({ ...row, reasoningEffort: v })} />
163
+ <SelectField
164
+ id={`dsm-provider-${name}`}
165
+ label={t('provider')}
166
+ hint={t('providerHint')}
167
+ value={row.provider}
168
+ options={providerOptions(catalog, row, t)}
169
+ disabled={disabled}
170
+ onEdit={(v) => onEdit({ ...row, provider: v })}
171
+ />
172
+ <SelectField
173
+ id={`dsm-model-${name}`}
174
+ label={t('model')}
175
+ hint={t('modelHint')}
176
+ value={row.model}
177
+ options={modelOptions(catalog, row, t)}
178
+ disabled={disabled || row.provider.length === 0}
179
+ invalid={invalidModel}
180
+ invalidLabel={t('invalidModel')}
181
+ onEdit={(v) => onEdit({ ...row, model: v })}
182
+ />
183
+ <SelectField
184
+ id={`dsm-effort-${name}`}
185
+ label={t('effort')}
186
+ hint={t('effortHint')}
187
+ value={row.reasoningEffort}
188
+ options={effortOptions(catalog, row, t)}
189
+ disabled={disabled}
190
+ onEdit={(v) => onEdit({ ...row, reasoningEffort: v })}
191
+ />
150
192
  </div>
151
193
  )
152
194
  }
@@ -202,18 +244,24 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
202
244
 
203
245
  const dirty = useMemo(
204
246
  () =>
205
- value !== undefined && draft !== null &&
247
+ value !== undefined &&
248
+ draft !== null &&
206
249
  JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFrom(value, catalog))),
207
250
  [value, draft, catalog],
208
251
  )
209
252
  const invalid = useMemo(
210
- () => draft !== null && Object.values(draft.rows).some((row) =>
211
- row.model.length > 0 && row.provider.length === 0),
253
+ () =>
254
+ draft !== null &&
255
+ Object.values(draft.rows).some((row) => row.model.length > 0 && row.provider.length === 0),
212
256
  [draft],
213
257
  )
214
258
 
215
259
  if (value === undefined || draft === null) {
216
- return <li className="dsm-card"><p className="dsm-empty">{t('loading')}</p></li>
260
+ return (
261
+ <li className="dsm-card">
262
+ <p className="dsm-empty">{t('loading')}</p>
263
+ </li>
264
+ )
217
265
  }
218
266
  const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
219
267
  setDraft({ ...draft, [key]: editValue })
@@ -226,11 +274,12 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
226
274
  const onSave = (): void => {
227
275
  setSaving(true)
228
276
  const revision = scope.getSnapshot().revision
229
- api.update({
230
- ns: SETTINGS_NS,
231
- patch: toPatch(draft),
232
- ...(revision !== undefined ? { expectedRevision: revision } : {}),
233
- })
277
+ api
278
+ .update({
279
+ ns: SETTINGS_NS,
280
+ patch: toPatch(draft),
281
+ ...(revision !== undefined ? { expectedRevision: revision } : {}),
282
+ })
234
283
  .then(async (response) => {
235
284
  if (!response.result.ok) {
236
285
  throw new Error(response.result.error?.message ?? t('saveFailed'))
@@ -241,7 +290,10 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
241
290
  setStatus(IDLE_STATUS)
242
291
  })
243
292
  .catch((error: unknown) => {
244
- setStatus({ kind: 'error', text: `${t('saveFailed')}${error instanceof Error ? error.message : ''}` })
293
+ setStatus({
294
+ kind: 'error',
295
+ text: `${t('saveFailed')}${error instanceof Error ? error.message : ''}`,
296
+ })
245
297
  })
246
298
  .finally(() => setSaving(false))
247
299
  }
@@ -271,9 +323,18 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
271
323
  </button>
272
324
  {open ? (
273
325
  <div className="dsm-body">
274
- {disabled ? <p className="dsm-empty" role="status">{t('readOnly')}</p> : null}
275
- <CheckRow id="dsm-enabled" label={t('enabled')} checked={draft.enabled} disabled={disabled}
276
- onEdit={(v) => edit('enabled', v)} />
326
+ {disabled ? (
327
+ <p className="dsm-empty" role="status">
328
+ {t('readOnly')}
329
+ </p>
330
+ ) : null}
331
+ <CheckRow
332
+ id="dsm-enabled"
333
+ label={t('enabled')}
334
+ checked={draft.enabled}
335
+ disabled={disabled}
336
+ onEdit={(v) => edit('enabled', v)}
337
+ />
277
338
  {catalogFailed ? (
278
339
  <div className="dsm-banner" role="status">
279
340
  <span>{t('catalogError')}</span>
@@ -283,31 +344,53 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
283
344
  </div>
284
345
  ) : null}
285
346
  {catalog === null && !catalogFailed ? (
286
- <p className="dsm-empty" role="status">{t('catalogLoading')}</p>
347
+ <p className="dsm-empty" role="status">
348
+ {t('catalogLoading')}
349
+ </p>
287
350
  ) : null}
288
351
  {rowNames.length === 0 ? (
289
- <p className="dsm-empty" role="status">{t('noRows')}</p>
352
+ <p className="dsm-empty" role="status">
353
+ {t('noRows')}
354
+ </p>
290
355
  ) : null}
291
356
  {rowNames.map((name) => (
292
- <RowBlock key={name} name={name} row={draft.rows[name] ?? { ...EMPTY_ROW }}
293
- catalog={catalog} disabled={disabled} t={t}
294
- onEdit={(row) => editRow(name, row)} />
357
+ <RowBlock
358
+ key={name}
359
+ name={name}
360
+ row={draft.rows[name] ?? { ...EMPTY_ROW }}
361
+ catalog={catalog}
362
+ disabled={disabled}
363
+ t={t}
364
+ onEdit={(row) => editRow(name, row)}
365
+ />
295
366
  ))}
296
- <p className="dsm-hint dsm-rowHint">
297
- {providerCount > 0 ? t('rowDesc') : ''}
298
- </p>
367
+ <p className="dsm-hint dsm-rowHint">{providerCount > 0 ? t('rowDesc') : ''}</p>
299
368
  <div className="dsm-footer">
300
369
  {status.kind !== 'idle' ? (
301
- <p className={`dsm-status${status.kind === 'error' ? ' dsm-statusError' : ''}`} role="status">
370
+ <p
371
+ className={`dsm-status${status.kind === 'error' ? ' dsm-statusError' : ''}`}
372
+ role="status"
373
+ >
302
374
  {status.text}
303
375
  </p>
304
376
  ) : null}
305
- <button type="button" className="dsm-btn dsm-btnGhost" disabled={!dirty || saving}
306
- onClick={() => { setDraft(draftFrom(value, catalog)); setStatus(IDLE_STATUS) }}>
377
+ <button
378
+ type="button"
379
+ className="dsm-btn dsm-btnGhost"
380
+ disabled={!dirty || saving}
381
+ onClick={() => {
382
+ setDraft(draftFrom(value, catalog))
383
+ setStatus(IDLE_STATUS)
384
+ }}
385
+ >
307
386
  {t('discard')}
308
387
  </button>
309
- <button type="button" className="dsm-btn dsm-btnPrimary" disabled={!dirty || invalid || saving || disabled}
310
- onClick={onSave}>
388
+ <button
389
+ type="button"
390
+ className="dsm-btn dsm-btnPrimary"
391
+ disabled={!dirty || invalid || saving || disabled}
392
+ onClick={onSave}
393
+ >
311
394
  {t(saving ? 'saving' : 'save')}
312
395
  </button>
313
396
  </div>