@deepseek-ai/dsh-client-ui-model-selection 0.1.5-rc.2 → 0.1.6-alpha.2
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.i18n.yaml +2 -2
- package/README.md +7 -3
- package/README.zh.md +13 -9
- package/lib/client.js +157 -34
- package/lib/types/client/directory.d.ts +5 -4
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/slots.d.ts +3 -2
- package/package.json +16 -15
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-model-selection/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 5e16cdf18d2eccb63097c2a524bfb2d20178d15e
|
|
6
|
+
README.zh.md: a99581e827c5ce66a31f6e4cbad11394e03dde4d
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The Web GUI lets users switch the model and reasoning effort for an existing ses
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## Use this package
|
|
27
27
|
|
|
28
|
-
Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. Both surfaces show the host-reported current selection when the exact provider/model pair remains in the advertised groups; a missing catalog row leaves the routable selection intact while the trigger prompts `Select model`.
|
|
28
|
+
Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus across the rows of the shown pane, Tab settles the focused row, and Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling lands on the row of the value in use, and going back lands on the cell that opened the pane left. Both surfaces show the host-reported current selection when the exact provider/model pair remains in the advertised groups; a missing catalog row leaves the routable selection intact while the trigger prompts `Select model`.
|
|
29
29
|
|
|
30
30
|
### Model and effort
|
|
31
31
|
|
|
@@ -35,6 +35,10 @@ Models stay grouped by provider. The composer menu shows model and effort names
|
|
|
35
35
|
|
|
36
36
|
When the Host reports that no adapter serves the session's route, this plugin raises a composer block and the input goes inert with its own copy; recovering clears it without a reload. A `null` before the first load or after one failed never blocks, and catalog membership never blocks either — a route serving a model it does not advertise is missing from the groups yet usable.
|
|
37
37
|
|
|
38
|
+
### Selection failures
|
|
39
|
+
|
|
40
|
+
When another writer owns the Session, model-selection failures tell the user to quit other running DSH instances and retry.
|
|
41
|
+
|
|
38
42
|
-----
|
|
39
43
|
|
|
40
44
|
<a id="understand-the-implementation"></a>
|
|
@@ -43,7 +47,7 @@ When the Host reports that no adapter serves the session's route, this plugin ra
|
|
|
43
47
|
<details>
|
|
44
48
|
<summary>Implementation internals — click to expand</summary>
|
|
45
49
|
|
|
46
|
-
Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`ctx.modelDirectories`): the `/model` popupSelect contribution (registered through `ctx.commandUi`) and the composer's named `conversation.input.model` seat both load the session's advisory directory through `session.models` and submit through `session.selectModel` via the same `ModelDirectory` instance, so a switch made in either entry is what the other shows next. Directory loads and selections share a generation counter so an older response never overwrites a newer one; a connection reset drops every resident projection and repulls the Host-restored selection. Directories are per-session, resolved lazily, and disposed with the session scope; addressed subagent sessions expose neither entry. Every resident directory refetches directly on forwarded `llm/adapters-updated` and `settings/document-updated` owner events.
|
|
50
|
+
Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`ctx.modelDirectories`): the `/model` popupSelect contribution (registered through `ctx.commandUi`) and the composer's named `conversation.input.model` seat both load the session's advisory directory through `session.models` and submit through `session.selectModel` via the same `ModelDirectory` instance, so a switch made in either entry is what the other shows next. Directory loads and selections share a generation counter so an older response never overwrites a newer one; a connection reset drops every resident projection and repulls the Host-restored selection before display. Directories are per-session, resolved lazily, and disposed with the session scope; addressed subagent sessions expose neither entry. Every resident directory refetches directly on forwarded `llm/adapters-updated` and `settings/document-updated` owner events.
|
|
47
51
|
|
|
48
52
|
</details>
|
|
49
53
|
|
|
@@ -91,4 +95,4 @@ None.
|
|
|
91
95
|
|
|
92
96
|
</details>
|
|
93
97
|
|
|
94
|
-
**Runtime invariant:** No companion is published.
|
|
98
|
+
**Runtime invariant:** No companion is published. The plugin registers a single command contribution, and the HMR-safety spec proves that the registration is disposed correctly. The plugin emits no Cordis events and owns no cross-plugin mutable state.
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
Web GUI 允许用户通过 `/model` 弹窗或 composer
|
|
12
|
+
Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有会话使用的模型与推理(reasoning)强度。两个界面呈现同一组按提供方分组的选择;所选模型决定可用的推理强度名称与默认值。完整选择从下一次请求开始生效;运行中的步骤保留其启动时的模型与推理强度。如果没有适配器可以服务会话路由,composer 会保持停用,直至路由恢复可用。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## 使用本包
|
|
27
27
|
|
|
28
|
-
与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model`
|
|
28
|
+
与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。当确切提供方/模型对仍在已公布分组中时,两个界面都显示 Host 报告的当前选择;目录行缺席时,可路由的选择保持不变,触发器提示 `Select model`。
|
|
29
29
|
|
|
30
30
|
### 模型与推理强度
|
|
31
31
|
|
|
@@ -33,7 +33,11 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
33
33
|
|
|
34
34
|
### 不可路由的会话
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
当 Host 报告没有适配器服务该会话的路由时,本插件注册一个 composer 阻塞块,输入框随之停用并显示本插件自己的文案;恢复后无需重新加载即清除。首次加载之前或加载失败之后的 `null` 绝不阻断;目录成员关系同样不阻断——一条仍在服务、只是不公布该模型的路由不在分组里,却可用。
|
|
37
|
+
|
|
38
|
+
### 选择失败
|
|
39
|
+
|
|
40
|
+
当会话被其他写句柄占用时,模型选择失败提示用户退出其他正在运行的 DSH 后重试。
|
|
37
41
|
|
|
38
42
|
-----
|
|
39
43
|
|
|
@@ -43,7 +47,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
43
47
|
<details>
|
|
44
48
|
<summary>实现细节——点击展开</summary>
|
|
45
49
|
|
|
46
|
-
两个入口共用一份由 `ModelDirectoryResolver`(`ctx.modelDirectories`)持有的会话级目录:`/model` popupSelect 贡献项(经 `ctx.commandUi` 注册)与 composer 的具名 `conversation.input.model` 位都经 `session.models` 加载会话的建议目录、经 `session.selectModel` 通过同一个 `ModelDirectory`
|
|
50
|
+
两个入口共用一份由 `ModelDirectoryResolver`(`ctx.modelDirectories`)持有的会话级目录:`/model` popupSelect 贡献项(经 `ctx.commandUi` 注册)与 composer 的具名 `conversation.input.model` 位都经 `session.models` 加载会话的建议目录、经 `session.selectModel` 通过同一个 `ModelDirectory` 实例提交,因此任一入口所做的切换正是另一个入口接下来显示的。目录加载与选择共享一个代次计数器,旧响应不会覆盖新结果;连接重置丢弃所有常驻投影,并在显示前重新拉取 Host 恢复的选择。目录按会话惰性解析,随会话作用域一并 dispose(资源释放);已寻址 subagent 会话不公开任一入口。每份常驻目录都会直接在转发的 `llm/adapters-updated` 与 `settings/document-updated` owner 事件上重拉。
|
|
47
51
|
|
|
48
52
|
</details>
|
|
49
53
|
|
|
@@ -52,7 +56,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
52
56
|
<a id="further-exploration"></a>
|
|
53
57
|
## 进一步探索
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
当仅了解模型界面还不够时,请阅读以下页面。这些页面从浏览器界面逐步深入到命令弹窗外壳与选择约定。
|
|
56
60
|
|
|
57
61
|
- [ui-commands](../ui-commands/README.zh.md)——`/model` 贡献项注册进的 popupSelect 外壳。
|
|
58
62
|
- [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位与 composer 阻塞块。
|
|
@@ -64,7 +68,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
64
68
|
<a id="model-experience"></a>
|
|
65
69
|
## 模型体验
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
两个入口提交的 `session.selectModel` 选择会间接影响模型:Host 会在下一次提示词组装边界为完整的 `ModelSelection` 创建快照,并负责使其对模型生效;运行中的步骤则保留已组装的选择。
|
|
68
72
|
|
|
69
73
|
#### KV Cache 影响
|
|
70
74
|
|
|
@@ -75,9 +79,9 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
75
79
|
<a id="known-limitations-and-deferred-work"></a>
|
|
76
80
|
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
这些限制界定了当前模型选择界面。它们是当前包约束,不是通用模型路由器对比或任务积压。
|
|
79
83
|
|
|
80
|
-
- **无创建期或已寻址 subagent 选择**——两个入口都要求既有普通会话的 agent
|
|
84
|
+
- **无创建期或已寻址 subagent 选择**——两个入口都要求既有普通会话的 agent(智能体);没有可纳入会话创建的草稿阶段模型选择,subagent 继续执行也有意不公开独立的模型选择约定。
|
|
81
85
|
- **目录名仅供呈现**——选择与持久化使用提供方/模型/推理强度 id;目录查询或确切模型元数据查询失败的提供方以不可选失败行列出,重新加载前保持原样。
|
|
82
86
|
- **不能任意输入推理强度**——composer 仅提供确切模型由适配器公布的推理强度;适配器没有推理元数据时不显示 Effort 行。
|
|
83
87
|
|
|
@@ -91,4 +95,4 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
|
|
|
91
95
|
|
|
92
96
|
</details>
|
|
93
97
|
|
|
94
|
-
**运行时不变式:** 不发布伴生入口。插件只注册一个 command contribution,HMR
|
|
98
|
+
**运行时不变式:** 不发布伴生入口。插件只注册一个 command contribution,HMR(热模块替换)安全性测试证明该注册的 dispose 能正确完成;它不发出 Cordis 事件,也不持有跨插件可变状态。
|
package/lib/client.js
CHANGED
|
@@ -4,12 +4,75 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
7
8
|
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
8
9
|
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
9
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
11
|
let react = require("react");
|
|
11
12
|
let react_dom = require("react-dom");
|
|
12
|
-
|
|
13
|
+
//#region ../../util/values/src/index.ts
|
|
14
|
+
/**
|
|
15
|
+
* Weak-key lookup with a strongly retained iterable set of associated values.
|
|
16
|
+
*
|
|
17
|
+
* Each value must belong to only one key. The container performs no automatic
|
|
18
|
+
* cleanup; owners delete associations or clear the container at lifecycle end.
|
|
19
|
+
*/
|
|
20
|
+
var WeakMapWithValues = class {
|
|
21
|
+
keys = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
valueSet = /* @__PURE__ */ new Set();
|
|
23
|
+
/** Live strongly retained values in insertion order. */
|
|
24
|
+
values = this.valueSet;
|
|
25
|
+
/**
|
|
26
|
+
* Read the value associated with a key.
|
|
27
|
+
* @param key - weakly held lookup key.
|
|
28
|
+
* @returns the associated value, or absence.
|
|
29
|
+
*/
|
|
30
|
+
get(key) {
|
|
31
|
+
return this.keys.get(key);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Test whether a key has an association.
|
|
35
|
+
* @param key - weakly held lookup key.
|
|
36
|
+
* @returns whether the key is present.
|
|
37
|
+
*/
|
|
38
|
+
has(key) {
|
|
39
|
+
return this.keys.has(key);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Associate one key with one caller-unique value.
|
|
43
|
+
* @param key - weakly held lookup key.
|
|
44
|
+
* @param value - strongly retained value that belongs to no other key.
|
|
45
|
+
* @returns this container.
|
|
46
|
+
*/
|
|
47
|
+
set(key, value) {
|
|
48
|
+
if (this.keys.has(key)) {
|
|
49
|
+
const previous = this.keys.get(key);
|
|
50
|
+
if (previous === value) return this;
|
|
51
|
+
this.valueSet.delete(previous);
|
|
52
|
+
}
|
|
53
|
+
this.keys.set(key, value);
|
|
54
|
+
this.valueSet.add(value);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Remove one association and its strongly retained value.
|
|
59
|
+
* @param key - weakly held lookup key.
|
|
60
|
+
* @returns whether an association was removed.
|
|
61
|
+
*/
|
|
62
|
+
delete(key) {
|
|
63
|
+
if (!this.keys.has(key)) return false;
|
|
64
|
+
const value = this.keys.get(key);
|
|
65
|
+
const deleted = this.keys.delete(key);
|
|
66
|
+
this.valueSet.delete(value);
|
|
67
|
+
return deleted;
|
|
68
|
+
}
|
|
69
|
+
/** Remove every association and strongly retained value. */
|
|
70
|
+
clear() {
|
|
71
|
+
this.keys = /* @__PURE__ */ new WeakMap();
|
|
72
|
+
this.valueSet.clear();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
13
76
|
//#region lib/types/client/catalog.js
|
|
14
77
|
/** One Host-generation model catalog shared by every Session selector. */
|
|
15
78
|
/** Loads at most one model catalog for the current Host generation. */
|
|
@@ -146,8 +209,9 @@ window.__ModuleLoader__.load({
|
|
|
146
209
|
/**
|
|
147
210
|
* Select the complete provider/model/reasoning selection. The durable
|
|
148
211
|
* projection frame updates the shared current; failures surface on the store
|
|
149
|
-
* and
|
|
212
|
+
* and return with the operation so each entry can present its own failure.
|
|
150
213
|
* @param selection - provider, provider-owned model id, and optional adapter-owned effort.
|
|
214
|
+
* @returns the selection outcome, including the original Remote failure.
|
|
151
215
|
*/
|
|
152
216
|
async select(selection) {
|
|
153
217
|
this.assertAvailable();
|
|
@@ -162,22 +226,26 @@ window.__ModuleLoader__.load({
|
|
|
162
226
|
model: selection.model,
|
|
163
227
|
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
|
|
164
228
|
});
|
|
165
|
-
if (this.disposed || generation !== this.generation) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
229
|
+
if (this.disposed || generation !== this.generation) return result.ok ? {
|
|
230
|
+
ok: true,
|
|
231
|
+
value: void 0
|
|
232
|
+
} : result;
|
|
169
233
|
if (!result.ok) {
|
|
170
234
|
this.store.update((s) => {
|
|
171
235
|
s.status = "error";
|
|
172
236
|
s.error = `${result.error.code}: ${result.error.message}`;
|
|
173
237
|
});
|
|
174
|
-
|
|
238
|
+
return result;
|
|
175
239
|
}
|
|
176
240
|
this.store.update((s) => {
|
|
177
241
|
s.status = "ready";
|
|
178
242
|
s.error = null;
|
|
179
243
|
});
|
|
180
244
|
this.syncInputs();
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
value: void 0
|
|
248
|
+
};
|
|
181
249
|
}
|
|
182
250
|
/**
|
|
183
251
|
* Invalidate an in-flight selection response from the previous Host generation.
|
|
@@ -260,7 +328,7 @@ window.__ModuleLoader__.load({
|
|
|
260
328
|
"remote",
|
|
261
329
|
"remote.session"
|
|
262
330
|
];
|
|
263
|
-
live = { directories:
|
|
331
|
+
live = { directories: new WeakMapWithValues() };
|
|
264
332
|
catalog;
|
|
265
333
|
/** Localized composer-block copy; this plugin owns the string it raises. */
|
|
266
334
|
blockReason;
|
|
@@ -275,7 +343,7 @@ window.__ModuleLoader__.load({
|
|
|
275
343
|
this.catalog.load().catch(() => {});
|
|
276
344
|
ctx.on("connection/reset", () => {
|
|
277
345
|
this.catalog.resetGeneration();
|
|
278
|
-
for (const directory of this.live.directories.values
|
|
346
|
+
for (const directory of this.live.directories.values) directory.resetConnected();
|
|
279
347
|
});
|
|
280
348
|
ctx.remote.$on("llm/adapters-updated", () => {
|
|
281
349
|
this.catalog.refresh();
|
|
@@ -295,15 +363,15 @@ window.__ModuleLoader__.load({
|
|
|
295
363
|
*/
|
|
296
364
|
directoryFor(sessionId) {
|
|
297
365
|
const { live } = this;
|
|
298
|
-
const existing = live.directories.get(sessionId);
|
|
299
|
-
if (existing !== void 0) return existing;
|
|
300
366
|
const sessions = this.ctx.sessions;
|
|
301
367
|
const actx = sessions.scope(sessionId);
|
|
302
368
|
if (actx === void 0) throw new Error(`ui-model-selection: session "${String(sessionId)}" resolved no scope`);
|
|
303
369
|
const binding = sessions.binding(sessionId);
|
|
304
370
|
if (binding === void 0) throw new Error(`ui-model-selection: session "${String(sessionId)}" resolved no binding`);
|
|
371
|
+
const existing = live.directories.get(binding);
|
|
372
|
+
if (existing !== void 0) return existing;
|
|
305
373
|
const directory = new ModelDirectory(this.ctx.remote.session, sessionId, () => sessions.subagentAddress(sessionId) === void 0, this.catalog, binding.session.projections.faceOf("modelSelection"));
|
|
306
|
-
live.directories.set(
|
|
374
|
+
live.directories.set(binding, directory);
|
|
307
375
|
const conversation = this.ctx.get("conversation");
|
|
308
376
|
if (conversation !== void 0) {
|
|
309
377
|
const publish = () => {
|
|
@@ -320,7 +388,7 @@ window.__ModuleLoader__.load({
|
|
|
320
388
|
}
|
|
321
389
|
actx.effect(() => () => {
|
|
322
390
|
directory.dispose();
|
|
323
|
-
live.directories.delete(
|
|
391
|
+
live.directories.delete(binding);
|
|
324
392
|
}, "ui-model-selection: session directory");
|
|
325
393
|
return directory;
|
|
326
394
|
}
|
|
@@ -387,11 +455,17 @@ window.__ModuleLoader__.load({
|
|
|
387
455
|
* each drilling into its own list — the provider-grouped model list over
|
|
388
456
|
* the shared directory, and the effort levels. The trigger (313:14108's
|
|
389
457
|
* ToggleButton) shows both: model name + effort in the caption tone.
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
458
|
+
* While open, ↑/↓ move focus across the rows of the shown pane (wrapping; a
|
|
459
|
+
* step taken while the trigger still holds focus enters at the near end), Tab
|
|
460
|
+
* settles like Enter, and Escape and Shift+Tab leave a drilled pane first and
|
|
461
|
+
* otherwise close back to the trigger. A drilled pane hands focus to the row
|
|
462
|
+
* of the value in use, and returning to the root pane hands it back to the
|
|
463
|
+
* cell that opened it. Data and submission ride the SAME per-session
|
|
464
|
+
* ModelDirectory as the /model popup; exact-model reasoning metadata and the
|
|
465
|
+
* selected effort come from the Host rather than a client-owned vocabulary. A
|
|
466
|
+
* rejected selection announces through the shared transient Toast anchored to
|
|
467
|
+
* the composer card; the in-menu strip with Retry remains the catalog-load
|
|
468
|
+
* surface.
|
|
395
469
|
*/
|
|
396
470
|
/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */
|
|
397
471
|
const MEASURE_STYLE = {
|
|
@@ -457,6 +531,18 @@ window.__ModuleLoader__.load({
|
|
|
457
531
|
document.removeEventListener("mousedown", closeOutside);
|
|
458
532
|
};
|
|
459
533
|
}, [open]);
|
|
534
|
+
const paneFocus = (0, react.useRef)(null);
|
|
535
|
+
(0, react.useEffect)(() => {
|
|
536
|
+
const intent = paneFocus.current;
|
|
537
|
+
paneFocus.current = null;
|
|
538
|
+
if (!open || intent === null) return;
|
|
539
|
+
if (intent === "drill") {
|
|
540
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled) ?? triggerRef.current)?.focus();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const cell = itemRefs.current[intent === "effort" ? 1 : 0];
|
|
544
|
+
(cell !== null && cell !== void 0 && !cell.disabled ? cell : triggerRef.current)?.focus();
|
|
545
|
+
}, [open, pane]);
|
|
460
546
|
(0, react.useLayoutEffect)(() => {
|
|
461
547
|
if (!open) {
|
|
462
548
|
setMenuPos(null);
|
|
@@ -503,20 +589,48 @@ window.__ModuleLoader__.load({
|
|
|
503
589
|
triggerRef.current?.focus();
|
|
504
590
|
});
|
|
505
591
|
};
|
|
592
|
+
const drill = (next) => {
|
|
593
|
+
paneFocus.current = "drill";
|
|
594
|
+
setPane(next);
|
|
595
|
+
};
|
|
596
|
+
/** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
|
|
597
|
+
const back = (from) => {
|
|
598
|
+
paneFocus.current = from;
|
|
599
|
+
setPane("root");
|
|
600
|
+
};
|
|
506
601
|
const moveFocus = (offset) => {
|
|
507
602
|
const items = itemRefs.current.filter((item) => item !== null);
|
|
508
603
|
if (items.length === 0) return;
|
|
509
604
|
const active = items.findIndex((item) => item === document.activeElement);
|
|
510
|
-
items[
|
|
605
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
511
606
|
};
|
|
512
607
|
const onRootKeyDown = (event) => {
|
|
513
608
|
if (event.key === "Escape" && open) {
|
|
514
609
|
event.preventDefault();
|
|
515
|
-
if (pane !== "root")
|
|
610
|
+
if (pane !== "root") back(pane);
|
|
516
611
|
else close(true);
|
|
517
612
|
return;
|
|
518
613
|
}
|
|
519
614
|
if (!open) return;
|
|
615
|
+
if (event.key === "Tab") {
|
|
616
|
+
if (event.shiftKey) {
|
|
617
|
+
event.preventDefault();
|
|
618
|
+
if (pane !== "root") back(pane);
|
|
619
|
+
else close(true);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const focused = document.activeElement;
|
|
623
|
+
const rows = itemRefs.current.filter((item) => item !== null);
|
|
624
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
625
|
+
event.preventDefault();
|
|
626
|
+
focused.click();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (focused !== triggerRef.current) return;
|
|
630
|
+
event.preventDefault();
|
|
631
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows.find((item) => !item.disabled))?.focus();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
520
634
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
521
635
|
event.preventDefault();
|
|
522
636
|
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
@@ -526,19 +640,18 @@ window.__ModuleLoader__.load({
|
|
|
526
640
|
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return;
|
|
527
641
|
close();
|
|
528
642
|
};
|
|
529
|
-
const settleSelection = (
|
|
530
|
-
if (
|
|
643
|
+
const settleSelection = (result) => {
|
|
644
|
+
if (result === void 0) return;
|
|
645
|
+
if (result.ok) {
|
|
531
646
|
if (rootRef.current !== null) close(true);
|
|
532
647
|
return;
|
|
533
648
|
}
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
});
|
|
541
|
-
}
|
|
649
|
+
const { error } = result;
|
|
650
|
+
toastSeq.current += 1;
|
|
651
|
+
setToast({
|
|
652
|
+
seq: toastSeq.current,
|
|
653
|
+
text: error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${error.code}: ${error.message}` })
|
|
654
|
+
});
|
|
542
655
|
};
|
|
543
656
|
const choose = (selection) => {
|
|
544
657
|
if (state.current?.provider === selection.provider && state.current.model === selection.model) {
|
|
@@ -628,7 +741,7 @@ window.__ModuleLoader__.load({
|
|
|
628
741
|
role: "menuitem",
|
|
629
742
|
className: ModelSelect_module_css_default.cell,
|
|
630
743
|
onClick: () => {
|
|
631
|
-
|
|
744
|
+
drill("model");
|
|
632
745
|
},
|
|
633
746
|
children: [
|
|
634
747
|
(0, react_jsx_runtime.jsx)("span", {
|
|
@@ -647,7 +760,7 @@ window.__ModuleLoader__.load({
|
|
|
647
760
|
role: "menuitem",
|
|
648
761
|
className: ModelSelect_module_css_default.cell,
|
|
649
762
|
onClick: () => {
|
|
650
|
-
|
|
763
|
+
drill("effort");
|
|
651
764
|
},
|
|
652
765
|
children: [
|
|
653
766
|
(0, react_jsx_runtime.jsx)("span", {
|
|
@@ -793,6 +906,7 @@ window.__ModuleLoader__.load({
|
|
|
793
906
|
*/
|
|
794
907
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
795
908
|
const zh = {
|
|
909
|
+
"command.label": "模型",
|
|
796
910
|
"command.description": "选择本会话使用的模型",
|
|
797
911
|
"option.loadError": "目录加载失败:{message}",
|
|
798
912
|
"option.deepseekV4Flash.description": "快速、高效且经济;适合目标明确、常规或并行任务。",
|
|
@@ -808,6 +922,7 @@ window.__ModuleLoader__.load({
|
|
|
808
922
|
"effort.providerDefault": "Default",
|
|
809
923
|
"status.loading": "正在刷新模型列表…",
|
|
810
924
|
"error.action": "模型操作失败:{message}",
|
|
925
|
+
"error.sessionInUse": "当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。",
|
|
811
926
|
"action.reload": "重新加载",
|
|
812
927
|
"warning.groupLoad": "{name} 加载失败:{message}",
|
|
813
928
|
"empty.models": "没有可用的模型。",
|
|
@@ -816,6 +931,7 @@ window.__ModuleLoader__.load({
|
|
|
816
931
|
};
|
|
817
932
|
/** English dictionary, checked complete against the zh key set. */
|
|
818
933
|
const en = {
|
|
934
|
+
"command.label": "Model",
|
|
819
935
|
"command.description": "Select the model for this conversation",
|
|
820
936
|
"option.loadError": "Catalog failed to load: {message}",
|
|
821
937
|
"option.deepseekV4Flash.description": "Fast, efficient, and economical; suited to focused, routine, or parallel tasks.",
|
|
@@ -831,6 +947,7 @@ window.__ModuleLoader__.load({
|
|
|
831
947
|
"effort.providerDefault": "Default",
|
|
832
948
|
"status.loading": "Refreshing model list…",
|
|
833
949
|
"error.action": "Model operation failed: {message}",
|
|
950
|
+
"error.sessionInUse": "This session is already in use, possibly by another running DSH instance (such as dsh web or the desktop app). Quit other running DSH instances and try again.",
|
|
834
951
|
"action.reload": "Reload",
|
|
835
952
|
"warning.groupLoad": "{name} failed to load: {message}",
|
|
836
953
|
"empty.models": "No models available.",
|
|
@@ -918,7 +1035,9 @@ window.__ModuleLoader__.load({
|
|
|
918
1035
|
const sessions = scope.sessions;
|
|
919
1036
|
scope.effect(() => command.register({
|
|
920
1037
|
name: "model",
|
|
1038
|
+
label: () => t("command.label"),
|
|
921
1039
|
description: () => t("command.description"),
|
|
1040
|
+
icon: _deepseek_ai_dsh_client_ui_primitives.IconDataOutline16,
|
|
922
1041
|
available: (session) => sessions.subagentAddress(session.sessionId) === void 0,
|
|
923
1042
|
ui: {
|
|
924
1043
|
kind: "popupSelect",
|
|
@@ -931,7 +1050,11 @@ window.__ModuleLoader__.load({
|
|
|
931
1050
|
const directory = models.directoryFor(session.sessionId);
|
|
932
1051
|
const selection = selectionOf(directory.store.getSnapshot(), option.id);
|
|
933
1052
|
if (selection === void 0) throw new Error("this provider's catalog failed to load — pick a model from a loaded group");
|
|
934
|
-
await directory.select(selection);
|
|
1053
|
+
const result = await directory.select(selection);
|
|
1054
|
+
if (!result.ok) {
|
|
1055
|
+
if (result.error.code === "session/writer-held") throw new Error(t("error.sessionInUse"));
|
|
1056
|
+
throw result.error;
|
|
1057
|
+
}
|
|
935
1058
|
}
|
|
936
1059
|
}
|
|
937
1060
|
}), "ui-model-selection: /model contribution");
|
|
@@ -951,7 +1074,7 @@ window.__ModuleLoader__.load({
|
|
|
951
1074
|
load: () => {
|
|
952
1075
|
if (available) directory.load().catch(() => {});
|
|
953
1076
|
},
|
|
954
|
-
select: (selection) => available ? directory.select(selection)
|
|
1077
|
+
select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
|
|
955
1078
|
};
|
|
956
1079
|
}
|
|
957
1080
|
}, ModelSelect));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ModelCatalogFailure, ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
|
|
8
8
|
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
|
|
9
|
-
import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol';
|
|
9
|
+
import type { RemoteResult, TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol';
|
|
10
10
|
import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
11
11
|
import type { ModelCatalogDirectory } from './catalog.ts';
|
|
12
12
|
/** Directory snapshot both entries render from. */
|
|
@@ -61,10 +61,11 @@ export declare class ModelDirectory {
|
|
|
61
61
|
/**
|
|
62
62
|
* Select the complete provider/model/reasoning selection. The durable
|
|
63
63
|
* projection frame updates the shared current; failures surface on the store
|
|
64
|
-
* and
|
|
64
|
+
* and return with the operation so each entry can present its own failure.
|
|
65
65
|
* @param selection - provider, provider-owned model id, and optional adapter-owned effort.
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
* @returns the selection outcome, including the original Remote failure.
|
|
67
|
+
*/
|
|
68
|
+
select(selection: ModelSelection): Promise<RemoteResult<void>>;
|
|
68
69
|
/**
|
|
69
70
|
* Invalidate an in-flight selection response from the previous Host generation.
|
|
70
71
|
*/
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
11
11
|
export declare const zh: {
|
|
12
|
+
'command.label': string;
|
|
12
13
|
'command.description': string;
|
|
13
14
|
'option.loadError': string;
|
|
14
15
|
'option.deepseekV4Flash.description': string;
|
|
@@ -24,6 +25,7 @@ export declare const zh: {
|
|
|
24
25
|
'effort.providerDefault': string;
|
|
25
26
|
'status.loading': string;
|
|
26
27
|
'error.action': string;
|
|
28
|
+
'error.sessionInUse': string;
|
|
27
29
|
'action.reload': string;
|
|
28
30
|
'warning.groupLoad': string;
|
|
29
31
|
'empty.models': string;
|
|
@@ -34,6 +36,7 @@ export declare const zh: {
|
|
|
34
36
|
export type ModelKey = keyof typeof zh;
|
|
35
37
|
/** English dictionary, checked complete against the zh key set. */
|
|
36
38
|
export declare const en: {
|
|
39
|
+
'command.label': string;
|
|
37
40
|
'command.description': string;
|
|
38
41
|
'option.loadError': string;
|
|
39
42
|
'option.deepseekV4Flash.description': string;
|
|
@@ -49,6 +52,7 @@ export declare const en: {
|
|
|
49
52
|
'effort.providerDefault': string;
|
|
50
53
|
'status.loading': string;
|
|
51
54
|
'error.action': string;
|
|
55
|
+
'error.sessionInUse': string;
|
|
52
56
|
'action.reload': string;
|
|
53
57
|
'warning.groupLoad': string;
|
|
54
58
|
'empty.models': string;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* entry; this package only contributes the single occupant, so no SlotMap
|
|
5
5
|
* merge lives here.
|
|
6
6
|
*/
|
|
7
|
+
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
7
8
|
import type { ModelSelection } from '@deepseek-ai/dsh-api-remotes/client';
|
|
8
9
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
9
10
|
import type { ModelDirectoryState } from './directory.ts';
|
|
@@ -18,8 +19,8 @@ export interface ModelSelectInjected {
|
|
|
18
19
|
/**
|
|
19
20
|
* Select a complete provider/model/reasoning selection.
|
|
20
21
|
* @param selection - model selection and optional adapter-owned effort.
|
|
21
|
-
* @returns
|
|
22
|
+
* @returns the Host outcome, or undefined when this Session cannot select a model.
|
|
22
23
|
*/
|
|
23
|
-
select: (selection: ModelSelection) => Promise<
|
|
24
|
+
select: (selection: ModelSelection) => Promise<RemoteResult<void> | undefined>;
|
|
24
25
|
}
|
|
25
26
|
//# sourceMappingURL=slots.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-model-selection",
|
|
3
3
|
"description": "Model selection over the shared model catalog, Session projection, and session.selectModel",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -46,21 +46,22 @@
|
|
|
46
46
|
"react-dom": "^18.2.0",
|
|
47
47
|
"@types/react-dom": "~18.3.0",
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-client-test-runtime": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-commands": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
49
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.6-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.6-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.1.6-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.6-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.6-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.2",
|
|
58
58
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-session": "^0.1.
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "^0.1.6-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.2"
|
|
64
65
|
},
|
|
65
66
|
"files": [
|
|
66
67
|
"lib/index.js",
|