@crazx/dsh-client-ui-model-selection 0.1.0-rc.7.zw.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +29 -0
- package/README.zh.md +29 -0
- package/lib/client.js +804 -0
- package/lib/index.js +11 -0
- package/lib/invariant.js +24 -0
- package/lib/types/client/ModelSelect.d.ts +12 -0
- package/lib/types/client/directory.d.ts +70 -0
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/locales.d.ts +52 -0
- package/lib/types/client/service.d.ts +45 -0
- package/lib/types/client/slots.d.ts +25 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/ui-model-selection/README.md
|
|
5
|
+
README.md: 929d23ad4af1fe41e1cc7398ef432e73ca2a7a94
|
|
6
|
+
README.zh.md: 23e786d9e490b0739498a3e83703b42ebc345cbd
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-client-ui-model-selection
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Model selection plugin, browser half: TWO entries over ONE per-session directory owned by `ModelDirectoryResolver` (`ctx.modelDirectories`). For ordinary sessions, 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. The compact composer trigger opens a two-level Model/Effort menu: models stay provider-grouped, while the selected exact model supplies its adapter-owned effort names, descriptions, and default. `/model` applies the selected model's default effort, and the composer can then choose any advertised effort.
|
|
6
|
+
|
|
7
|
+
The Host-reported provider/model/reasoning `ModelSelection` is the single selection fact, but it is echoed only when the exact provider/model pair remains in the advertised groups; an absent catalog row leaves the routable selection intact while the trigger prompts `Select model`, no stale row is synthesized, and no Effort row is shown until the user picks an advertised model. 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. Provider-local metadata failures list inline while usable groups stay selectable, and selection failures retain the prior selection and directory.
|
|
8
|
+
|
|
9
|
+
When the Host reports that no adapter serves the session's route (`session.models.routable`), this plugin raises a composer block through `ctx.conversation.blocks` and the input goes inert with this plugin's own copy; recovering clears it without a reload. It follows `routable` and nothing else: a `null` — before the first load, or after one failed — never blocks, or a slow Host would lock a working composer, and catalog membership never blocks either, because a route serving a model it stopped advertising is missing from the groups yet perfectly usable. The trigger's own `Select model` fallback still covers that case, which is display, not a gate.
|
|
10
|
+
|
|
11
|
+
Directories are per-session, resolved lazily through `ctx.modelDirectories.directoryFor(sessionId)`, and disposed with the session scope. Addressed subagent sessions expose neither entry, and their directory rejects loads, selections, and reconnect refreshes, because ordinary Agent-bound model RPCs would activate persisted child history outside the direct-parent continuation path.
|
|
12
|
+
|
|
13
|
+
Every resident directory refetches directly on forwarded `llm/adapters-updated` and `settings/document-updated` owner events. Provider topology, provider catalogs, and the default selection therefore converge without the Host or client runtime deriving a separate model-change alias.
|
|
14
|
+
|
|
15
|
+
The `/client` exports are the plugin body (`apply`/`inject`), `ModelDirectoryResolver`, `ModelDirectory` with its state fields, and the seat's injected face type.
|
|
16
|
+
|
|
17
|
+
## Model Experience
|
|
18
|
+
|
|
19
|
+
Indirectly, through the `session.selectModel` RPC available to ordinary sessions, both entries submit the complete `ModelSelection` that the Host snapshots at the next prompt-assembly boundary, so the following request uses the selected provider, model, and effort while a running step keeps its assembled selection; the selection becomes durable only when the existing request header records a request that consumes it, and menu interaction adds no prompt content.
|
|
20
|
+
|
|
21
|
+
#### KV Cache effect
|
|
22
|
+
|
|
23
|
+
Switching the route can reduce or invalidate provider-side cache reuse for subsequent requests; the prompt prefix itself is untouched.
|
|
24
|
+
|
|
25
|
+
## Known Limitations and Deferred Work
|
|
26
|
+
|
|
27
|
+
- **No create-time or addressed-subagent selection** — both entries require an existing ordinary session's Agent; there is no draft-phase model choice to fold into session creation, and subagent continuation deliberately exposes no independent model-selection contract.
|
|
28
|
+
- **Directory names are presentation-only** — selection and persistence use provider/model/effort ids; a provider whose catalog or exact-model metadata lookup fails lists as an unselectable failure row until reload.
|
|
29
|
+
- **No arbitrary effort input** — the composer offers only the exact model's adapter-advertised levels; an adapter without reasoning metadata leaves the Effort row absent.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-client-ui-model-selection
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
模型选择插件(浏览器侧):**两个入口共用一份会话级目录**,由 `ModelDirectoryResolver`(`ctx.modelDirectories`)持有。对于普通会话,`/model` popupSelect 贡献项(经 `ctx.commandUi` 注册)与 composer 的具名 `conversation.input.model` slot 都通过同一个 `ModelDirectory` 实例,经 `session.models` 加载会话的建议目录,并经 `session.selectModel` 提交。紧凑型 composer 触发器会打开两级 Model/Effort 菜单:模型仍按提供方分组,所选具体模型则提供由其适配器持有的推理强度名称、说明和默认值。`/model` 应用所选模型的默认推理强度,composer 随后可以选择任一已公布的推理强度。
|
|
6
|
+
|
|
7
|
+
Host 报告的 `ModelSelection` 是唯一的选择事实,其中包含提供方、模型与推理(reasoning)强度;但只有当该提供方/模型对仍在已公布分组中时才会回显。目录行缺席时,可路由的选择保持不变,但触发器会提示 `Select model`;系统不会合成陈旧行,且在用户选择已公布的模型之前不会显示 Effort 行。目录加载与选择共享一个代次计数器,旧响应不会覆盖新结果;连接重置会丢弃所有常驻目录投影,并在显示前重新拉取 Host 恢复的选择。各提供方的元数据获取失败会内联列出,同时可用分组仍可选择;选择失败会保留先前的选择和目录。
|
|
8
|
+
|
|
9
|
+
当宿主报告没有适配器服务该会话的路由(`session.models.routable`)时,本插件经 `ctx.conversation.blocks` 注册一个 composer 阻塞块,输入框随之停用并显示本插件自己的文案;恢复后无需重新加载即自动清除。它只跟随 `routable`:`null`(首次加载之前,或加载失败之后)绝不阻断,否则一个慢的宿主就会锁死一个本来可用的 composer;目录成员关系同样不阻断,因为一条仍在服务、只是不再公布该模型的路由不在分组里,却完全可用。触发器自己的 `Select model` 回退仍然覆盖那种情形——那是显示,不是闸门。
|
|
10
|
+
|
|
11
|
+
目录按会话惰性解析(`ctx.modelDirectories.directoryFor(sessionId)`),随会话作用域一并 dispose(资源释放)。已寻址 subagent 会话不公开任一入口,其目录会拒绝加载、选择与重新连接刷新,因为绑定到 agent(智能体)的普通模型 RPC 会在直接 parent 继续执行路径之外激活持久化 child 历史。
|
|
12
|
+
|
|
13
|
+
每一份常驻目录都会直接在转发的 owner 事件 `llm/adapters-updated` 与 `settings/document-updated` 上重拉。因此提供方拓扑、提供方目录与默认选择都能收敛,Host 与 client runtime 无需再派生一个单独的模型变更别名。
|
|
14
|
+
|
|
15
|
+
`/client` 导出面为插件本体(`apply`/`inject`)、`ModelDirectoryResolver`、`ModelDirectory` 及其状态形状、slot 注入面类型。
|
|
16
|
+
|
|
17
|
+
## 模型体验
|
|
18
|
+
|
|
19
|
+
间接影响。两个入口都通过仅供普通会话使用的 `session.selectModel` RPC 提交完整的 `ModelSelection`;Host 会在下一次提示词组装边界对其进行快照,因此后续请求采用所选提供方、模型与推理强度,而运行中的步骤保留已组装选择。只有当现有请求头记录一次实际采用该选择的请求后,选择才会持久化;菜单交互不会添加提示词内容。
|
|
20
|
+
|
|
21
|
+
#### KV Cache 影响
|
|
22
|
+
|
|
23
|
+
切换路由可能减少提供方侧后续请求的缓存复用,或使其失效;提示词前缀本身不受影响。
|
|
24
|
+
|
|
25
|
+
## 已知限制与暂缓事项
|
|
26
|
+
|
|
27
|
+
- **无创建期或已寻址 subagent 选择**——两个入口都要求既有普通会话的 agent;没有可纳入会话创建的草稿阶段模型选择,subagent 继续执行也有意不公开独立的模型选择约定。
|
|
28
|
+
- **目录名仅供呈现**——选择与持久化使用提供方/模型/推理强度 id;目录查询或确切模型元数据查询失败的提供方以不可选失败行列出,重新加载前保持原样。
|
|
29
|
+
- **不能任意输入推理强度**——composer 仅提供确切模型由适配器公布的推理强度;适配器没有推理元数据时不显示 Effort 行。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-client-ui-model-selection",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
8
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let react = require("react");
|
|
11
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
12
|
+
//#region lib/types/client/directory.js
|
|
13
|
+
/** One session's shared directory controller; disposed with the session scope. */
|
|
14
|
+
var ModelDirectory = class {
|
|
15
|
+
sessions;
|
|
16
|
+
sessionId;
|
|
17
|
+
available;
|
|
18
|
+
/** The shared snapshot both entries render from (uSES-safe store). */
|
|
19
|
+
store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({
|
|
20
|
+
current: null,
|
|
21
|
+
routable: null,
|
|
22
|
+
groups: [],
|
|
23
|
+
failures: [],
|
|
24
|
+
status: "idle",
|
|
25
|
+
error: null
|
|
26
|
+
});
|
|
27
|
+
/** Latest operation wins; an older response never overwrites a newer one. */
|
|
28
|
+
generation = 0;
|
|
29
|
+
disposed = false;
|
|
30
|
+
/**
|
|
31
|
+
* @param sessions - the session wire face (captured from the plugin's root connection).
|
|
32
|
+
* @param sessionId - the owning session.
|
|
33
|
+
* @param available - whether this session may use Agent-bound model RPCs.
|
|
34
|
+
*/
|
|
35
|
+
constructor(sessions, sessionId, available) {
|
|
36
|
+
this.sessions = sessions;
|
|
37
|
+
this.sessionId = sessionId;
|
|
38
|
+
this.available = available;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refresh the advisory directory (both entries call this on open).
|
|
42
|
+
* Failure preserves the last good groups and current selection.
|
|
43
|
+
* @returns the fresh directory value.
|
|
44
|
+
*/
|
|
45
|
+
async load() {
|
|
46
|
+
this.assertAvailable();
|
|
47
|
+
const generation = ++this.generation;
|
|
48
|
+
this.store.update((s) => {
|
|
49
|
+
s.status = "loading";
|
|
50
|
+
s.error = null;
|
|
51
|
+
});
|
|
52
|
+
const { result } = await this.sessions.models({ sessionId: this.sessionId });
|
|
53
|
+
if (this.disposed || generation !== this.generation) {
|
|
54
|
+
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
55
|
+
return result.value;
|
|
56
|
+
}
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
this.store.update((s) => {
|
|
59
|
+
s.status = "error";
|
|
60
|
+
s.error = `${result.error.code}: ${result.error.message}`;
|
|
61
|
+
});
|
|
62
|
+
throw new Error(`session.models failed: ${result.error.code}: ${result.error.message}`);
|
|
63
|
+
}
|
|
64
|
+
const { current, routable, groups, failures } = result.value;
|
|
65
|
+
this.store.update((s) => {
|
|
66
|
+
s.current = current;
|
|
67
|
+
s.routable = routable;
|
|
68
|
+
s.groups = groups;
|
|
69
|
+
s.failures = failures;
|
|
70
|
+
s.status = "ready";
|
|
71
|
+
s.error = null;
|
|
72
|
+
});
|
|
73
|
+
return result.value;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Select the complete provider/model/reasoning selection (both entries submit through here). Success
|
|
77
|
+
* updates the shared current; failure surfaces on the store and throws so
|
|
78
|
+
* each entry's own retry surface engages.
|
|
79
|
+
* @param selection - provider, provider-owned model id, and optional adapter-owned effort.
|
|
80
|
+
*/
|
|
81
|
+
async select(selection) {
|
|
82
|
+
this.assertAvailable();
|
|
83
|
+
const generation = ++this.generation;
|
|
84
|
+
this.store.update((s) => {
|
|
85
|
+
s.status = "selecting";
|
|
86
|
+
s.error = null;
|
|
87
|
+
});
|
|
88
|
+
const { result } = await this.sessions.selectModel({
|
|
89
|
+
sessionId: this.sessionId,
|
|
90
|
+
provider: selection.provider,
|
|
91
|
+
model: selection.model,
|
|
92
|
+
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
|
|
93
|
+
});
|
|
94
|
+
if (this.disposed || generation !== this.generation) {
|
|
95
|
+
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
this.store.update((s) => {
|
|
100
|
+
s.status = "error";
|
|
101
|
+
s.error = `${result.error.code}: ${result.error.message}`;
|
|
102
|
+
});
|
|
103
|
+
throw new Error(`session.selectModel failed: ${result.error.code}: ${result.error.message}`);
|
|
104
|
+
}
|
|
105
|
+
this.store.update((s) => {
|
|
106
|
+
s.current = result.value.selected;
|
|
107
|
+
s.routable = true;
|
|
108
|
+
s.status = "ready";
|
|
109
|
+
s.error = null;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Drop the previous Host generation's projection and repull it. Clearing
|
|
114
|
+
* first prevents an unconsumed process-local selection from being displayed
|
|
115
|
+
* while the restarted Host has restored the last logged model selection.
|
|
116
|
+
*/
|
|
117
|
+
resetConnected() {
|
|
118
|
+
if (this.disposed) return;
|
|
119
|
+
++this.generation;
|
|
120
|
+
this.store.update((s) => {
|
|
121
|
+
s.current = null;
|
|
122
|
+
s.routable = null;
|
|
123
|
+
s.groups = [];
|
|
124
|
+
s.failures = [];
|
|
125
|
+
s.status = "idle";
|
|
126
|
+
s.error = null;
|
|
127
|
+
});
|
|
128
|
+
if (!this.available()) return;
|
|
129
|
+
this.load().catch(() => {});
|
|
130
|
+
}
|
|
131
|
+
/** Scope teardown: late settlements lose write access to the store. */
|
|
132
|
+
dispose() {
|
|
133
|
+
this.disposed = true;
|
|
134
|
+
}
|
|
135
|
+
assertAvailable() {
|
|
136
|
+
if (!this.available()) throw new Error("model selection is unavailable for addressed subagent sessions");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region lib/types/client/service.js
|
|
141
|
+
/**
|
|
142
|
+
* ModelDirectoryResolver (`ctx.modelDirectories`): the root owner of per-session
|
|
143
|
+
* {@link ModelDirectory} instances. Both selection entries (the /model popup
|
|
144
|
+
* and the composer model seat) resolve their session's directory through
|
|
145
|
+
* this service, which is what makes the dual entry one shared state.
|
|
146
|
+
*
|
|
147
|
+
* Per-session storage follows the client service pattern (InputTriggerService /
|
|
148
|
+
* CommandUiRuntime): a lazy service-internal map whose entry is deleted by the
|
|
149
|
+
* owning scope's disposer. The host `dsh-scope` ScopedLayers registry does
|
|
150
|
+
* does not belong here: it derives scope from the host carrier mechanism
|
|
151
|
+
* (object-keyed), while client scopes tag contexts with branded SessionId
|
|
152
|
+
* strings, and it models global+shadow named registries — this is a
|
|
153
|
+
* per-session singleton with no global layer to merge.
|
|
154
|
+
*/
|
|
155
|
+
/** The `ctx.modelDirectories` session model-selection service. */
|
|
156
|
+
var ModelDirectoryResolver = class extends _deepseek_ai_cordis.Service {
|
|
157
|
+
static inject = [
|
|
158
|
+
"connection",
|
|
159
|
+
"sessions",
|
|
160
|
+
"remote"
|
|
161
|
+
];
|
|
162
|
+
live = { directories: /* @__PURE__ */ new Map() };
|
|
163
|
+
/** Localized composer-block copy; this plugin owns the string it raises. */
|
|
164
|
+
blockReason;
|
|
165
|
+
/**
|
|
166
|
+
* @param ctx - owning root context (the service registers itself as `models`).
|
|
167
|
+
* @param config - the bound translator for this plugin's own dictionary.
|
|
168
|
+
*/
|
|
169
|
+
constructor(ctx, config) {
|
|
170
|
+
super(ctx, "modelDirectories");
|
|
171
|
+
this.blockReason = config.blockReason;
|
|
172
|
+
ctx.on("connection/reset", () => {
|
|
173
|
+
for (const directory of this.live.directories.values()) directory.resetConnected();
|
|
174
|
+
});
|
|
175
|
+
const refresh = () => {
|
|
176
|
+
for (const directory of this.live.directories.values()) directory.load().catch(() => void 0);
|
|
177
|
+
};
|
|
178
|
+
ctx.remote.$on("llm/adapters-updated", refresh);
|
|
179
|
+
ctx.remote.$on("settings/document-updated", refresh);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the per-session shared directory (lazy; the scope disposer
|
|
183
|
+
* removes and disposes it). Unknown sessions fail loud.
|
|
184
|
+
* @param sessionId - the owning session.
|
|
185
|
+
* @returns the resident directory both entries share.
|
|
186
|
+
*/
|
|
187
|
+
directoryFor(sessionId) {
|
|
188
|
+
const { live } = this;
|
|
189
|
+
const existing = live.directories.get(sessionId);
|
|
190
|
+
if (existing !== void 0) return existing;
|
|
191
|
+
const sessions = this.ctx.get("sessions");
|
|
192
|
+
const actx = sessions.scope(sessionId);
|
|
193
|
+
if (actx === void 0) throw new Error(`ui-model-selection: session "${String(sessionId)}" resolved no scope`);
|
|
194
|
+
const directory = new ModelDirectory(this.ctx.get("connection").api.sessions, sessionId, () => sessions.subagentAddress(sessionId) === void 0);
|
|
195
|
+
live.directories.set(sessionId, directory);
|
|
196
|
+
const conversation = this.ctx.get("conversation");
|
|
197
|
+
if (conversation !== void 0) {
|
|
198
|
+
const publish = () => {
|
|
199
|
+
conversation.blocks.set(sessionId, directory.store.getSnapshot().routable === false ? { reason: this.blockReason() } : void 0);
|
|
200
|
+
};
|
|
201
|
+
publish();
|
|
202
|
+
actx.effect(() => {
|
|
203
|
+
const stop = directory.store.subscribe(publish);
|
|
204
|
+
return () => {
|
|
205
|
+
stop();
|
|
206
|
+
conversation.blocks.set(sessionId, void 0);
|
|
207
|
+
};
|
|
208
|
+
}, "ui-model-selection: composer block");
|
|
209
|
+
}
|
|
210
|
+
actx.effect(() => () => {
|
|
211
|
+
directory.dispose();
|
|
212
|
+
live.directories.delete(sessionId);
|
|
213
|
+
}, "ui-model-selection: session directory");
|
|
214
|
+
return directory;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
219
|
+
function r(e) {
|
|
220
|
+
var t, f, n = "";
|
|
221
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
222
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
223
|
+
var o = e.length;
|
|
224
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
225
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
226
|
+
return n;
|
|
227
|
+
}
|
|
228
|
+
function clsx() {
|
|
229
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
230
|
+
return n;
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-model-selection/src/client/ModelSelect.module.css.mjs
|
|
234
|
+
const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{min-width:0;max-width:220px;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{color:var(--dsw-alias-label-caption);flex:none}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{z-index:20;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);width:min(240px,100vw - 32px);max-height:min(360px,100vh - 96px);box-shadow:var(--dsw-shadow-lv3);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-radius:12px;flex-direction:column;padding:4px;display:flex;position:absolute;bottom:calc(100% + 8px);right:0;overflow:hidden}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}._7KE1Ra_error,._7KE1Ra_warning{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;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:4px}._7KE1Ra_groupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px;position:sticky;top:0}._7KE1Ra_option{width:100%;min-height:38px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:8px;padding:6px 8px;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:14px;font-weight:500;line-height:20px;overflow:hidden}._7KE1Ra_description{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 18px;place-items:center;display:grid}._7KE1Ra_cell{width:100%;height:40px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:0 10px;font-size:14px;line-height:22px;display:flex}._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_cellLabel{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-tertiary);flex:0 auto;overflow:hidden}._7KE1Ra_cellChevron{color:var(--dsw-alias-label-tertiary);flex:none}";
|
|
235
|
+
const tagId = "@deepseek-ai/dsh-client-ui-model-selection/ModelSelect.module.css";
|
|
236
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
237
|
+
const tag = document.createElement("style");
|
|
238
|
+
tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-model-selection";
|
|
239
|
+
tag.dataset.pluginCss = tagId;
|
|
240
|
+
tag.textContent = css;
|
|
241
|
+
document.head.appendChild(tag);
|
|
242
|
+
}
|
|
243
|
+
var ModelSelect_module_css_default = {
|
|
244
|
+
"optionCopy": "_7KE1Ra_optionCopy",
|
|
245
|
+
"triggerLabel": "_7KE1Ra_triggerLabel",
|
|
246
|
+
"status": "_7KE1Ra_status",
|
|
247
|
+
"cellChevron": "_7KE1Ra_cellChevron",
|
|
248
|
+
"group": "_7KE1Ra_group",
|
|
249
|
+
"trigger": "_7KE1Ra_trigger",
|
|
250
|
+
"error": "_7KE1Ra_error",
|
|
251
|
+
"option": "_7KE1Ra_option",
|
|
252
|
+
"check": "_7KE1Ra_check",
|
|
253
|
+
"empty": "_7KE1Ra_empty",
|
|
254
|
+
"warning": "_7KE1Ra_warning",
|
|
255
|
+
"description": "_7KE1Ra_description",
|
|
256
|
+
"groupTitle": "_7KE1Ra_groupTitle",
|
|
257
|
+
"cell": "_7KE1Ra_cell",
|
|
258
|
+
"groups": "_7KE1Ra_groups",
|
|
259
|
+
"cellValue": "_7KE1Ra_cellValue",
|
|
260
|
+
"chevronOpen": "_7KE1Ra_chevronOpen",
|
|
261
|
+
"chevron": "_7KE1Ra_chevron",
|
|
262
|
+
"modelName": "_7KE1Ra_modelName",
|
|
263
|
+
"cellLabel": "_7KE1Ra_cellLabel",
|
|
264
|
+
"menu": "_7KE1Ra_menu",
|
|
265
|
+
"root": "_7KE1Ra_root",
|
|
266
|
+
"triggerEffort": "_7KE1Ra_triggerEffort",
|
|
267
|
+
"retry": "_7KE1Ra_retry",
|
|
268
|
+
"selected": "_7KE1Ra_selected"
|
|
269
|
+
};
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region lib/types/client/ModelSelect.js
|
|
272
|
+
/**
|
|
273
|
+
* ModelSelect: the composer's named model seat (`conversation.input.model`).
|
|
274
|
+
* Two-level selection per figma 496:26454's MenuDropdown: the root menu is
|
|
275
|
+
* the Model / Effort row pair (label + current value + a right chevron),
|
|
276
|
+
* each drilling into its own list — the provider-grouped model list over
|
|
277
|
+
* the shared directory, and the effort levels. The trigger (313:14108's
|
|
278
|
+
* ToggleButton) shows both: model name + effort in the caption tone.
|
|
279
|
+
* Data and submission ride the SAME per-session ModelDirectory as the
|
|
280
|
+
* /model popup; exact-model reasoning metadata and the selected effort come
|
|
281
|
+
* from the Host rather than a client-owned vocabulary. A rejected selection
|
|
282
|
+
* announces through the shared transient Toast anchored to the composer
|
|
283
|
+
* card; the in-menu strip with Retry remains the catalog-load surface.
|
|
284
|
+
*/
|
|
285
|
+
/**
|
|
286
|
+
* Render the composer model seat.
|
|
287
|
+
* @param props - owner share (locked) + injected face (shared directory
|
|
288
|
+
* store/verbs) + the standard locale seat.
|
|
289
|
+
* @returns the trigger and, while open, the two-level menu.
|
|
290
|
+
*/
|
|
291
|
+
function ModelSelect({ locked, available, directory, load, select, t }) {
|
|
292
|
+
const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
|
|
293
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
294
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
295
|
+
const lastActionRef = (0, react.useRef)("load");
|
|
296
|
+
const [toast, setToast] = (0, react.useState)(null);
|
|
297
|
+
const toastSeq = (0, react.useRef)(0);
|
|
298
|
+
const rootRef = (0, react.useRef)(null);
|
|
299
|
+
const triggerRef = (0, react.useRef)(null);
|
|
300
|
+
const itemRefs = (0, react.useRef)([]);
|
|
301
|
+
const id = (0, react.useId)();
|
|
302
|
+
const choices = (0, react.useMemo)(() => state.groups.flatMap((group) => group.models.map((model) => ({
|
|
303
|
+
group,
|
|
304
|
+
model,
|
|
305
|
+
selection: {
|
|
306
|
+
provider: group.id,
|
|
307
|
+
model: model.id,
|
|
308
|
+
...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
|
|
309
|
+
}
|
|
310
|
+
}))), [state.groups]);
|
|
311
|
+
const currentChoice = choices[state.current === null ? -1 : choices.findIndex((c) => c.selection.provider === state.current?.provider && c.selection.model === state.current.model)];
|
|
312
|
+
const reasoning = currentChoice?.model.reasoning;
|
|
313
|
+
const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
314
|
+
const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
315
|
+
const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
|
|
316
|
+
key: "provider-default",
|
|
317
|
+
effort: void 0,
|
|
318
|
+
label: t("effort.providerDefault")
|
|
319
|
+
}] : [], ...reasoning.efforts.map((effort) => ({
|
|
320
|
+
key: `effort:${effort.id}`,
|
|
321
|
+
effort: effort.id,
|
|
322
|
+
label: effort.name,
|
|
323
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
324
|
+
}))], [reasoning, t]);
|
|
325
|
+
const busy = state.status === "selecting";
|
|
326
|
+
const reload = () => {
|
|
327
|
+
lastActionRef.current = "load";
|
|
328
|
+
load();
|
|
329
|
+
};
|
|
330
|
+
(0, react.useEffect)(() => {
|
|
331
|
+
if (available) {
|
|
332
|
+
lastActionRef.current = "load";
|
|
333
|
+
load();
|
|
334
|
+
}
|
|
335
|
+
}, [available, load]);
|
|
336
|
+
(0, react.useEffect)(() => {
|
|
337
|
+
if (!open) return;
|
|
338
|
+
const closeOutside = (event) => {
|
|
339
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false);
|
|
340
|
+
};
|
|
341
|
+
document.addEventListener("mousedown", closeOutside);
|
|
342
|
+
return () => {
|
|
343
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
344
|
+
};
|
|
345
|
+
}, [open]);
|
|
346
|
+
if (!available) return null;
|
|
347
|
+
const show = () => {
|
|
348
|
+
setPane("root");
|
|
349
|
+
setOpen(true);
|
|
350
|
+
reload();
|
|
351
|
+
};
|
|
352
|
+
const close = (restoreFocus = false) => {
|
|
353
|
+
setOpen(false);
|
|
354
|
+
setPane("root");
|
|
355
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
356
|
+
triggerRef.current?.focus();
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
const moveFocus = (offset) => {
|
|
360
|
+
const items = itemRefs.current.filter((item) => item !== null);
|
|
361
|
+
if (items.length === 0) return;
|
|
362
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
363
|
+
items[(Math.max(active, 0) + offset + items.length) % items.length]?.focus();
|
|
364
|
+
};
|
|
365
|
+
const onRootKeyDown = (event) => {
|
|
366
|
+
if (event.key === "Escape" && open) {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
if (pane !== "root") setPane("root");
|
|
369
|
+
else close(true);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!open) return;
|
|
373
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const onBlur = (event) => {
|
|
379
|
+
if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
|
|
380
|
+
close();
|
|
381
|
+
};
|
|
382
|
+
const settleSelection = (accepted) => {
|
|
383
|
+
if (accepted) {
|
|
384
|
+
if (rootRef.current !== null) close(true);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const message = directory.getSnapshot().error;
|
|
388
|
+
if (message !== null) {
|
|
389
|
+
toastSeq.current += 1;
|
|
390
|
+
setToast({
|
|
391
|
+
seq: toastSeq.current,
|
|
392
|
+
text: t("error.action", { message })
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const choose = (selection) => {
|
|
397
|
+
if (state.current?.provider === selection.provider && state.current.model === selection.model) {
|
|
398
|
+
close(true);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
lastActionRef.current = "select";
|
|
402
|
+
select(selection).then(settleSelection);
|
|
403
|
+
};
|
|
404
|
+
const chooseEffort = (effort) => {
|
|
405
|
+
if (state.current === null) return;
|
|
406
|
+
if (effectiveEffort === effort) {
|
|
407
|
+
close(true);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const selection = {
|
|
411
|
+
provider: state.current.provider,
|
|
412
|
+
model: state.current.model,
|
|
413
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
414
|
+
};
|
|
415
|
+
lastActionRef.current = "select";
|
|
416
|
+
select(selection).then(settleSelection);
|
|
417
|
+
};
|
|
418
|
+
const modelLabel = currentChoice?.model.name ?? t("trigger.fallback");
|
|
419
|
+
const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
|
|
420
|
+
const triggerAria = currentChoice === void 0 ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
|
|
421
|
+
model: modelLabel,
|
|
422
|
+
effort: effortLabel
|
|
423
|
+
});
|
|
424
|
+
itemRefs.current = [];
|
|
425
|
+
let itemIndex = 0;
|
|
426
|
+
const itemRef = () => {
|
|
427
|
+
const at = itemIndex++;
|
|
428
|
+
return (node) => {
|
|
429
|
+
itemRefs.current[at] = node;
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
433
|
+
ref: rootRef,
|
|
434
|
+
className: ModelSelect_module_css_default.root,
|
|
435
|
+
onKeyDown: onRootKeyDown,
|
|
436
|
+
onBlur,
|
|
437
|
+
children: [
|
|
438
|
+
(0, react_jsx_runtime.jsxs)("button", {
|
|
439
|
+
ref: triggerRef,
|
|
440
|
+
type: "button",
|
|
441
|
+
className: ModelSelect_module_css_default.trigger,
|
|
442
|
+
"aria-label": triggerAria,
|
|
443
|
+
"aria-haspopup": "menu",
|
|
444
|
+
"aria-expanded": open,
|
|
445
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
446
|
+
title: triggerLabel,
|
|
447
|
+
disabled: locked,
|
|
448
|
+
onClick: () => {
|
|
449
|
+
if (open) close();
|
|
450
|
+
else show();
|
|
451
|
+
},
|
|
452
|
+
children: [
|
|
453
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
454
|
+
className: ModelSelect_module_css_default.triggerLabel,
|
|
455
|
+
children: modelLabel
|
|
456
|
+
}),
|
|
457
|
+
effortLabel !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
458
|
+
className: ModelSelect_module_css_default.triggerEffort,
|
|
459
|
+
children: effortLabel
|
|
460
|
+
}),
|
|
461
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: clsx(ModelSelect_module_css_default.chevron, open && ModelSelect_module_css_default.chevronOpen) })
|
|
462
|
+
]
|
|
463
|
+
}),
|
|
464
|
+
open && (0, react_jsx_runtime.jsxs)("div", {
|
|
465
|
+
id: `${id}-menu`,
|
|
466
|
+
className: ModelSelect_module_css_default.menu,
|
|
467
|
+
role: "menu",
|
|
468
|
+
"aria-label": t("menu.aria"),
|
|
469
|
+
"aria-busy": state.status === "loading" || busy,
|
|
470
|
+
children: [
|
|
471
|
+
pane === "root" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
472
|
+
ref: itemRef(),
|
|
473
|
+
type: "button",
|
|
474
|
+
role: "menuitem",
|
|
475
|
+
className: ModelSelect_module_css_default.cell,
|
|
476
|
+
onClick: () => {
|
|
477
|
+
setPane("model");
|
|
478
|
+
},
|
|
479
|
+
children: [
|
|
480
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
481
|
+
className: ModelSelect_module_css_default.cellLabel,
|
|
482
|
+
children: t("menu.model")
|
|
483
|
+
}),
|
|
484
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
485
|
+
className: ModelSelect_module_css_default.cellValue,
|
|
486
|
+
children: modelLabel
|
|
487
|
+
}),
|
|
488
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: ModelSelect_module_css_default.cellChevron })
|
|
489
|
+
]
|
|
490
|
+
}), reasoning !== void 0 && (0, react_jsx_runtime.jsxs)("button", {
|
|
491
|
+
ref: itemRef(),
|
|
492
|
+
type: "button",
|
|
493
|
+
role: "menuitem",
|
|
494
|
+
className: ModelSelect_module_css_default.cell,
|
|
495
|
+
onClick: () => {
|
|
496
|
+
setPane("effort");
|
|
497
|
+
},
|
|
498
|
+
children: [
|
|
499
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
500
|
+
className: ModelSelect_module_css_default.cellLabel,
|
|
501
|
+
children: t("menu.effort")
|
|
502
|
+
}),
|
|
503
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
504
|
+
className: ModelSelect_module_css_default.cellValue,
|
|
505
|
+
children: effortLabel
|
|
506
|
+
}),
|
|
507
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: ModelSelect_module_css_default.cellChevron })
|
|
508
|
+
]
|
|
509
|
+
})] }),
|
|
510
|
+
pane === "model" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
511
|
+
state.status === "loading" && (0, react_jsx_runtime.jsx)("div", {
|
|
512
|
+
className: ModelSelect_module_css_default.status,
|
|
513
|
+
children: t("status.loading")
|
|
514
|
+
}),
|
|
515
|
+
state.error !== null && lastActionRef.current === "load" && (0, react_jsx_runtime.jsxs)("div", {
|
|
516
|
+
className: ModelSelect_module_css_default.error,
|
|
517
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), (0, react_jsx_runtime.jsx)("button", {
|
|
518
|
+
type: "button",
|
|
519
|
+
className: ModelSelect_module_css_default.retry,
|
|
520
|
+
onClick: reload,
|
|
521
|
+
children: t("retry")
|
|
522
|
+
})]
|
|
523
|
+
}),
|
|
524
|
+
state.failures.map((failure) => (0, react_jsx_runtime.jsxs)("div", {
|
|
525
|
+
className: ModelSelect_module_css_default.warning,
|
|
526
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
|
|
527
|
+
name: failure.name,
|
|
528
|
+
message: failure.message
|
|
529
|
+
}) }), (0, react_jsx_runtime.jsx)("button", {
|
|
530
|
+
type: "button",
|
|
531
|
+
className: ModelSelect_module_css_default.retry,
|
|
532
|
+
onClick: reload,
|
|
533
|
+
children: t("retry")
|
|
534
|
+
})]
|
|
535
|
+
}, failure.id)),
|
|
536
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
537
|
+
className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
|
|
538
|
+
children: state.groups.map((group) => {
|
|
539
|
+
const headingId = `${id}-${group.id}`;
|
|
540
|
+
return (0, react_jsx_runtime.jsxs)("section", {
|
|
541
|
+
role: "group",
|
|
542
|
+
"aria-labelledby": headingId,
|
|
543
|
+
className: ModelSelect_module_css_default.group,
|
|
544
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
545
|
+
className: ModelSelect_module_css_default.groupTitle,
|
|
546
|
+
id: headingId,
|
|
547
|
+
children: group.name
|
|
548
|
+
}), group.models.map((model) => {
|
|
549
|
+
const selected = state.current?.provider === group.id && state.current.model === model.id;
|
|
550
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
551
|
+
ref: itemRef(),
|
|
552
|
+
type: "button",
|
|
553
|
+
role: "menuitemradio",
|
|
554
|
+
"aria-checked": selected,
|
|
555
|
+
className: clsx(ModelSelect_module_css_default.option, selected && ModelSelect_module_css_default.selected),
|
|
556
|
+
title: model.name,
|
|
557
|
+
disabled: busy,
|
|
558
|
+
onClick: () => {
|
|
559
|
+
choose({
|
|
560
|
+
provider: group.id,
|
|
561
|
+
model: model.id
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
565
|
+
className: ModelSelect_module_css_default.optionCopy,
|
|
566
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
567
|
+
className: ModelSelect_module_css_default.modelName,
|
|
568
|
+
children: model.name
|
|
569
|
+
}), model.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
570
|
+
className: ModelSelect_module_css_default.description,
|
|
571
|
+
children: model.description
|
|
572
|
+
})]
|
|
573
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
574
|
+
className: ModelSelect_module_css_default.check,
|
|
575
|
+
children: selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
|
|
576
|
+
})]
|
|
577
|
+
}, model.id);
|
|
578
|
+
})]
|
|
579
|
+
}, group.id);
|
|
580
|
+
})
|
|
581
|
+
}),
|
|
582
|
+
state.status === "ready" && choices.length === 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
583
|
+
className: ModelSelect_module_css_default.empty,
|
|
584
|
+
children: t("empty.models")
|
|
585
|
+
})
|
|
586
|
+
] }),
|
|
587
|
+
pane === "effort" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.error !== null && lastActionRef.current === "load" && (0, react_jsx_runtime.jsxs)("div", {
|
|
588
|
+
className: ModelSelect_module_css_default.error,
|
|
589
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), (0, react_jsx_runtime.jsx)("button", {
|
|
590
|
+
type: "button",
|
|
591
|
+
className: ModelSelect_module_css_default.retry,
|
|
592
|
+
onClick: reload,
|
|
593
|
+
children: t("action.reload")
|
|
594
|
+
})]
|
|
595
|
+
}), effortChoices.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
|
|
596
|
+
className: ModelSelect_module_css_default.empty,
|
|
597
|
+
children: t("empty.efforts")
|
|
598
|
+
}) : effortChoices.map((level) => (0, react_jsx_runtime.jsxs)("button", {
|
|
599
|
+
ref: itemRef(),
|
|
600
|
+
type: "button",
|
|
601
|
+
role: "menuitemradio",
|
|
602
|
+
"aria-checked": effectiveEffort === level.effort,
|
|
603
|
+
className: clsx(ModelSelect_module_css_default.option, effectiveEffort === level.effort && ModelSelect_module_css_default.selected),
|
|
604
|
+
disabled: busy,
|
|
605
|
+
onClick: () => {
|
|
606
|
+
chooseEffort(level.effort);
|
|
607
|
+
},
|
|
608
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
609
|
+
className: ModelSelect_module_css_default.optionCopy,
|
|
610
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
611
|
+
className: ModelSelect_module_css_default.modelName,
|
|
612
|
+
children: level.label
|
|
613
|
+
}), level.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
614
|
+
className: ModelSelect_module_css_default.description,
|
|
615
|
+
children: level.description
|
|
616
|
+
})]
|
|
617
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
618
|
+
className: ModelSelect_module_css_default.check,
|
|
619
|
+
children: effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
|
|
620
|
+
})]
|
|
621
|
+
}, level.key))] })
|
|
622
|
+
]
|
|
623
|
+
}),
|
|
624
|
+
toast !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
625
|
+
text: toast.text,
|
|
626
|
+
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
|
|
627
|
+
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
628
|
+
onDone: () => {
|
|
629
|
+
setToast(null);
|
|
630
|
+
}
|
|
631
|
+
}, toast.seq)
|
|
632
|
+
]
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region lib/types/client/locales.js
|
|
637
|
+
/**
|
|
638
|
+
* `model` namespace dictionaries.
|
|
639
|
+
*
|
|
640
|
+
* `trigger.selectAria` reads identically to `trigger.fallback` today and is
|
|
641
|
+
* still a separate key: the visible fallback label and the accessible name of
|
|
642
|
+
* an unset trigger are free to diverge per locale, and folding it into
|
|
643
|
+
* `trigger.aria` would announce the degenerate "Select model, current Select
|
|
644
|
+
* model".
|
|
645
|
+
*/
|
|
646
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
647
|
+
const zh = {
|
|
648
|
+
"command.description": "选择本会话使用的模型",
|
|
649
|
+
"option.loadError": "目录加载失败:{message}",
|
|
650
|
+
"trigger.fallback": "选择模型",
|
|
651
|
+
"trigger.selectAria": "选择模型",
|
|
652
|
+
"trigger.aria": "选择模型,当前 {model}",
|
|
653
|
+
"trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
|
|
654
|
+
"menu.aria": "模型与推理等级",
|
|
655
|
+
"menu.model": "模型",
|
|
656
|
+
"menu.effort": "推理等级",
|
|
657
|
+
"effort.providerDefault": "Default",
|
|
658
|
+
"status.loading": "正在刷新模型列表…",
|
|
659
|
+
"error.action": "模型操作失败:{message}",
|
|
660
|
+
"action.reload": "重新加载",
|
|
661
|
+
"warning.groupLoad": "{name} 加载失败:{message}",
|
|
662
|
+
"empty.models": "没有可用的模型。",
|
|
663
|
+
"blocked.composer": "当前模型不可用,请先选择模型",
|
|
664
|
+
"empty.efforts": "当前模型未提供推理等级。"
|
|
665
|
+
};
|
|
666
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
667
|
+
const en = {
|
|
668
|
+
"command.description": "Select the model for this conversation",
|
|
669
|
+
"option.loadError": "Catalog failed to load: {message}",
|
|
670
|
+
"trigger.fallback": "Select model",
|
|
671
|
+
"trigger.selectAria": "Select model",
|
|
672
|
+
"trigger.aria": "Select model, current {model}",
|
|
673
|
+
"trigger.ariaEffort": "Select model, current {model}, reasoning effort {effort}",
|
|
674
|
+
"menu.aria": "Model and reasoning effort",
|
|
675
|
+
"menu.model": "Model",
|
|
676
|
+
"menu.effort": "Effort",
|
|
677
|
+
"effort.providerDefault": "Default",
|
|
678
|
+
"status.loading": "Refreshing model list…",
|
|
679
|
+
"error.action": "Model operation failed: {message}",
|
|
680
|
+
"action.reload": "Reload",
|
|
681
|
+
"warning.groupLoad": "{name} failed to load: {message}",
|
|
682
|
+
"empty.models": "No models available.",
|
|
683
|
+
"blocked.composer": "This model is unavailable — select one to continue",
|
|
684
|
+
"empty.efforts": "This model provides no reasoning effort levels."
|
|
685
|
+
};
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region lib/types/client/index.js
|
|
688
|
+
/** One selectable row's id: an opaque row key (resolved by lookup, never parsed). */
|
|
689
|
+
function rowId(providerId, modelId) {
|
|
690
|
+
return `${providerId}/${modelId}`;
|
|
691
|
+
}
|
|
692
|
+
/** Flatten the directory into popup rows; failure rows are listed for visibility but never selectable. */
|
|
693
|
+
function optionsOf(directory, t) {
|
|
694
|
+
const rows = [];
|
|
695
|
+
for (const group of directory.groups) for (const model of group.models) rows.push({
|
|
696
|
+
id: rowId(group.id, model.id),
|
|
697
|
+
label: model.name,
|
|
698
|
+
detail: model.description !== void 0 ? `${group.name} · ${model.description}` : group.name,
|
|
699
|
+
...directory.current.provider === group.id && directory.current.model === model.id ? { active: true } : {}
|
|
700
|
+
});
|
|
701
|
+
for (const failure of directory.failures) rows.push({
|
|
702
|
+
id: `failure/${failure.id}`,
|
|
703
|
+
label: failure.name,
|
|
704
|
+
detail: t("option.loadError", { message: failure.message })
|
|
705
|
+
});
|
|
706
|
+
return rows;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Resolve a picked row back to its model selection by matching against the loaded
|
|
710
|
+
* groups (the same data the rows were built from — ids stay opaque).
|
|
711
|
+
* @param state - the session's directory snapshot.
|
|
712
|
+
* @param id - the picked row id.
|
|
713
|
+
* @returns the row's model selection, or undefined for failure rows / stale ids.
|
|
714
|
+
*/
|
|
715
|
+
function selectionOf(state, id) {
|
|
716
|
+
for (const group of state.groups) for (const model of group.models) {
|
|
717
|
+
if (rowId(group.id, model.id) !== id) continue;
|
|
718
|
+
const reasoningEffort = state.current?.provider === group.id && state.current.model === model.id ? state.current?.reasoningEffort ?? model.reasoning?.defaultEffort : void 0;
|
|
719
|
+
return {
|
|
720
|
+
provider: group.id,
|
|
721
|
+
model: model.id,
|
|
722
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/** Dictionary namespace owned by this plugin. */
|
|
727
|
+
const NS = "model";
|
|
728
|
+
/** Required services: the contribution registry, the seat's slot registry, locale, and the service's own faces. */
|
|
729
|
+
const inject = [
|
|
730
|
+
"commandUi",
|
|
731
|
+
"connection",
|
|
732
|
+
"locale",
|
|
733
|
+
"sessions",
|
|
734
|
+
"slots",
|
|
735
|
+
"remote"
|
|
736
|
+
];
|
|
737
|
+
/**
|
|
738
|
+
* Client plugin body: mount ModelDirectoryResolver, register the `model` dictionaries,
|
|
739
|
+
* then register the /model popup contribution and the composer model seat
|
|
740
|
+
* over the service.
|
|
741
|
+
* @param ctx - client root context.
|
|
742
|
+
*/
|
|
743
|
+
function apply(ctx) {
|
|
744
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
745
|
+
zh,
|
|
746
|
+
en
|
|
747
|
+
}), "ui-model-selection: dictionaries");
|
|
748
|
+
const t = ctx.locale.bind(NS);
|
|
749
|
+
ctx.plugin(ModelDirectoryResolver, { blockReason: () => t("blocked.composer") });
|
|
750
|
+
ctx.inject(["commandUi", "modelDirectories"], (scope) => {
|
|
751
|
+
const command = scope.get("commandUi");
|
|
752
|
+
const models = scope.modelDirectories;
|
|
753
|
+
const sessions = scope.sessions;
|
|
754
|
+
scope.effect(() => command.register({
|
|
755
|
+
name: "model",
|
|
756
|
+
description: t("command.description"),
|
|
757
|
+
available: (session) => sessions.subagentAddress(session.sessionId) === void 0,
|
|
758
|
+
ui: {
|
|
759
|
+
kind: "popupSelect",
|
|
760
|
+
options: async (session) => {
|
|
761
|
+
if (sessions.subagentAddress(session.sessionId) !== void 0) throw new Error("model selection is unavailable for addressed subagent sessions");
|
|
762
|
+
return optionsOf(await models.directoryFor(session.sessionId).load(), t);
|
|
763
|
+
},
|
|
764
|
+
onSelect: async (option, session) => {
|
|
765
|
+
if (sessions.subagentAddress(session.sessionId) !== void 0) throw new Error("model selection is unavailable for addressed subagent sessions");
|
|
766
|
+
const directory = models.directoryFor(session.sessionId);
|
|
767
|
+
const selection = selectionOf(directory.store.getSnapshot(), option.id);
|
|
768
|
+
if (selection === void 0) throw new Error("this provider's catalog failed to load — pick a model from a loaded group");
|
|
769
|
+
await directory.select(selection);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}), "ui-model-selection: /model contribution");
|
|
773
|
+
});
|
|
774
|
+
ctx.inject(["slots", "modelDirectories"], (scope) => {
|
|
775
|
+
const models = scope.modelDirectories;
|
|
776
|
+
const sessions = scope.sessions;
|
|
777
|
+
scope.slots.inject("conversation.input.model", () => scope.slots.register({
|
|
778
|
+
name: "conversation.input.model",
|
|
779
|
+
locale: NS,
|
|
780
|
+
inject: (sessionId) => {
|
|
781
|
+
const directory = models.directoryFor(sessionId);
|
|
782
|
+
const available = sessions.subagentAddress(sessionId) === void 0;
|
|
783
|
+
return {
|
|
784
|
+
available,
|
|
785
|
+
directory: directory.store,
|
|
786
|
+
load: () => {
|
|
787
|
+
if (available) directory.load().catch(() => {});
|
|
788
|
+
},
|
|
789
|
+
select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false)
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}, ModelSelect));
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
//#endregion
|
|
796
|
+
exports.ModelDirectory = ModelDirectory;
|
|
797
|
+
exports.ModelDirectoryResolver = ModelDirectoryResolver;
|
|
798
|
+
exports.apply = apply;
|
|
799
|
+
exports.inject = inject;
|
|
800
|
+
return module.exports;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region lib/types/index.js
|
|
2
|
+
/**
|
|
3
|
+
* Model selection plugin, node half. Pure UI plugin: the empty apply exists
|
|
4
|
+
* so the plugin appears in the host cordis.yml / Loader; the browser half
|
|
5
|
+
* ships via exports["./client"], discovered through the package.json
|
|
6
|
+
* dsh.client declaration.
|
|
7
|
+
*/
|
|
8
|
+
/** Host plugin body — no host-side behavior for this surface plugin. */
|
|
9
|
+
function apply() {}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { apply };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-model-selection`.
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-model-selection/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-client-ui-model-selection";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "client-ui-model-selection-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: a single command contribution registration whose disposal is
|
|
13
|
+
* proven by the HMR-safety spec — it emits no cordis events and owns no
|
|
14
|
+
* cross-plugin mutable state.
|
|
15
|
+
*/
|
|
16
|
+
const install = () => {};
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
21
|
+
*/
|
|
22
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
+
//#endregion
|
|
24
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import type { ModelSelectInjected } from './slots.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Render the composer model seat.
|
|
5
|
+
* @param props - owner share (locked) + injected face (shared directory
|
|
6
|
+
* store/verbs) + the standard locale seat.
|
|
7
|
+
* @returns the trigger and, while open, the two-level menu.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ModelSelect({ locked, available, directory, load, select, t }: ModelSelectInjected & {
|
|
10
|
+
locked: boolean;
|
|
11
|
+
} & PropsLocale<'model'>): import("react").JSX.Element | null;
|
|
12
|
+
//# sourceMappingURL=ModelSelect.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session model directory: the ONE state both selection entries share.
|
|
3
|
+
* The /model popup and the composer-seat selector load through the same
|
|
4
|
+
* controller and submit through the same selectModel call, so the host stays
|
|
5
|
+
* the single fact source and the store is one shared echo — a switch made in
|
|
6
|
+
* either entry is what the other shows next.
|
|
7
|
+
*/
|
|
8
|
+
import type { IApiClient, ModelCatalogFailure, ModelProviderGroup, ModelSelection, SessionId, SessionModels } from '@deepseek-ai/dsh-api-remotes/client';
|
|
9
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
|
+
/** Directory snapshot both entries render from. */
|
|
11
|
+
export interface ModelDirectoryState {
|
|
12
|
+
/** Model selection the host reports for the next assembled step; null before the first load. */
|
|
13
|
+
current: ModelSelection | null;
|
|
14
|
+
/**
|
|
15
|
+
* Whether an adapter serves the current selection's provider, as the host reports
|
|
16
|
+
* it — null before the first load, which is NOT the same as blocked. Read
|
|
17
|
+
* this rather than "current matches no group": catalog membership is
|
|
18
|
+
* advisory, so a route serving a model it stopped advertising is missing
|
|
19
|
+
* from the groups yet perfectly usable.
|
|
20
|
+
*/
|
|
21
|
+
routable: boolean | null;
|
|
22
|
+
/** Successfully loaded provider groups (last good load). */
|
|
23
|
+
groups: readonly ModelProviderGroup[];
|
|
24
|
+
/** Provider-local failures from the last load; usable groups stay usable. */
|
|
25
|
+
failures: readonly ModelCatalogFailure[];
|
|
26
|
+
/** Lifecycle of the in-flight operation. */
|
|
27
|
+
status: 'idle' | 'loading' | 'ready' | 'selecting' | 'error';
|
|
28
|
+
/** Whole-request or selection failure text; null when none. */
|
|
29
|
+
error: string | null;
|
|
30
|
+
}
|
|
31
|
+
/** One session's shared directory controller; disposed with the session scope. */
|
|
32
|
+
export declare class ModelDirectory {
|
|
33
|
+
private readonly sessions;
|
|
34
|
+
private readonly sessionId;
|
|
35
|
+
private readonly available;
|
|
36
|
+
/** The shared snapshot both entries render from (uSES-safe store). */
|
|
37
|
+
readonly store: SnapshotStore<ModelDirectoryState>;
|
|
38
|
+
/** Latest operation wins; an older response never overwrites a newer one. */
|
|
39
|
+
private generation;
|
|
40
|
+
private disposed;
|
|
41
|
+
/**
|
|
42
|
+
* @param sessions - the session wire face (captured from the plugin's root connection).
|
|
43
|
+
* @param sessionId - the owning session.
|
|
44
|
+
* @param available - whether this session may use Agent-bound model RPCs.
|
|
45
|
+
*/
|
|
46
|
+
constructor(sessions: Pick<IApiClient['sessions'], 'models' | 'selectModel'>, sessionId: SessionId, available: () => boolean);
|
|
47
|
+
/**
|
|
48
|
+
* Refresh the advisory directory (both entries call this on open).
|
|
49
|
+
* Failure preserves the last good groups and current selection.
|
|
50
|
+
* @returns the fresh directory value.
|
|
51
|
+
*/
|
|
52
|
+
load(): Promise<SessionModels>;
|
|
53
|
+
/**
|
|
54
|
+
* Select the complete provider/model/reasoning selection (both entries submit through here). Success
|
|
55
|
+
* updates the shared current; failure surfaces on the store and throws so
|
|
56
|
+
* each entry's own retry surface engages.
|
|
57
|
+
* @param selection - provider, provider-owned model id, and optional adapter-owned effort.
|
|
58
|
+
*/
|
|
59
|
+
select(selection: ModelSelection): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Drop the previous Host generation's projection and repull it. Clearing
|
|
62
|
+
* first prevents an unconsumed process-local selection from being displayed
|
|
63
|
+
* while the restarted Host has restored the last logged model selection.
|
|
64
|
+
*/
|
|
65
|
+
resetConnected(): void;
|
|
66
|
+
/** Scope teardown: late settlements lose write access to the store. */
|
|
67
|
+
dispose(): void;
|
|
68
|
+
private assertAvailable;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=directory.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
|
+
import { type ModelKey } from './locales.ts';
|
|
3
|
+
export { ModelDirectory } from './directory.ts';
|
|
4
|
+
export type { ModelDirectoryState } from './directory.ts';
|
|
5
|
+
export { ModelDirectoryResolver } from './service.ts';
|
|
6
|
+
export type { ModelSelectInjected } from './slots.ts';
|
|
7
|
+
export type { ModelKey } from './locales.ts';
|
|
8
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
9
|
+
interface LocaleNamespaceMap {
|
|
10
|
+
/** The model selection surfaces' copy (/model popup + composer seat). */
|
|
11
|
+
model: ModelKey;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Required services: the contribution registry, the seat's slot registry, locale, and the service's own faces. */
|
|
15
|
+
export declare const inject: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Client plugin body: mount ModelDirectoryResolver, register the `model` dictionaries,
|
|
18
|
+
* then register the /model popup contribution and the composer model seat
|
|
19
|
+
* over the service.
|
|
20
|
+
* @param ctx - client root context.
|
|
21
|
+
*/
|
|
22
|
+
export declare function apply(ctx: ClientContext): void;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `model` namespace dictionaries.
|
|
3
|
+
*
|
|
4
|
+
* `trigger.selectAria` reads identically to `trigger.fallback` today and is
|
|
5
|
+
* still a separate key: the visible fallback label and the accessible name of
|
|
6
|
+
* an unset trigger are free to diverge per locale, and folding it into
|
|
7
|
+
* `trigger.aria` would announce the degenerate "Select model, current Select
|
|
8
|
+
* model".
|
|
9
|
+
*/
|
|
10
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
11
|
+
export declare const zh: {
|
|
12
|
+
'command.description': string;
|
|
13
|
+
'option.loadError': string;
|
|
14
|
+
'trigger.fallback': string;
|
|
15
|
+
'trigger.selectAria': string;
|
|
16
|
+
'trigger.aria': string;
|
|
17
|
+
'trigger.ariaEffort': string;
|
|
18
|
+
'menu.aria': string;
|
|
19
|
+
'menu.model': string;
|
|
20
|
+
'menu.effort': string;
|
|
21
|
+
'effort.providerDefault': string;
|
|
22
|
+
'status.loading': string;
|
|
23
|
+
'error.action': string;
|
|
24
|
+
'action.reload': string;
|
|
25
|
+
'warning.groupLoad': string;
|
|
26
|
+
'empty.models': string;
|
|
27
|
+
'blocked.composer': string;
|
|
28
|
+
'empty.efforts': string;
|
|
29
|
+
};
|
|
30
|
+
/** The model namespace key union. */
|
|
31
|
+
export type ModelKey = keyof typeof zh;
|
|
32
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
33
|
+
export declare const en: {
|
|
34
|
+
'command.description': string;
|
|
35
|
+
'option.loadError': string;
|
|
36
|
+
'trigger.fallback': string;
|
|
37
|
+
'trigger.selectAria': string;
|
|
38
|
+
'trigger.aria': string;
|
|
39
|
+
'trigger.ariaEffort': string;
|
|
40
|
+
'menu.aria': string;
|
|
41
|
+
'menu.model': string;
|
|
42
|
+
'menu.effort': string;
|
|
43
|
+
'effort.providerDefault': string;
|
|
44
|
+
'status.loading': string;
|
|
45
|
+
'error.action': string;
|
|
46
|
+
'action.reload': string;
|
|
47
|
+
'warning.groupLoad': string;
|
|
48
|
+
'empty.models': string;
|
|
49
|
+
'blocked.composer': string;
|
|
50
|
+
'empty.efforts': string;
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelDirectoryResolver (`ctx.modelDirectories`): the root owner of per-session
|
|
3
|
+
* {@link ModelDirectory} instances. Both selection entries (the /model popup
|
|
4
|
+
* and the composer model seat) resolve their session's directory through
|
|
5
|
+
* this service, which is what makes the dual entry one shared state.
|
|
6
|
+
*
|
|
7
|
+
* Per-session storage follows the client service pattern (InputTriggerService /
|
|
8
|
+
* CommandUiRuntime): a lazy service-internal map whose entry is deleted by the
|
|
9
|
+
* owning scope's disposer. The host `dsh-scope` ScopedLayers registry does
|
|
10
|
+
* does not belong here: it derives scope from the host carrier mechanism
|
|
11
|
+
* (object-keyed), while client scopes tag contexts with branded SessionId
|
|
12
|
+
* strings, and it models global+shadow named registries — this is a
|
|
13
|
+
* per-session singleton with no global layer to merge.
|
|
14
|
+
*/
|
|
15
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
|
|
18
|
+
import { ModelDirectory } from './directory.ts';
|
|
19
|
+
declare module '@deepseek-ai/cordis' {
|
|
20
|
+
interface Context {
|
|
21
|
+
modelDirectories: ModelDirectoryResolver;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The `ctx.modelDirectories` session model-selection service. */
|
|
25
|
+
export declare class ModelDirectoryResolver extends Service {
|
|
26
|
+
static inject: string[];
|
|
27
|
+
private readonly live;
|
|
28
|
+
/** Localized composer-block copy; this plugin owns the string it raises. */
|
|
29
|
+
private readonly blockReason;
|
|
30
|
+
/**
|
|
31
|
+
* @param ctx - owning root context (the service registers itself as `models`).
|
|
32
|
+
* @param config - the bound translator for this plugin's own dictionary.
|
|
33
|
+
*/
|
|
34
|
+
constructor(ctx: Context, config: {
|
|
35
|
+
blockReason: () => string;
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the per-session shared directory (lazy; the scope disposer
|
|
39
|
+
* removes and disposes it). Unknown sessions fail loud.
|
|
40
|
+
* @param sessionId - the owning session.
|
|
41
|
+
* @returns the resident directory both entries share.
|
|
42
|
+
*/
|
|
43
|
+
directoryFor(sessionId: SessionId): ModelDirectory;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelSelect's injected face. The target 'conversation.input.model' seat is
|
|
3
|
+
* declared (children table) and typed by ui-conversation's composer-bar
|
|
4
|
+
* entry; this package only contributes the single occupant, so no SlotMap
|
|
5
|
+
* merge lives here.
|
|
6
|
+
*/
|
|
7
|
+
import type { ModelSelection } from '@deepseek-ai/dsh-api-remotes/client';
|
|
8
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
|
+
import type { ModelDirectoryState } from './directory.ts';
|
|
10
|
+
/** Injected business face of the composer model seat. */
|
|
11
|
+
export interface ModelSelectInjected {
|
|
12
|
+
/** Whether this session supports Agent-bound model inspection and selection. */
|
|
13
|
+
available: boolean;
|
|
14
|
+
/** The session's shared directory store (same instance the /model popup reads). */
|
|
15
|
+
directory: SnapshotStore<ModelDirectoryState>;
|
|
16
|
+
/** Refresh the advisory directory (fire-and-forget; errors land on the store). */
|
|
17
|
+
load: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Select a complete provider/model/reasoning selection.
|
|
20
|
+
* @param selection - model selection and optional adapter-owned effort.
|
|
21
|
+
* @returns whether the host accepted the selection.
|
|
22
|
+
*/
|
|
23
|
+
select: (selection: ModelSelection) => Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=slots.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model selection plugin, node half. Pure UI plugin: the empty apply exists
|
|
3
|
+
* so the plugin appears in the host cordis.yml / Loader; the browser half
|
|
4
|
+
* ships via exports["./client"], discovered through the package.json
|
|
5
|
+
* dsh.client declaration.
|
|
6
|
+
*/
|
|
7
|
+
/** Host plugin body — no host-side behavior for this surface plugin. */
|
|
8
|
+
export declare function apply(): void;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-model-selection`.
|
|
3
|
+
* @module @deepseek-ai/dsh-client-ui-model-selection/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "client-ui-model-selection-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crazx/dsh-client-ui-model-selection",
|
|
3
|
+
"description": "Model selection: the /model popupSelect over session.models / session.selectModel",
|
|
4
|
+
"version": "0.1.0-rc.7.zw.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/aka-danielZhang/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/client/ui-model-selection"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./client": {
|
|
26
|
+
"types": "./lib/types/client/index.d.ts",
|
|
27
|
+
"default": "./lib/client.js"
|
|
28
|
+
},
|
|
29
|
+
"./src/*": "./src/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"client": {
|
|
34
|
+
"inject": [
|
|
35
|
+
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-commands",
|
|
38
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
39
|
+
],
|
|
40
|
+
"platform": "web"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
|
|
46
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
47
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
48
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.7",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.7",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
55
|
+
"clsx": "^2.1.1",
|
|
56
|
+
"@deepseek-ai/cordis": "^0.1.0-rc.7",
|
|
57
|
+
"react": "^18.2.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
|
|
61
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
64
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.7",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
71
|
+
"@types/react": "~18.3.1",
|
|
72
|
+
"clsx": "^2.1.1",
|
|
73
|
+
"@deepseek-ai/cordis": "^0.1.0-rc.7",
|
|
74
|
+
"react": "^18.2.0"
|
|
75
|
+
},
|
|
76
|
+
"files": [
|
|
77
|
+
"lib/index.js",
|
|
78
|
+
"lib/invariant.js",
|
|
79
|
+
"lib/client.js",
|
|
80
|
+
"lib/types/**/*.d.ts"
|
|
81
|
+
],
|
|
82
|
+
"scripts": {
|
|
83
|
+
"bundle": "tsdown",
|
|
84
|
+
"watch": "tsdown --watch"
|
|
85
|
+
}
|
|
86
|
+
}
|