@deepseek-ai/dsh-client-ui-model-selection 0.1.6-alpha.1 → 0.1.7-alpha.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.i18n.yaml +2 -2
- package/README.md +9 -1
- package/README.zh.md +9 -1
- package/lib/client.js +164 -42
- package/lib/types/client/directory.d.ts +5 -4
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/slots.d.ts +3 -2
- package/package.json +18 -17
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: 37253cf04242275f22d646da0e65cfd44f4fefbf
|
|
6
|
+
README.zh.md: 4e339e6a6c6db8c3125e6ccc19c0fc3af9947f88
|
package/README.md
CHANGED
|
@@ -25,16 +25,24 @@ 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
|
|
|
32
32
|
Models stay grouped by provider. The composer menu shows model and effort names only. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
|
|
33
33
|
|
|
34
|
+
The composer replaces the model and effort text with the Models icon when the expanded controls cannot share one line, and restores the text when space permits. The full selection remains available in the trigger's accessible name, tooltip, and menu.
|
|
35
|
+
|
|
34
36
|
### Unroutable sessions
|
|
35
37
|
|
|
36
38
|
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
39
|
|
|
40
|
+
Only the current Client binding's directory can publish its composer block. Cleanup from an older binding preserves a replacement directory's block; without a replacement directory, cleanup removes the obsolete block.
|
|
41
|
+
|
|
42
|
+
### Selection failures
|
|
43
|
+
|
|
44
|
+
When another writer owns the Session, model-selection failures tell the user to quit other running DSH instances and retry.
|
|
45
|
+
|
|
38
46
|
-----
|
|
39
47
|
|
|
40
48
|
<a id="understand-the-implementation"></a>
|
package/README.zh.md
CHANGED
|
@@ -25,16 +25,24 @@ 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
|
|
|
32
32
|
模型按提供方分组。composer 菜单只显示模型与推理强度名称。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
|
|
33
33
|
|
|
34
|
+
展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。
|
|
35
|
+
|
|
34
36
|
### 不可路由的会话
|
|
35
37
|
|
|
36
38
|
当 Host 报告没有适配器服务该会话的路由时,本插件注册一个 composer 阻塞块,输入框随之停用并显示本插件自己的文案;恢复后无需重新加载即清除。首次加载之前或加载失败之后的 `null` 绝不阻断;目录成员关系同样不阻断——一条仍在服务、只是不公布该模型的路由不在分组里,却可用。
|
|
37
39
|
|
|
40
|
+
只有当前 Client binding 的目录可以发布对应的 composer 阻塞块。旧 binding 的清理保留后继目录的阻塞块;若尚无后继目录,则移除已失效的阻塞块。
|
|
41
|
+
|
|
42
|
+
### 选择失败
|
|
43
|
+
|
|
44
|
+
当会话被其他写句柄占用时,模型选择失败提示用户退出其他正在运行的 DSH 后重试。
|
|
45
|
+
|
|
38
46
|
-----
|
|
39
47
|
|
|
40
48
|
<a id="understand-the-implementation"></a>
|
package/lib/client.js
CHANGED
|
@@ -10,6 +10,69 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
11
|
let react = require("react");
|
|
12
12
|
let react_dom = require("react-dom");
|
|
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,18 +363,19 @@ 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 = () => {
|
|
378
|
+
if (sessions.binding(sessionId) !== binding) return;
|
|
310
379
|
conversation.blocks.set(sessionId, directory.store.getSnapshot().routable === false ? { reason: this.blockReason() } : void 0);
|
|
311
380
|
};
|
|
312
381
|
publish();
|
|
@@ -314,13 +383,15 @@ window.__ModuleLoader__.load({
|
|
|
314
383
|
const stop = directory.store.subscribe(publish);
|
|
315
384
|
return () => {
|
|
316
385
|
stop();
|
|
386
|
+
const current = sessions.binding(sessionId);
|
|
387
|
+
if (current !== void 0 && current !== binding && live.directories.get(current) !== void 0) return;
|
|
317
388
|
conversation.blocks.set(sessionId, void 0);
|
|
318
389
|
};
|
|
319
390
|
}, "ui-model-selection: composer block");
|
|
320
391
|
}
|
|
321
392
|
actx.effect(() => () => {
|
|
322
393
|
directory.dispose();
|
|
323
|
-
live.directories.delete(
|
|
394
|
+
live.directories.delete(binding);
|
|
324
395
|
}, "ui-model-selection: session directory");
|
|
325
396
|
return directory;
|
|
326
397
|
}
|
|
@@ -342,7 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
342
413
|
}
|
|
343
414
|
//#endregion
|
|
344
415
|
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-model-selection/src/client/ModelSelect.module.css.mjs
|
|
345
|
-
const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-caption);flex-shrink:1000;overflow:hidden}._7KE1Ra_triggerIcon{
|
|
416
|
+
const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-caption);flex-shrink:1000;overflow:hidden}._7KE1Ra_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}._7KE1Ra_triggerLabel,._7KE1Ra_triggerEffort{display:var(--dsh-composer-model-text-display,block)}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{z-index:1100;background:var(--dsw-specific-menu);width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:16px;flex-direction:column;padding:3px;display:flex;position:fixed;overflow:hidden}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:12px;line-height:18px}._7KE1Ra_error,._7KE1Ra_warning{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:7px;justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:3px;padding:6px 7px;font-size:11px;line-height:16px;display:flex}._7KE1Ra_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}._7KE1Ra_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}._7KE1Ra_groups{min-height:0;overflow-y:auto}._7KE1Ra_group+._7KE1Ra_group{margin-top:3px}._7KE1Ra_groupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:4px 7px 2px;font-size:11px;font-weight:500;line-height:16px;position:sticky;top:0}._7KE1Ra_option{box-sizing:border-box;width:auto;min-width:100%;min-height:34px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:8px;outline:none;align-items:center;gap:6px;padding:5px 7px;display:flex}._7KE1Ra_option:hover:not(:disabled),._7KE1Ra_option:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_selected{background:0 0}._7KE1Ra_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}._7KE1Ra_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:18px;overflow:hidden}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 14px;place-items:center;display:grid}._7KE1Ra_check svg{width:14px;height:14px}._7KE1Ra_cell{box-sizing:border-box;width:auto;min-width:100%;height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:8px;align-items:center;gap:6px;padding:0 8px;font-size:13px;line-height:20px;display:flex}._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_cellLabel{white-space:nowrap;flex:none}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}._7KE1Ra_cellChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary);flex:none}";
|
|
346
417
|
const tagId = "@deepseek-ai/dsh-client-ui-model-selection/ModelSelect.module.css";
|
|
347
418
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
348
419
|
const tag = document.createElement("style");
|
|
@@ -387,11 +458,17 @@ window.__ModuleLoader__.load({
|
|
|
387
458
|
* each drilling into its own list — the provider-grouped model list over
|
|
388
459
|
* the shared directory, and the effort levels. The trigger (313:14108's
|
|
389
460
|
* ToggleButton) shows both: model name + effort in the caption tone.
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
461
|
+
* While open, ↑/↓ move focus across the rows of the shown pane (wrapping; a
|
|
462
|
+
* step taken while the trigger still holds focus enters at the near end), Tab
|
|
463
|
+
* settles like Enter, and Escape and Shift+Tab leave a drilled pane first and
|
|
464
|
+
* otherwise close back to the trigger. A drilled pane hands focus to the row
|
|
465
|
+
* of the value in use, and returning to the root pane hands it back to the
|
|
466
|
+
* cell that opened it. Data and submission ride the SAME per-session
|
|
467
|
+
* ModelDirectory as the /model popup; exact-model reasoning metadata and the
|
|
468
|
+
* selected effort come from the Host rather than a client-owned vocabulary. A
|
|
469
|
+
* rejected selection announces through the shared transient Toast anchored to
|
|
470
|
+
* the composer card; the in-menu strip with Retry remains the catalog-load
|
|
471
|
+
* surface.
|
|
395
472
|
*/
|
|
396
473
|
/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */
|
|
397
474
|
const MEASURE_STYLE = {
|
|
@@ -457,6 +534,18 @@ window.__ModuleLoader__.load({
|
|
|
457
534
|
document.removeEventListener("mousedown", closeOutside);
|
|
458
535
|
};
|
|
459
536
|
}, [open]);
|
|
537
|
+
const paneFocus = (0, react.useRef)(null);
|
|
538
|
+
(0, react.useEffect)(() => {
|
|
539
|
+
const intent = paneFocus.current;
|
|
540
|
+
paneFocus.current = null;
|
|
541
|
+
if (!open || intent === null) return;
|
|
542
|
+
if (intent === "drill") {
|
|
543
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled) ?? triggerRef.current)?.focus();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const cell = itemRefs.current[intent === "effort" ? 1 : 0];
|
|
547
|
+
(cell !== null && cell !== void 0 && !cell.disabled ? cell : triggerRef.current)?.focus();
|
|
548
|
+
}, [open, pane]);
|
|
460
549
|
(0, react.useLayoutEffect)(() => {
|
|
461
550
|
if (!open) {
|
|
462
551
|
setMenuPos(null);
|
|
@@ -503,20 +592,48 @@ window.__ModuleLoader__.load({
|
|
|
503
592
|
triggerRef.current?.focus();
|
|
504
593
|
});
|
|
505
594
|
};
|
|
595
|
+
const drill = (next) => {
|
|
596
|
+
paneFocus.current = "drill";
|
|
597
|
+
setPane(next);
|
|
598
|
+
};
|
|
599
|
+
/** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
|
|
600
|
+
const back = (from) => {
|
|
601
|
+
paneFocus.current = from;
|
|
602
|
+
setPane("root");
|
|
603
|
+
};
|
|
506
604
|
const moveFocus = (offset) => {
|
|
507
605
|
const items = itemRefs.current.filter((item) => item !== null);
|
|
508
606
|
if (items.length === 0) return;
|
|
509
607
|
const active = items.findIndex((item) => item === document.activeElement);
|
|
510
|
-
items[
|
|
608
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
511
609
|
};
|
|
512
610
|
const onRootKeyDown = (event) => {
|
|
513
611
|
if (event.key === "Escape" && open) {
|
|
514
612
|
event.preventDefault();
|
|
515
|
-
if (pane !== "root")
|
|
613
|
+
if (pane !== "root") back(pane);
|
|
516
614
|
else close(true);
|
|
517
615
|
return;
|
|
518
616
|
}
|
|
519
617
|
if (!open) return;
|
|
618
|
+
if (event.key === "Tab") {
|
|
619
|
+
if (event.shiftKey) {
|
|
620
|
+
event.preventDefault();
|
|
621
|
+
if (pane !== "root") back(pane);
|
|
622
|
+
else close(true);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const focused = document.activeElement;
|
|
626
|
+
const rows = itemRefs.current.filter((item) => item !== null);
|
|
627
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
focused.click();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (focused !== triggerRef.current) return;
|
|
633
|
+
event.preventDefault();
|
|
634
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows.find((item) => !item.disabled))?.focus();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
520
637
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
521
638
|
event.preventDefault();
|
|
522
639
|
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
@@ -526,19 +643,18 @@ window.__ModuleLoader__.load({
|
|
|
526
643
|
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return;
|
|
527
644
|
close();
|
|
528
645
|
};
|
|
529
|
-
const settleSelection = (
|
|
530
|
-
if (
|
|
646
|
+
const settleSelection = (result) => {
|
|
647
|
+
if (result === void 0) return;
|
|
648
|
+
if (result.ok) {
|
|
531
649
|
if (rootRef.current !== null) close(true);
|
|
532
650
|
return;
|
|
533
651
|
}
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
});
|
|
541
|
-
}
|
|
652
|
+
const { error } = result;
|
|
653
|
+
toastSeq.current += 1;
|
|
654
|
+
setToast({
|
|
655
|
+
seq: toastSeq.current,
|
|
656
|
+
text: error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${error.code}: ${error.message}` })
|
|
657
|
+
});
|
|
542
658
|
};
|
|
543
659
|
const choose = (selection) => {
|
|
544
660
|
if (state.current?.provider === selection.provider && state.current.model === selection.model) {
|
|
@@ -598,7 +714,7 @@ window.__ModuleLoader__.load({
|
|
|
598
714
|
else show();
|
|
599
715
|
},
|
|
600
716
|
children: [
|
|
601
|
-
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
717
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, {
|
|
602
718
|
className: ModelSelect_module_css_default.triggerIcon,
|
|
603
719
|
size: 16
|
|
604
720
|
}),
|
|
@@ -610,7 +726,7 @@ window.__ModuleLoader__.load({
|
|
|
610
726
|
className: ModelSelect_module_css_default.triggerEffort,
|
|
611
727
|
children: effortLabel
|
|
612
728
|
}),
|
|
613
|
-
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
729
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: clsx(ModelSelect_module_css_default.chevron, open && ModelSelect_module_css_default.chevronOpen) })
|
|
614
730
|
]
|
|
615
731
|
}),
|
|
616
732
|
open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -628,7 +744,7 @@ window.__ModuleLoader__.load({
|
|
|
628
744
|
role: "menuitem",
|
|
629
745
|
className: ModelSelect_module_css_default.cell,
|
|
630
746
|
onClick: () => {
|
|
631
|
-
|
|
747
|
+
drill("model");
|
|
632
748
|
},
|
|
633
749
|
children: [
|
|
634
750
|
(0, react_jsx_runtime.jsx)("span", {
|
|
@@ -639,7 +755,7 @@ window.__ModuleLoader__.load({
|
|
|
639
755
|
className: ModelSelect_module_css_default.cellValue,
|
|
640
756
|
children: modelLabel
|
|
641
757
|
}),
|
|
642
|
-
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
758
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: ModelSelect_module_css_default.cellChevron })
|
|
643
759
|
]
|
|
644
760
|
}), reasoning !== void 0 && (0, react_jsx_runtime.jsxs)("button", {
|
|
645
761
|
ref: itemRef(),
|
|
@@ -647,7 +763,7 @@ window.__ModuleLoader__.load({
|
|
|
647
763
|
role: "menuitem",
|
|
648
764
|
className: ModelSelect_module_css_default.cell,
|
|
649
765
|
onClick: () => {
|
|
650
|
-
|
|
766
|
+
drill("effort");
|
|
651
767
|
},
|
|
652
768
|
children: [
|
|
653
769
|
(0, react_jsx_runtime.jsx)("span", {
|
|
@@ -658,7 +774,7 @@ window.__ModuleLoader__.load({
|
|
|
658
774
|
className: ModelSelect_module_css_default.cellValue,
|
|
659
775
|
children: effortLabel
|
|
660
776
|
}),
|
|
661
|
-
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
777
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: ModelSelect_module_css_default.cellChevron })
|
|
662
778
|
]
|
|
663
779
|
})] }),
|
|
664
780
|
pane === "model" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
@@ -723,7 +839,7 @@ window.__ModuleLoader__.load({
|
|
|
723
839
|
})
|
|
724
840
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
725
841
|
className: ModelSelect_module_css_default.check,
|
|
726
|
-
children: selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
842
|
+
children: selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
727
843
|
})]
|
|
728
844
|
}, model.id);
|
|
729
845
|
})]
|
|
@@ -764,14 +880,14 @@ window.__ModuleLoader__.load({
|
|
|
764
880
|
})
|
|
765
881
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
766
882
|
className: ModelSelect_module_css_default.check,
|
|
767
|
-
children: effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
883
|
+
children: effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
768
884
|
})]
|
|
769
885
|
}, level.key))] })
|
|
770
886
|
]
|
|
771
887
|
}), document.body),
|
|
772
888
|
toast !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
773
889
|
text: toast.text,
|
|
774
|
-
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
890
|
+
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
|
|
775
891
|
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
776
892
|
onDone: () => {
|
|
777
893
|
setToast(null);
|
|
@@ -809,6 +925,7 @@ window.__ModuleLoader__.load({
|
|
|
809
925
|
"effort.providerDefault": "Default",
|
|
810
926
|
"status.loading": "正在刷新模型列表…",
|
|
811
927
|
"error.action": "模型操作失败:{message}",
|
|
928
|
+
"error.sessionInUse": "当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。",
|
|
812
929
|
"action.reload": "重新加载",
|
|
813
930
|
"warning.groupLoad": "{name} 加载失败:{message}",
|
|
814
931
|
"empty.models": "没有可用的模型。",
|
|
@@ -833,6 +950,7 @@ window.__ModuleLoader__.load({
|
|
|
833
950
|
"effort.providerDefault": "Default",
|
|
834
951
|
"status.loading": "Refreshing model list…",
|
|
835
952
|
"error.action": "Model operation failed: {message}",
|
|
953
|
+
"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.",
|
|
836
954
|
"action.reload": "Reload",
|
|
837
955
|
"warning.groupLoad": "{name} failed to load: {message}",
|
|
838
956
|
"empty.models": "No models available.",
|
|
@@ -922,7 +1040,7 @@ window.__ModuleLoader__.load({
|
|
|
922
1040
|
name: "model",
|
|
923
1041
|
label: () => t("command.label"),
|
|
924
1042
|
description: () => t("command.description"),
|
|
925
|
-
icon: _deepseek_ai_dsh_client_ui_primitives.
|
|
1043
|
+
icon: _deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular,
|
|
926
1044
|
available: (session) => sessions.subagentAddress(session.sessionId) === void 0,
|
|
927
1045
|
ui: {
|
|
928
1046
|
kind: "popupSelect",
|
|
@@ -935,7 +1053,11 @@ window.__ModuleLoader__.load({
|
|
|
935
1053
|
const directory = models.directoryFor(session.sessionId);
|
|
936
1054
|
const selection = selectionOf(directory.store.getSnapshot(), option.id);
|
|
937
1055
|
if (selection === void 0) throw new Error("this provider's catalog failed to load — pick a model from a loaded group");
|
|
938
|
-
await directory.select(selection);
|
|
1056
|
+
const result = await directory.select(selection);
|
|
1057
|
+
if (!result.ok) {
|
|
1058
|
+
if (result.error.code === "session/writer-held") throw new Error(t("error.sessionInUse"));
|
|
1059
|
+
throw result.error;
|
|
1060
|
+
}
|
|
939
1061
|
}
|
|
940
1062
|
}
|
|
941
1063
|
}), "ui-model-selection: /model contribution");
|
|
@@ -955,7 +1077,7 @@ window.__ModuleLoader__.load({
|
|
|
955
1077
|
load: () => {
|
|
956
1078
|
if (available) directory.load().catch(() => {});
|
|
957
1079
|
},
|
|
958
|
-
select: (selection) => available ? directory.select(selection)
|
|
1080
|
+
select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
|
|
959
1081
|
};
|
|
960
1082
|
}
|
|
961
1083
|
}, 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
|
*/
|
|
@@ -25,6 +25,7 @@ export declare const zh: {
|
|
|
25
25
|
'effort.providerDefault': string;
|
|
26
26
|
'status.loading': string;
|
|
27
27
|
'error.action': string;
|
|
28
|
+
'error.sessionInUse': string;
|
|
28
29
|
'action.reload': string;
|
|
29
30
|
'warning.groupLoad': string;
|
|
30
31
|
'empty.models': string;
|
|
@@ -51,6 +52,7 @@ export declare const en: {
|
|
|
51
52
|
'effort.providerDefault': string;
|
|
52
53
|
'status.loading': string;
|
|
53
54
|
'error.action': string;
|
|
55
|
+
'error.sessionInUse': string;
|
|
54
56
|
'action.reload': string;
|
|
55
57
|
'warning.groupLoad': string;
|
|
56
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.7-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
41
|
+
"@deepseek-ai/cordis": "^4.0.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "~18.3.1",
|
|
@@ -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-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-client-
|
|
52
|
-
"@deepseek-ai/dsh-client-
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/
|
|
58
|
-
"@deepseek-ai/
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
49
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.7-alpha.1",
|
|
50
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.1.7-alpha.1",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.7-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-alpha.1",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.7-alpha.1",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
|
|
56
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.7-alpha.1",
|
|
57
|
+
"@deepseek-ai/cordis": "^4.0.3",
|
|
58
|
+
"@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1",
|
|
59
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.7-alpha.1",
|
|
60
|
+
"@deepseek-ai/dsh-util-values": "^0.1.7-alpha.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.7-alpha.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-alpha.1",
|
|
63
|
+
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1"
|
|
64
65
|
},
|
|
65
66
|
"files": [
|
|
66
67
|
"lib/index.js",
|