dsh-provider-show 0.1.0 → 0.1.1

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
@@ -3,221 +3,122 @@
3
3
  [![check](https://github.com/MosCloud/dsh-provider-show/actions/workflows/check.yml/badge.svg)](https://github.com/MosCloud/dsh-provider-show/actions/workflows/check.yml)
4
4
 
5
5
  在 DSH 输入框右下角的「模型 · 推理强度」前面,补上**当前模型所属的供应商**。
6
-
7
6
  已被其他插件接管的供应商(例如 WorkBuddy)保持原样,不重复显示。
8
7
 
9
- ---
10
-
11
- ## 它解决什么问题
12
-
13
- DSH 的模型选择器(composer 模型位)自带两项信息:
14
-
15
- | 位置 | 内容 | 由谁渲染 |
16
- |---|---|---|
17
- | 模型名 | `DeepSeek-V4.1-Flash` | 宿主,经 `conversation.input.model.label` slot |
18
- | 推理强度 | `Default` / `High` | 宿主,非 slot |
19
-
20
- **缺的是第三项:这是哪个供应商的模型。**
21
-
22
- 第三方插件接管某个供应商后会自己补上这一项(WorkBuddy 画 `WB CN · 2,987`、倍率、促销角标),
23
- 但没有任何插件接管的供应商(例如你在设置里自建的火山方舟路由)就只有「模型 · 推理强度」,
24
- 看不出模型来自哪里。
25
-
26
- 本插件补齐这个缺口,并遵守一条让位规则:
27
-
28
- > **供应商已被第三方插件接管 ⇒ 我方完全不渲染;否则 ⇒ 我方显示供应商名。**
29
-
30
- ## 效果
31
-
32
8
  ```
33
- 未接管(火山方舟) 火山方舟 DeepSeek V4.1 Flash · Default ← 我方新增「火山方舟」
34
- 已接管(WorkBuddy) DeepSeek-V4.1-Flash · x0.11 … ← 我方不出现
9
+ 未接管(火山方舟) 火山方舟 DeepSeek V4.1 Flash · Default ← 新增「火山方舟」
10
+ 已接管(WorkBuddy) DeepSeek-V4.1-Flash · x0.11 … ← 本插件不出现
35
11
  ```
36
12
 
37
- 芯片挂在模型选择器**左侧、同一条 flex 行**上,只显示供应商名:
38
- 模型名与推理强度宿主已经画了,重复即噪声。
39
-
40
13
  ## 安装
41
14
 
42
15
  ```bash
43
- dsh plugin --profile <你的-profile> add /path/to/dsh-provider-show
16
+ dsh plugin --profile <你的-profile> add dsh-provider-show
44
17
  ```
45
18
 
46
- 或在插件管理页安装该本地目录。安装后**刷新页面**。
47
-
48
- 卸载同样通过插件管理页,或从 profile 的插件清单移除。
49
-
50
- ## 判定规则
19
+ 也可以在插件管理页的「添加插件」里填包名或 GitHub 仓库地址。安装后**刷新页面**
20
+ (客户端模块是新增的,必须刷新才会加载)。
51
21
 
52
- 「是否已被第三方插件接管」由 Host 半读取 LLM 目录后判定,优先级从上到下:
22
+ 从源码开发时改用本地目录,改动即时生效:
53
23
 
54
- | 顺序 | 规则 | 结果 |
55
- |---|---|---|
56
- | 1 | `overrides[providerId]` 显式配置 | `third-party` 让位 / `mine` 显示 / `hide` 不显示 |
57
- | 2 | `settingsNs` 不在 `coreNamespaces` 内 | 判定为**已被插件接管**,让位 |
58
- | 3 | 目录里查不到该 provider,或目录不可读 | 判定为**未知**,**照常显示**(fail-open) |
59
-
60
- `settingsNs` 是「注册该 provider 的 Loader entry id」:
61
-
62
- - `llm-pi-ai` —— 你在设置里自建的 API 路由(火山、OpenRouter、任何 OpenAI 兼容服务)
63
- - `llm-deepseek` / `llm-deepseek-account` —— 官方 DeepSeek 两条路由
64
- - 其他任何值 —— 某个插件用**自己的** entry id 注册的,例如 `dsh-connect-workbuddy`
24
+ ```bash
25
+ dsh plugin --profile <你的-profile> add /path/to/dsh-provider-show
26
+ ```
65
27
 
66
- ### 为什么探测失败时选择「显示」而不是「隐藏」
28
+ 卸载通过插件管理页,或从 profile 的插件清单移除。
67
29
 
68
- 漏显只是信息不全,与插件没装时一样;
69
- 误判为「已接管」会让**所有**供应商都不显示,属于明显回归。
70
- 两种错误代价不对等,所以默认显示。
30
+ > npm 12 会留下一个 `0.0.0-stage` 占位版本(339 字节),是其暂存发布机制的产物;
31
+ > `latest` 指向真正的发布版本,安装不受影响。
71
32
 
72
33
  ## 配置
73
34
 
74
- 写在 profile 的 `cordis.patch.yml`,或插件条目的 `config`。**每一项都有默认值,
75
- 但一处配置会替换整个 `config` 对象,所以只写需要改的键即可,其余键自动取默认。**
35
+ 写在 profile 的 `cordis.patch.yml` 的插件条目下。**每项都有默认值,但一处配置会替换整个
36
+ `config` 对象,所以只写需要改的键,其余自动取默认。**
76
37
 
77
38
  ```yaml
78
39
  - id: dsh-provider-show
79
40
  name: dsh-provider-show
80
41
  config:
81
- # 总开关(默认 true)
82
- enabled: true
42
+ enabled: true # 总开关,默认 true
83
43
 
84
- # 供应商短名,覆盖目录里的显示名
85
- shortNames:
44
+ shortNames: # 供应商短名,覆盖目录里的显示名
86
45
  volcano-api: 火山
87
46
 
88
- # 强制某个 provider 的处置
89
- overrides:
47
+ overrides: # 强制某个供应商的处置
90
48
  some-connector-provider: third-party # 让位给接管它的插件
91
- another-provider: mine # 即使探测说被接管,也照常显示
92
- noisy-provider: hide # 不显示,但不声称它被别人接管
49
+ another-provider: mine # 即使探测说被接管也显示
50
+ noisy-provider: hide # 不显示,但不声称被别人接管
93
51
 
94
- # 宿主自有的 settings 命名空间;宿主升级新增内置 provider 时可扩展
95
- coreNamespaces:
52
+ coreNamespaces: # 宿主自有的 settings 命名空间,宿主升级可扩展
96
53
  - llm-pi-ai
97
54
  - llm-deepseek
98
55
  - llm-deepseek-account
99
56
  ```
100
57
 
101
- ## 架构
102
-
103
- ```
104
- lib/ownership.js 纯函数:归属判定 + 文案选择(无任何 import,两侧共用)
105
- lib/index.js Host 半:读 LLM 目录 → 决策 → GET /plugins/dsh-provider-show/providers
106
- lib/client.js Client 半:注册 conversation.input.right,读宿主文档并渲染
107
- ```
108
-
109
- ### 三个关键设计取舍
110
-
111
- **1. 用 `conversation.input.right`,不用 `conversation.input.model.label`**
112
-
113
- `input.model.label` 是 `single` slot:同优先级二次注册会**直接抛错**,
114
- 且 workbuddy 已用 `priority: -0.5` 占位、压制了宿主的 1017 徽标。
115
- 去抢那一格必然与第三方插件冲突。
116
-
117
- `input.right` 是 `list` slot(`replaceRisk: "none"`),用独有 id 叠加,
118
- 渲染位置恰好就在模型选择器左边同一条 flex 行(`ui-conversation` 中
119
- `.standardControls` 的子节点顺序即 `input.right` → `input.model`)。
120
-
121
- 代价:芯片宽度计入 `.trailing`,会让模型名**更早**折叠成图标,
122
- 所以芯片有 `max-width: 140px` 与省略号截断,并用 `margin-inline-end:-10px`
123
- 抵消宿主的 12px 栅格间距。
124
-
125
- **排版对齐:靠"同度量",不靠位移**(由 `scripts/verify.mjs` 守住)
126
-
127
- | 约束 | 原因 |
128
- |---|---|
129
- | `font-size:13px` + `font-weight:400` + `line-height:20px` + `height:28px` + `align-items:center`,且**不声明 `font-family`** | 与模型触发器 `.htuFrW_trigger` 完全同一套度量。行盒内基线位置是 `half-leading + ascent`,**由字号与字体决定、与 `line-height` 无关**;度量相同 ⇒ 基线自动重合,不需要任何对齐技巧。 |
130
- | `margin-inline-end:-10px`、自身 `padding:0` | 宿主 `.standardControls` 有 12px `gap`,模型触发器又有 8px 左内边距;不补偿则两标签相距约 20px,补偿后约 10px,读作一行。 |
131
- | **禁止 `translateY`** | 见下节。任何人再想加中文补偿,`verify.mjs` 会直接判红。 |
132
-
133
- ### 中文曾经"不居中"的真相(有实测数据)
134
-
135
- 早期我在 12px 字号下看到中文偏高,推断是"汉字占满 em 框、无下伸部"导致的,于是加了位移补偿。
136
- **这个推断在字号不一致的前提下才成立,字号对齐后就不成立了。**
58
+ ## 判定规则
137
59
 
138
- 从 2 倍截图里量每个文字块的墨迹质心(1 CSS px = 2 图像 px):
60
+ 「是否已被第三方插件接管」由 Host 半读取 LLM 目录后判定,优先级自上而下:
139
61
 
140
- | 文字块 | 墨迹行范围 | 质心 |
62
+ | 顺序 | 规则 | 结果 |
141
63
  |---|---|---|
142
- | `火山` | 19..43 | **33.12** |
143
- | `DeepSeek` | 19..42 | 29.80 |
144
- | `Flash` | 19..38 | 28.90 |
145
- | `×0.20` | 19..38 | 28.76 |
146
- | `High` | 19..42 | 30.02 |
147
-
148
- 拉丁质心均值约 **29.4**。加上 `+2px` 补偿后汉字质心 33.12,比拉丁**低 3.7 图像 px ≈ 1.85 CSS px**;
149
- 去掉补偿正好落到 29.1,与拉丁齐平。**所以 13px 下的最优补偿是 0。**
64
+ | 1 | `overrides[providerId]` 显式配置 | `third-party` 让位 / `mine` 显示 / `hide` 不显示 |
65
+ | 2 | `settingsNs` 不在 `coreNamespaces` 内 | 判定为**已被插件接管**,让位 |
66
+ | 3 | 目录里查不到,或目录不可读 | 判定为**未知**,**照常显示**(fail-open) |
150
67
 
151
- 这也解释了为什么纯拉丁标签一直正常:它们的位移恒为 0,从未被补偿影响。
68
+ `settingsNs` 是「注册该 provider 的 Loader entry id」:
152
69
 
153
- ### 为什么层级靠颜色而不是字号
70
+ - `llm-pi-ai` —— 你在设置里自建的 API 路由(火山、OpenRouter、任何 OpenAI 兼容服务)
71
+ - `llm-deepseek` / `llm-deepseek-account` —— 官方 DeepSeek 两条路由
72
+ - 其他任何值 —— 某个插件用**自己的** entry id 注册的,例如 `dsh-connect-workbuddy`
154
73
 
155
- 宿主自己就是这么做的:模型名 `label-secondary`、推理强度 `label-caption`,**两者都是 13px**。
156
- 供应商作为第三层用 `label-tertiary`、同为 13px,于是整行读作「供应商 · 模型 · 强度」一个视觉单位。
74
+ 第 3 条选 fail-open 是因为两种错误代价不对等:漏显只是信息不全,而误判会让**所有**供应商都不显示。
157
75
 
158
- **2. 归属判定只在 Host 侧做一次**
76
+ **新会话显示什么**:会话还没有任何模型选择时(没切过模型、也没发过消息),`modelSelection`
77
+ 投影是空的 —— 此时显示的是**部署默认模型所属的供应商**。这个默认值由 Host 从
78
+ `agentDefaultModel` 服务读出,与模型座位同源,所以芯片和座位不会各说各话。
79
+ 发出第一条消息或切一次模型之后,改由该会话自己的选择决定。
80
+ 默认通道若被第三方插件接管,仍然让位。
159
81
 
160
- 浏览器看不到 `settingsNs`:模型目录的 provider group 只有 `{ id, name, models }`,
161
- `prepareRoutes` 在进入目录前就把适配器元数据裁掉了。
162
- 所以规则放在 Host,客户端只做查表 —— 一处实现,不会自我漂移。
82
+ ## 架构
163
83
 
164
- **3. Host 半不 import 任何 Harness 包**
84
+ | 文件 | 职责 |
85
+ |---|---|
86
+ | `lib/ownership.js` | 归属判定与文案选择,纯函数、零 import,Host/Client 共用 |
87
+ | `lib/index.js` | Host 半:读 LLM 目录 → 决策 → 只读路由 `GET /plugins/dsh-provider-show/providers` |
88
+ | `lib/client.js` | Client 半:注册 `conversation.input.right`,读 Host 文档并渲染 |
165
89
 
166
- 本地目录安装会把本仓库链接进 profile,其模块解析走 profile 的 `node_modules`,
167
- 而那里**没有**内置 Harness 包。一个 `import Schema from '@deepseek-ai/schemastery'`
168
- 就会让整个 Host 半加载失败。
169
- 因此配置用纯数据描述(`CONFIG_KEYS` / `effectiveConfig`),不导出 `Config`;
170
- Loader 在没有 `Config` 时原样透传配置,默认值由本插件自己补。
90
+ 分层原则:**浏览器看不到的运行时事实(`settingsNs`)在 Host 决策,客户端只消费结论**,
91
+ 规则只有一份实现。设计取舍见 [docs/design.md](docs/design.md)。
171
92
 
172
93
  ## 开发
173
94
 
174
95
  ```bash
175
- npm test # 27 项单元 + 行为测试(纯 Node,不需要 DSH 运行时)
176
- npm run check # 语法检查 + 19 项静态校验 + 全部测试
96
+ npm test # 单元 + 行为测试(纯 Node,不需要 DSH 运行时)
97
+ npm run check # 语法检查 + 静态校验 + 全部测试
177
98
  ```
178
99
 
179
- `scripts/verify.mjs` 的 19 项检查各自对应一条「否则只会在运行时才暴露」的规则:
180
- manifest 完整性、图标合法性与体积、locale 非占位文案、客户端只通过
181
- `__ModuleLoader__` 注册且不 import Harness Client 包、样式只用主题 token 且有宽度上界、
182
- 路由是只读 GET 且在 `ctx.effect` 内注册、归属规则只有一份实现等。
100
+ `scripts/verify.mjs` 的每项检查都对应一条「否则只会在运行时才暴露」的规则:manifest 完整性、
101
+ 图标合法性、locale 非占位文案、客户端不 import Harness Client 包、样式只用主题 token 且有宽度上界、
102
+ 路由是只读 GET 且由 `scope.effect` 持有、归属规则只有一份实现、排版度量与宿主一致且禁止位移补偿。
183
103
 
184
- ## 已知限制
185
-
186
- - 需要 Host 半的只读路由(`GET /plugins/dsh-provider-show/providers`)可用;
187
- 该路由仅接受本页同源请求。该路径是 `/plugins/` 下的 **exact** 路由,
188
- 优先级高于 `/plugins` 前缀路由(webserver 先查 exact 表),因此不与客户端模块路由冲突。
189
- - 芯片会让模型名更早折叠为图标(见上文取舍 1)。
190
- - 当 `conversation.input.activity` 展开整行时,宿主会隐藏整组 `standardControls`,
191
- 芯片随之隐藏 —— 这是宿主行为。
192
- - 供应商显示名默认取目录里的 `displayName`(即你在设置里填的名字);
193
- 想更短就配 `shortNames`。
194
- - 归属判定的可靠性取决于第三方插件是否用自有 `settingsNs` 注册 provider。
195
- 若某插件复用核心命名空间,请用 `overrides` 显式标注。
196
-
197
- ## 状态
198
-
199
- **已完成:安装 → 运行 → 视觉验收。全部验收项通过。**
104
+ 零外部依赖,所以 CI 没有安装步骤——干净 checkout 直接跑同一套检查(Node 22 / 24)。
200
105
 
201
- | 项 | 结果 |
202
- |---|---|
203
- | 安装 | 进入 profile bundles 与依赖(本地目录 `link:`) |
204
- | Host 行 | `fiberPhase: active` |
205
- | 只读路由 | `GET /plugins/dsh-provider-show/providers` 实测 200,决策正确 |
206
- | 真实判定 | `火山API` → 显示;`workbuddy` / `workbuddy-global` → 让位 |
207
- | 视觉验收 | 供应商与模型/强度齐平、间距合适、纯拉丁与中文均正常 |
208
- | 测试 | 38 项(归属规则、Host 路由行为、Client 渲染) |
209
- | 静态校验 | 23 项,逐条做过反证测试 |
210
- | CI | GitHub Actions 在 Node 22 / 24 上跑同一套检查 |
211
-
212
- ### 卸载
106
+ ## 已知限制
213
107
 
214
- 插件管理页禁用该 bundle,或从 profile 依赖里移除 `dsh-provider-show`。
108
+ - 芯片会让模型名更早折叠为图标(`max-width: 140px`,宿主按整行宽度判定折叠)。
109
+ - `conversation.input.activity` 展开整行时,宿主会隐藏整组 `standardControls`,芯片随之隐藏。
110
+ - 供应商显示名默认取目录里的 `displayName`;想更短就配 `shortNames`。
111
+ - 判定依赖第三方插件用自有 `settingsNs` 注册 provider;若某插件复用核心命名空间,
112
+ 请用 `overrides` 显式标注。
113
+ - 只读路由仅接受本页同源请求;它是 `/plugins/` 下的 exact 路由,优先级高于客户端模块的
114
+ `/plugins` 前缀路由,两者不冲突。
215
115
 
216
116
  ## 文档
217
117
 
218
118
  | 文件 | 内容 |
219
119
  |---|---|
220
- | [docs/design.md](docs/design.md) | 需求确认、架构决策(ADR)、边界约束、验收标准、返工记录 |
120
+ | [docs/lessons.md](docs/lessons.md) | 开发经验与踩坑记录——插槽生命周期、验证方法论、发布流程。做同类插件前先读 |
121
+ | [docs/design.md](docs/design.md) | 架构决策(ADR)、边界约束、验收标准、渲染与排版细节、返工留档 |
221
122
  | [docs/research.md](docs/research.md) | 技术考据:槽位拓扑、归属判定信号、排版实测数据,均带源码文件与行号 |
222
123
 
223
124
  ## 许可
package/lib/client.js CHANGED
@@ -144,11 +144,18 @@ window.__ModuleLoader__.load({
144
144
  /**
145
145
  * The provider label beside the model seat.
146
146
  *
147
- * Reads the session's durable `modelSelection` projection — the same value
148
- * the Host stamps on outgoing requests — so the label cannot disagree with
149
- * the model seat about which provider is selected. It renders nothing while
150
- * the document is in flight, because showing a label that is about to be
151
- * suppressed (the provider belongs to another plugin) would flash.
147
+ * Which provider is in effect is decided by a three-leg chain, strongest
148
+ * first: a pending selection, the last logged request header, then the
149
+ * deployment default. The `modelSelection` projection carries the first two
150
+ * (`next` is `pending ?? lastUsed`); the third exists only on the Host,
151
+ * because it is read from the `agentDefaultModel` service — the same one the
152
+ * model seat falls back to. That is why the document ships a `default`:
153
+ * without it a brand-new Session, which has no selection and no request yet,
154
+ * would show no provider at all while the seat already shows its model.
155
+ *
156
+ * Reading the seat's own authority is what keeps the two in agreement; this
157
+ * half still only looks a row up, and still draws nothing while the document
158
+ * is in flight, so a label that is about to be suppressed cannot flash.
152
159
  *
153
160
  * @param {object} props - slot props; `useProjection` and `t` are supplied by the slot.
154
161
  * @returns {import('react').ReactElement | null} the label, or null.
@@ -156,14 +163,11 @@ window.__ModuleLoader__.load({
156
163
  function ProviderLabel(props) {
157
164
  const { useProjection, sessionId, t } = props
158
165
  const selection = useProjection('modelSelection')
159
- const providerId = selection?.next?.provider ?? selection?.lastUsed?.provider ?? ''
160
166
  const [document, setDocument] = useState(undefined)
161
167
 
168
+ // Fetch unconditionally, including when nothing is selected: on a fresh
169
+ // Session the document's `default` is the ONLY source for this label.
162
170
  useEffect(() => {
163
- if (providerId === '') {
164
- setDocument(undefined)
165
- return undefined
166
- }
167
171
  let live = true
168
172
  readDocument().then((next) => {
169
173
  if (live) setDocument(next)
@@ -171,8 +175,9 @@ window.__ModuleLoader__.load({
171
175
  return () => {
172
176
  live = false
173
177
  }
174
- }, [providerId, sessionId])
178
+ }, [sessionId])
175
179
 
180
+ const providerId = selection?.next?.provider ?? document?.default?.provider ?? ''
176
181
  if (providerId === '') return null
177
182
  const row = rowFor(document, providerId)
178
183
  // `visible` is decided by the Host: it carries the plugin switch, the
@@ -193,15 +198,14 @@ window.__ModuleLoader__.load({
193
198
  }
194
199
 
195
200
  return {
196
- // Only what the component actually needs. `remote` is deliberately absent:
197
- // this half reads its own Host route, which works in every composition and
198
- // keeps the ownership rule on the side that can evaluate it.
199
201
  // Only what the component actually needs, spelled the way the cordis
200
202
  // Loader expects (a plain array of REQUIRED service keys — the Loader has
201
203
  // no "optional" inject form, and a key listed here parks the plugin until
202
204
  // that service exists). `remote` is deliberately absent: this half reads
203
205
  // its own Host route, which works in every composition and keeps the
204
- // ownership rule on the side that can evaluate it.
206
+ // ownership rule on the side that can evaluate it. The deployment default
207
+ // arrives inside that document for the same reason: the browser cannot see
208
+ // the Host's `agentDefaultModel`.
205
209
  inject: ['slots', 'locale'],
206
210
  apply(ctx) {
207
211
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-provider-show: dictionaries')
package/lib/index.js CHANGED
@@ -8,13 +8,24 @@
8
8
  * small, already-decided document:
9
9
  *
10
10
  * GET /plugins/dsh-provider-show/providers
11
- * { ok: true, value: { providers: [{ provider, displayName, label, owner, reason }] } }
11
+ * { ok: true, value: {
12
+ * providers: [{ provider, displayName, label, owner, reason }],
13
+ * default: { provider, model } | null,
14
+ * } }
12
15
  *
13
16
  * Each row is decided HERE by the shared pure resolver, so the ownership rule
14
17
  * has exactly one implementation and the client half stays a lookup. No Client
15
18
  * package, no `remote.llm` dependency, and no second copy of the rule that could
16
19
  * drift from this one.
17
20
  *
21
+ * `default` is the deployment's own default selection, read from the same
22
+ * `agentDefaultModel` service the composer's model seat falls back to. It is
23
+ * published because a brand-new Session has no model selection yet — the
24
+ * `modelSelection` projection stays `{ lastUsed: null, next: null }` until the
25
+ * first pick or the first request — and the browser has no other way to learn
26
+ * which provider is in effect before then. Handing over the VALUE instead of the
27
+ * service keeps the rule on this side: the client still only looks a row up.
28
+ *
18
29
  * A Host-only bundle would need the route's consumer to exist; the Client half
19
30
  * is what consumes it, so the two ship together in one bundle.
20
31
  *
@@ -144,6 +155,38 @@ function read(call) {
144
155
  }
145
156
  }
146
157
 
158
+ /**
159
+ * Read the deployment's default model selection.
160
+ *
161
+ * This is the third and last leg of the chain that decides which provider is in
162
+ * effect — `pending selection → last logged request header → deployment default`
163
+ * — and the only leg the browser cannot see. The model seat resolves it through
164
+ * the same service (`buildModelCatalog` defaults to
165
+ * `ctx.agentDefaultModel.currentSelection()`), so publishing this value is what
166
+ * makes the chip agree with the seat instead of guessing at it.
167
+ *
168
+ * The service is optional: a composition without `agent-default-model` has no
169
+ * default to publish, and a failing read is not a failing route. Both degrade to
170
+ * `null`, which the client renders as "draw nothing" — the failure mode this
171
+ * plugin has always chosen over showing a wrong label.
172
+ *
173
+ * @param {import('@deepseek-ai/cordis').Context} ctx - host context.
174
+ * @returns {{ provider: string, model: string } | null} the default selection.
175
+ */
176
+ function readDefaultSelection(ctx) {
177
+ try {
178
+ const service = ctx.get('agentDefaultModel')
179
+ const picked = service?.currentSelection?.()
180
+ if (picked === null || typeof picked !== 'object') return null
181
+ const { provider, model } = picked
182
+ if (typeof provider !== 'string' || provider === '') return null
183
+ if (typeof model !== 'string' || model === '') return null
184
+ return { provider, model }
185
+ } catch {
186
+ return null
187
+ }
188
+ }
189
+
147
190
  /**
148
191
  * Build the document the client half reads.
149
192
  *
@@ -155,8 +198,8 @@ function read(call) {
155
198
  *
156
199
  * @param {import('@deepseek-ai/cordis').Context} ctx - host context.
157
200
  * @param {ReturnType<typeof effectiveConfig>} config - effective configuration.
158
- * @returns {{ providers: Array<{provider: string, displayName: string, label: string, owner: string, reason: string}>, coreNamespaces: string[] }}
159
- * one decided row per provider route.
201
+ * @returns {{ providers: Array<{provider: string, displayName: string, label: string, owner: string, reason: string}>, coreNamespaces: string[], default: { provider: string, model: string } | null }}
202
+ * one decided row per provider route, plus the deployment default.
160
203
  */
161
204
  export function describeProviders(ctx, config) {
162
205
  const { rows, providers } = readDirectory(ctx)
@@ -197,7 +240,7 @@ export function describeProviders(ctx, config) {
197
240
  reason: decision.reason,
198
241
  })
199
242
  }
200
- return { providers: out, coreNamespaces }
243
+ return { providers: out, coreNamespaces, default: readDefaultSelection(ctx) }
201
244
  }
202
245
 
203
246
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-provider-show",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shows which provider the selected model belongs to in the composer row, and stays silent for providers another plugin already presents.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",