@domitor-syh/dsh-ui-skin-switcher 0.1.0

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 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.md ADDED
@@ -0,0 +1,11 @@
1
+ # @deepseek-ai/dsh-client-ui-skin-switcher
2
+
3
+ Model and reasoning-effort switcher for the composer. Registers a named
4
+ `conversation.input.model` seat that reads the session's provider-grouped
5
+ directory through `session.models` and submits through `session.selectModel`
6
+ (the same session-scoped RPC the first-party model selector uses), so a switch
7
+ made here is what `/model` shows next and takes effect on the current session.
8
+
9
+ Above the ordinary model list it renders a two-level (Off/Max) reasoning-effort
10
+ slider for models that declare `reasoningEfforts`, remembering each model's last
11
+ effort so switching model and back keeps the effort.
@@ -0,0 +1,9 @@
1
+ # Bundle layer contributed by @domitor-syh/dsh-ui-skin-switcher.
2
+ # Applied by `dsh plugin add` into any profile that lists this bundle.
3
+ #
4
+ # Skin switcher: composer model + reasoning-effort seat over session.models /
5
+ # session.selectModel. Mirrors the model-selection RPC face through a custom
6
+ # skin dropdown with Off/Max effort slider.
7
+ - insert:
8
+ - id: ui-skin-switcher
9
+ name: '@domitor-syh/dsh-ui-skin-switcher'
package/lib/client.js ADDED
@@ -0,0 +1,451 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@domitor-syh/dsh-ui-skin-switcher",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
29
+ let react = require("react");
30
+ react = __toESM(react, 1);
31
+ //#region src/client/index.ts
32
+ /**
33
+ * Skin-switcher plugin, browser half — the composer's model + reasoning-effort
34
+ * switcher. Registers the named `conversation.input.model` seat, loads the
35
+ * session's provider-grouped directory through `session.models` and submits
36
+ * through `session.selectModel` (the same session-scoped RPC the first-party
37
+ * selector uses), so a switch made here takes effect on the current session and
38
+ * is what `/model` shows next. Above the model list it renders a two-level
39
+ * (Off/Max) reasoning-effort slider for models that declare `reasoningEfforts`,
40
+ * remembering each model's last effort.
41
+ */
42
+ /** Required services: the connection RPC face (sessions) and the slot registry. */
43
+ /** Required services: the connection RPC face (sessions models/selectModel), the runtime sessions service (subagentAddress), and the slot registry. */
44
+ const inject = [
45
+ "connection",
46
+ "sessions",
47
+ "slots"
48
+ ];
49
+ /** Insert one stylesheet and return its disposer. */
50
+ function injectCss(css) {
51
+ const tag = document.createElement("style");
52
+ tag.dataset.plugin = "skin-switcher";
53
+ tag.textContent = css;
54
+ document.head.appendChild(tag);
55
+ return () => {
56
+ tag.remove();
57
+ };
58
+ }
59
+ /** Extract a message from an unknown error. */
60
+ function msg(e) {
61
+ if (e !== null && typeof e === "object" && typeof e.message === "string") return e.message;
62
+ return String(e);
63
+ }
64
+ /** Find the model descriptor inside one provider group. */
65
+ function findModel(groups, provider, model) {
66
+ const g = groups.find((x) => x.id === provider);
67
+ if (g === void 0) return void 0;
68
+ return g.models.find((m) => m.id === model);
69
+ }
70
+ /** Effort slider / matrix / model list styles, injected once for this plugin. */
71
+ const CSS = ".sk5-root{position:relative;display:inline-flex;}.sk5-triggers{display:inline-flex;align-items:center;gap:2px;}.sk5-trigger{display:inline-flex;align-items:center;padding:4px 8px;border-radius:7px;border:none;background:transparent;color:var(--dsw-alias-label-primary);font-size:12.5px;cursor:pointer;max-width:210px;}.sk5-trigger:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);}.sk5-trigger:disabled{opacity:.55;cursor:not-allowed;}.sk5-triggerName{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.sk5-triggerEffort{color:var(--dsw-alias-brand-primary);}.sk5-drop{position:absolute;bottom:calc(100% + 6px);right:0;border-radius:12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);box-shadow:0 10px 32px rgb(0 0 0 / .35);z-index:1200;color:var(--dsw-alias-label-primary);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);}.sk5-model{width:256px;}.sk5-modelsTitle{font-size:14px;font-weight:700;color:var(--dsw-alias-label-secondary);padding:12px 14px 6px;text-align:left;}.sk5-list{overflow-y:auto;padding:2px 6px 10px;max-height:min(420px,60vh);}.sk5-group{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary);padding:8px 10px 3px;text-align:left;}.sk5-row{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;text-align:left;padding:6px 10px;border-radius:8px;border:none;background:transparent;color:var(--dsw-alias-label-primary);font-size:12.5px;cursor:pointer;}.sk5-row:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);}.sk5-row:disabled{opacity:.6;cursor:wait;}.sk5-rowDesc{font-size:11px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.sk5-check{font-size:12px;color:var(--dsw-alias-brand-primary);}.sk5-status{padding:14px;font-size:12.5px;opacity:.65;text-align:center;}.sk5-err{padding:8px 4px 0;font-size:11.5px;color:var(--dsw-alias-state-error-primary);text-align:left;}.sk5-effort{width:225px;max-width:calc(100vw - 32px);box-sizing:border-box;padding:10px 12px 12px;}.sk5-titleRow{display:flex;align-items:center;gap:8px;margin-bottom:8px;}.sk5-effortLabel{font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary);}.sk5-help{position:relative;margin-left:auto;display:inline-flex;}.sk5-helpBtn{width:16px;height:16px;border-radius:50%;clip-path:circle(58%);border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1;display:flex;align-items:center;justify-content:center;padding:0;cursor:help;flex:none;}.sk5-helpBtn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);}.sk5-helpTip{position:absolute;top:calc(100% + 6px);right:0;width:206px;box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;box-shadow:0 8px 24px rgb(0 0 0 / .35);z-index:20;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .12s ease,visibility .12s ease;}.sk5-helpBtn:hover ~ .sk5-helpTip{opacity:1;visibility:visible;}.sk5-helpTip:hover{opacity:1;visibility:visible;}.sk5-helpTip::before{content:\"\";position:absolute;top:-8px;left:0;right:0;height:8px;}.sk5-helpTitle{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:3px;text-align:left;}.sk5-helpText{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-secondary);text-align:left;}.sk5-effortValue{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);}.sk5-effortValueMax{color:var(--dsw-static-deepseek-400);}@keyframes sk5-nameBlur{from{filter:blur(5px);opacity:.3}to{filter:blur(0);opacity:1}}.sk5-labelsRow{display:flex;justify-content:space-between;margin-bottom:6px;}.sk5-endLabel{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary);}.sk5-track{position:relative;height:26px;border-radius:6px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 15%,transparent);cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none;}.sk5-fill{position:absolute;top:0;bottom:0;left:0;transition:width .16s ease,opacity .16s ease;pointer-events:none;}.sk5-dotHit{position:absolute;top:0;bottom:0;width:16px;transform:translateX(-50%);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:2;}.sk5-dot{width:4px;height:4px;border-radius:50%;background:var(--dsw-alias-label-primary);opacity:.9;pointer-events:none;transition:transform .12s ease,opacity .12s ease;}.sk5-dotHit:hover .sk5-dot{opacity:1;transform:scale(1.2);}.sk5-handle{position:absolute;top:0;bottom:0;width:20px;border-radius:6px;background:var(--dsw-alias-label-primary);transform:translateX(-50%);box-shadow:0 0 0 transparent;transition:left .16s ease,box-shadow .5s ease,background-color .5s ease;z-index:3;cursor:grab;}.sk5-handleMax{background:color-mix(in srgb, var(--dsw-static-deepseek-450) 25%, var(--dsw-alias-label-primary));box-shadow:0 0 10px color-mix(in srgb, var(--dsw-static-deepseek-400) 90%, transparent);}.sk5-matrix{position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden;border-radius:6px;pointer-events:none;z-index:1;box-sizing:border-box;display:grid;gap:2px;justify-content:center;align-content:center;}.sk5-cell{width:100%;height:100%;opacity:0;animation:sk5-appear .15s ease forwards;}@keyframes sk5-appear{from{opacity:0}to{opacity:1}}.sk5-sq{width:100%;height:100%;border-radius:1px;animation:sk5-flash 1s infinite ease-in-out;}@keyframes sk5-flash{0%,24%{background-color:var(--dsw-static-deepseek-500)}28%,72%{background-color:var(--flash-light,var(--dsw-static-deepseek-400))}76%,100%{background-color:var(--dsw-static-deepseek-500)}}";
72
+ /** Per-model (provider\u0000model) effort memory, shared across the whole page. */
73
+ const effortMemory = /* @__PURE__ */ new Map();
74
+ /** Two-level mask curve: right side opaque, left transparent, transition over 5%→75%. */
75
+ function fadeAt(fx) {
76
+ if (fx <= .05) return 0;
77
+ if (fx >= .75) return 1;
78
+ const t = (fx - .05) / .7;
79
+ return t * t * (3 - 2 * t);
80
+ }
81
+ /**
82
+ * Client plugin body: inject the stylesheet once, then register the composer
83
+ * model seat whose component reads `sessions.models` and writes
84
+ * `sessions.selectModel`.
85
+ * @param ctx - client root context carrying connection and slots.
86
+ */
87
+ function apply(ctx) {
88
+ const api = ctx.connection.api.sessions;
89
+ const runtimeSessions = ctx.sessions;
90
+ ctx.effect(() => injectCss(CSS));
91
+ ctx.slots.inject("conversation.input.model", () => ctx.slots.register({
92
+ name: "conversation.input.model",
93
+ priority: -1
94
+ }, (props) => {
95
+ const { locked, sessionId } = props;
96
+ const [menu, setMenu] = react.useState(null);
97
+ const [data, setData] = react.useState(null);
98
+ const [busy, setBusy] = react.useState(false);
99
+ const [error, setError] = react.useState(null);
100
+ const [hoverIdx, setHoverIdx] = react.useState(null);
101
+ const [grid, setGrid] = react.useState({
102
+ c: 0,
103
+ r: 0
104
+ });
105
+ const rootRef = react.useRef(null);
106
+ const trackRef = react.useRef(null);
107
+ const dragIdxRef = react.useRef(null);
108
+ const busyRef = react.useRef(false);
109
+ const pendingRef = react.useRef(null);
110
+ const [settledIdx, setSettledIdx] = react.useState(null);
111
+ const GAP = 2;
112
+ const HANDLE_W = 20;
113
+ const load = () => {
114
+ api.models({ sessionId }).then((resp) => {
115
+ const r = resp.result;
116
+ if (!r.ok) {
117
+ setError(msg(r.error));
118
+ return;
119
+ }
120
+ setData({
121
+ groups: r.value.groups,
122
+ failures: r.value.failures,
123
+ current: r.value.current
124
+ });
125
+ setError(null);
126
+ }, (e) => setError(msg(e)));
127
+ };
128
+ react.useEffect(() => {
129
+ load();
130
+ }, []);
131
+ react.useEffect(() => {
132
+ if (menu !== null) load();
133
+ }, [menu]);
134
+ react.useEffect(() => {
135
+ if (menu === null) return;
136
+ const handler = (ev) => {
137
+ if (rootRef.current !== null && !rootRef.current.contains(ev.target)) setMenu(null);
138
+ };
139
+ document.addEventListener("mousedown", handler);
140
+ return () => document.removeEventListener("mousedown", handler);
141
+ }, [menu]);
142
+ react.useEffect(() => {
143
+ if (menu !== "effort") return;
144
+ const measure = () => {
145
+ if (trackRef.current !== null) {
146
+ const w = trackRef.current.clientWidth;
147
+ const h = trackRef.current.clientHeight;
148
+ const cn = Math.max(1, Math.floor((w + GAP) / 6));
149
+ const rn = Math.max(1, Math.floor((h + GAP) / 6));
150
+ setGrid((g) => g.c === cn && g.r === rn ? g : {
151
+ c: cn,
152
+ r: rn
153
+ });
154
+ }
155
+ };
156
+ measure();
157
+ window.addEventListener("resize", measure);
158
+ return () => window.removeEventListener("resize", measure);
159
+ }, [menu]);
160
+ react.useEffect(() => {
161
+ setHoverIdx(null);
162
+ }, [data === null ? null : data.current === null ? null : data.current.reasoningEffort]);
163
+ const apply = (sel, keepOpen) => {
164
+ pendingRef.current = sel;
165
+ if (busyRef.current) return;
166
+ busyRef.current = true;
167
+ setBusy(true);
168
+ setError(null);
169
+ api.selectModel({
170
+ sessionId,
171
+ provider: sel.provider,
172
+ model: sel.model,
173
+ ...sel.reasoningEffort === void 0 ? {} : { reasoningEffort: sel.reasoningEffort }
174
+ }).then((resp) => {
175
+ busyRef.current = false;
176
+ setBusy(false);
177
+ const r = resp.result;
178
+ if (!r.ok) {
179
+ setError(msg(r.error));
180
+ return;
181
+ }
182
+ const selected = r.value.selected;
183
+ if (!keepOpen) setMenu(null);
184
+ setData((d) => d === null ? null : {
185
+ ...d,
186
+ current: selected
187
+ });
188
+ effortMemory.set(sel.provider + "\0" + sel.model, selected.reasoningEffort);
189
+ const next = pendingRef.current;
190
+ if (next !== null && next !== sel) {
191
+ pendingRef.current = null;
192
+ apply(next, true);
193
+ }
194
+ }, (e) => {
195
+ busyRef.current = false;
196
+ setBusy(false);
197
+ setHoverIdx(null);
198
+ setError(msg(e));
199
+ pendingRef.current = null;
200
+ });
201
+ };
202
+ const groups = data === null ? [] : data.groups;
203
+ const current = data === null ? null : data.current;
204
+ const curGroup = current === null ? void 0 : groups.find((g) => g.id === current.provider);
205
+ const curModel = current === null ? void 0 : findModel(groups, current.provider, current.model);
206
+ const modelLabel = curModel !== void 0 ? curModel.name : current !== null && current.model !== "" ? current.model : "模型";
207
+ const reasoning = curModel !== void 0 ? curModel.reasoning : void 0;
208
+ const effectiveEffort = current !== null && current.reasoningEffort !== void 0 ? current.reasoningEffort : reasoning !== void 0 && reasoning.defaultEffort !== void 0 ? reasoning.defaultEffort : void 0;
209
+ const choices = reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
210
+ key: "default",
211
+ effort: void 0,
212
+ label: "默认"
213
+ }] : [], ...reasoning.efforts.map((e) => ({
214
+ key: "e:" + e.id,
215
+ effort: e.id,
216
+ label: e.name
217
+ }))];
218
+ const activeKey = effectiveEffort === void 0 ? "default" : "e:" + effectiveEffort;
219
+ const activeIdx = Math.max(0, choices.findIndex((c) => c.key === activeKey));
220
+ const posIdx = hoverIdx !== null ? hoverIdx : activeIdx;
221
+ const safePosIdx = choices.length > 0 ? Math.max(0, Math.min(choices.length - 1, posIdx)) : 0;
222
+ const atMaxPos = choices.length > 1 && safePosIdx === choices.length - 1;
223
+ const displayIdx = settledIdx === null ? activeIdx : settledIdx;
224
+ const safeDisplayIdx = choices.length > 0 ? Math.max(0, Math.min(choices.length - 1, displayIdx)) : 0;
225
+ const displayChoice = choices.length > 0 ? choices[safeDisplayIdx] : null;
226
+ const displayLabel = displayChoice !== null ? displayChoice.label : "默认";
227
+ const inMax = choices.length > 1 && safeDisplayIdx === choices.length - 1;
228
+ const matrixVisible = inMax && atMaxPos;
229
+ const posPct = choices.length > 1 ? posIdx / (choices.length - 1) * 100 : 0;
230
+ const matrixCells = react.useMemo(() => {
231
+ if (!inMax || grid.c <= 0 || grid.r <= 0) return [];
232
+ const deep = [
233
+ 65,
234
+ 118,
235
+ 230
236
+ ];
237
+ const lightMax = [
238
+ 103,
239
+ 158,
240
+ 254
241
+ ];
242
+ const revealMs = 1e3;
243
+ const revealCycles = 3;
244
+ const revealDepth = .6;
245
+ const speedAt = (t, phase) => 1 + revealDepth * Math.sin(2 * Math.PI * revealCycles * t + phase);
246
+ const integrate = (u, phase) => {
247
+ const steps = 120;
248
+ const du = u / steps;
249
+ let s = 0;
250
+ for (let i = 0; i < steps; i++) {
251
+ const t = (i + .5) * du;
252
+ s += 1 / speedAt(t, phase) * du;
253
+ }
254
+ return s;
255
+ };
256
+ const phases = [];
257
+ const fulls = [];
258
+ for (let r = 0; r < grid.r; r++) {
259
+ const ph = r % 4 * (Math.PI / 2);
260
+ phases.push(ph);
261
+ fulls.push(integrate(1, ph));
262
+ }
263
+ const out = [];
264
+ for (let r = 0; r < grid.r; r++) for (let c = 0; c < grid.c; c++) {
265
+ const k = .45 + (r * 37 + c * 53) % 51 / 100;
266
+ const lc = deep.map((v, i) => Math.round(v + (lightMax[i] - v) * k));
267
+ const flashDelay = ((r * 7 + c * 13) % 29 * .034).toFixed(3) + "s";
268
+ const fx = grid.c > 1 ? c / (grid.c - 1) : 0;
269
+ const appearDelay = (revealMs * (integrate(grid.c > 1 ? (grid.c - 1 - c) / (grid.c - 1) : 0, phases[r]) / fulls[r]) / 1e3).toFixed(3) + "s";
270
+ out.push({
271
+ key: r + "-" + c,
272
+ flashLight: "rgb(" + lc[0] + "," + lc[1] + "," + lc[2] + ")",
273
+ animationDelay: flashDelay,
274
+ appearDelay,
275
+ fade: fadeAt(fx)
276
+ });
277
+ }
278
+ return out;
279
+ }, [inMax, grid]);
280
+ react.useEffect(() => {
281
+ const id = setTimeout(() => setSettledIdx(safePosIdx), 160);
282
+ return () => clearTimeout(id);
283
+ }, [safePosIdx]);
284
+ if (runtimeSessions.subagentAddress(sessionId) !== void 0) return null;
285
+ const insetPx = HANDLE_W / 2;
286
+ const posFrac = posPct / 100;
287
+ const dotLeft = (i) => "calc(10px + (100% - 20px) * " + (choices.length > 1 ? i / (choices.length - 1) : 0) + ")";
288
+ const handleLeft = "calc(10px + (100% - 20px) * " + posFrac + ")";
289
+ const fillWidth = handleLeft;
290
+ const fillHidden = inMax && atMaxPos;
291
+ const fillSolid = "color-mix(in srgb, var(--dsw-alias-label-primary) 42%, transparent)";
292
+ const onPickModel = (m, g) => {
293
+ setSettledIdx(null);
294
+ const remembered = effortMemory.get(g.id + "\0" + m.id);
295
+ apply({
296
+ provider: g.id,
297
+ model: m.id,
298
+ ...remembered === void 0 ? {} : { reasoningEffort: remembered }
299
+ }, false);
300
+ };
301
+ const onPickEffort = (effort) => {
302
+ if (current === null) return;
303
+ apply({
304
+ provider: current.provider,
305
+ model: current.model,
306
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
307
+ }, true);
308
+ };
309
+ const idxFromEvent = (ev) => {
310
+ const el = trackRef.current;
311
+ if (el === null || choices.length < 2) return 0;
312
+ const rect = el.getBoundingClientRect();
313
+ const usable = Math.max(rect.width - insetPx * 2, 1);
314
+ const frac = Math.max(0, Math.min(1, (ev.clientX - rect.left - insetPx) / usable));
315
+ const idx = Math.round(frac * (choices.length - 1));
316
+ return Math.max(0, Math.min(choices.length - 1, idx));
317
+ };
318
+ const onWinMove = (ev) => {
319
+ if (dragIdxRef.current === null) return;
320
+ const idx = idxFromEvent(ev);
321
+ dragIdxRef.current = idx;
322
+ setHoverIdx(idx);
323
+ };
324
+ const stopDrag = () => {
325
+ window.removeEventListener("pointermove", onWinMove);
326
+ window.removeEventListener("pointerup", onWinUp);
327
+ window.removeEventListener("pointercancel", onWinCancel);
328
+ window.removeEventListener("blur", onWinCancel);
329
+ };
330
+ const onWinUp = () => {
331
+ if (dragIdxRef.current === null) return;
332
+ const idx = dragIdxRef.current;
333
+ dragIdxRef.current = null;
334
+ stopDrag();
335
+ const c = choices[idx];
336
+ if (c !== void 0) {
337
+ setHoverIdx(idx);
338
+ onPickEffort(c.effort);
339
+ }
340
+ };
341
+ const onWinCancel = () => {
342
+ dragIdxRef.current = null;
343
+ stopDrag();
344
+ setHoverIdx(null);
345
+ };
346
+ const onTrackDown = (ev) => {
347
+ if (choices.length === 0) return;
348
+ dragIdxRef.current = idxFromEvent(ev);
349
+ setHoverIdx(dragIdxRef.current);
350
+ window.addEventListener("pointermove", onWinMove);
351
+ window.addEventListener("pointerup", onWinUp);
352
+ window.addEventListener("pointercancel", onWinCancel);
353
+ window.addEventListener("blur", onWinCancel);
354
+ };
355
+ const triggerTitle = curGroup !== void 0 && curModel !== void 0 ? curGroup.name + " / " + curModel.name + (effectiveEffort !== void 0 ? " · " + displayLabel : "") : "选择模型";
356
+ return react.createElement("div", {
357
+ ref: rootRef,
358
+ className: "sk5-root"
359
+ }, react.createElement("div", { className: "sk5-triggers" }, react.createElement("button", {
360
+ type: "button",
361
+ className: "sk5-trigger",
362
+ disabled: locked || busy,
363
+ title: triggerTitle,
364
+ "aria-haspopup": "menu",
365
+ "aria-expanded": menu === "model",
366
+ onClick: () => setMenu((m) => m === "model" ? null : "model")
367
+ }, react.createElement("span", { className: "sk5-triggerName" }, modelLabel)), reasoning !== void 0 && choices.length > 0 ? react.createElement("button", {
368
+ type: "button",
369
+ className: "sk5-trigger sk5-triggerEffort",
370
+ disabled: locked || busy || current === null,
371
+ title: "思考强度",
372
+ "aria-haspopup": "menu",
373
+ "aria-expanded": menu === "effort",
374
+ onClick: () => setMenu((m) => m === "effort" ? null : "effort")
375
+ }, react.createElement("span", {
376
+ key: displayLabel,
377
+ style: { animation: "sk5-nameBlur .28s ease" }
378
+ }, displayLabel)) : null), menu === "model" ? react.createElement("div", {
379
+ className: "sk5-drop sk5-model",
380
+ role: "menu",
381
+ "aria-label": "选择模型"
382
+ }, react.createElement("div", { className: "sk5-modelsTitle" }, "Models"), react.createElement("div", { className: "sk5-list" }, data === null && error === null ? react.createElement("div", { className: "sk5-status" }, "加载中…") : null, data === null && error !== null ? react.createElement("div", { className: "sk5-err" }, error) : null, data !== null && groups.length === 0 ? react.createElement("div", { className: "sk5-status" }, "暂无可用模型") : null, data !== null ? groups.map((g) => react.createElement("div", { key: g.id }, react.createElement("div", { className: "sk5-group" }, g.name), g.models.map((m) => {
383
+ const active = current !== null && current.provider === g.id && current.model === m.id;
384
+ return react.createElement("button", {
385
+ key: m.id,
386
+ type: "button",
387
+ role: "menuitem",
388
+ className: "sk5-row",
389
+ disabled: busy,
390
+ onClick: () => onPickModel(m, g)
391
+ }, react.createElement("span", { style: { minWidth: 0 } }, react.createElement("div", null, m.name), m.description !== void 0 ? react.createElement("div", { className: "sk5-rowDesc" }, m.description) : null), active ? react.createElement("span", { className: "sk5-check" }, "✓") : null);
392
+ }))) : null)) : null, menu === "effort" ? react.createElement("div", {
393
+ className: "sk5-drop sk5-effort",
394
+ role: "menu",
395
+ "aria-label": "思考强度"
396
+ }, react.createElement("div", { className: "sk5-titleRow" }, react.createElement("span", { className: "sk5-effortLabel" }, "Effort"), react.createElement("span", {
397
+ key: displayLabel,
398
+ className: "sk5-effortValue" + (choices.length > 1 && inMax ? " sk5-effortValueMax" : ""),
399
+ style: { animation: "sk5-nameBlur .28s ease" }
400
+ }, displayLabel), react.createElement("div", { className: "sk5-help" }, react.createElement("button", {
401
+ type: "button",
402
+ className: "sk5-helpBtn",
403
+ "aria-label": "思考强度说明"
404
+ }, "?"), react.createElement("div", { className: "sk5-helpTip" }, react.createElement("div", { className: "sk5-helpTitle" }, "思考强度"), react.createElement("div", { className: "sk5-helpText" }, "调节模型推理深度,低强度回复快、内容简洁;高强度思考更全面,适合复杂问题,响应会稍慢。")))), react.createElement("div", { className: "sk5-labelsRow" }, react.createElement("span", { className: "sk5-endLabel" }, "Faster"), react.createElement("span", { className: "sk5-endLabel" }, "Smarter")), react.createElement("div", {
405
+ ref: trackRef,
406
+ className: "sk5-track",
407
+ onPointerDown: onTrackDown
408
+ }, react.createElement("div", {
409
+ className: "sk5-fill",
410
+ style: {
411
+ width: fillWidth,
412
+ background: fillSolid,
413
+ opacity: fillHidden ? 0 : 1,
414
+ borderRadius: posPct > 96 ? 6 : "6px 0 0 6px"
415
+ }
416
+ }), matrixVisible && grid.c > 0 && grid.r > 0 ? react.createElement("div", {
417
+ className: "sk5-matrix",
418
+ style: {
419
+ gridTemplateColumns: "repeat(" + grid.c + ",4px)",
420
+ gridTemplateRows: "repeat(" + grid.r + ",4px)"
421
+ }
422
+ }, matrixCells.map((sq) => react.createElement("div", {
423
+ key: sq.key,
424
+ className: "sk5-cell",
425
+ style: { animationDelay: sq.appearDelay }
426
+ }, react.createElement("div", {
427
+ className: "sk5-sq",
428
+ style: {
429
+ background: "var(--dsw-static-deepseek-500)",
430
+ opacity: sq.fade,
431
+ animationDelay: sq.animationDelay,
432
+ "--flash-light": sq.flashLight
433
+ }
434
+ })))) : null, !inMax ? choices.map((c, i) => react.createElement("span", {
435
+ key: c.key,
436
+ className: "sk5-dotHit",
437
+ style: { left: dotLeft(i) }
438
+ }, react.createElement("span", { className: "sk5-dot" }))) : null, react.createElement("div", {
439
+ className: "sk5-handle" + (inMax ? " sk5-handleMax" : ""),
440
+ style: { left: handleLeft }
441
+ })), error !== null ? react.createElement("div", { className: "sk5-err" }, error) : null) : null);
442
+ }));
443
+ }
444
+ //#endregion
445
+ exports.apply = apply;
446
+ exports.inject = inject;
447
+ return module.exports;
448
+ }
449
+ });
450
+
451
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Skin-switcher plugin, node half. Pure UI plugin: the empty apply exists so
4
+ * the plugin appears in the host cordis.yml / Loader; the browser half ships
5
+ * via exports["./client"], discovered through the package.json dsh.client
6
+ * declaration.
7
+ */
8
+ /** Host plugin body — no host-side behavior for this surface plugin. */
9
+ function apply() {}
10
+ //#endregion
11
+ export { apply };
@@ -0,0 +1,24 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@domitor-syh/dsh-ui-skin-switcher`.
4
+ * @module @domitor-syh/dsh-ui-skin-switcher/invariant
5
+ */
6
+ const PACKAGE_NAME = "@domitor-syh/dsh-ui-skin-switcher";
7
+ /** Cordis companion plugin name. */
8
+ const name = "client-ui-skin-switcher-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: a pure UI surface plugin whose slot registration
13
+ * disposal is proven by the HMR-safety contract — it emits no cordis events and
14
+ * owns no 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,11 @@
1
+ /** Required services: the connection RPC face (sessions) and the slot registry. */
2
+ /** Required services: the connection RPC face (sessions models/selectModel), the runtime sessions service (subagentAddress), and the slot registry. */
3
+ export declare const inject: string[];
4
+ /**
5
+ * Client plugin body: inject the stylesheet once, then register the composer
6
+ * model seat whose component reads `sessions.models` and writes
7
+ * `sessions.selectModel`.
8
+ * @param ctx - client root context carrying connection and slots.
9
+ */
10
+ export declare function apply(ctx: any): void;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Skin-switcher plugin, node half. Pure UI plugin: the empty apply exists so
3
+ * the plugin appears in the host cordis.yml / Loader; the browser half ships
4
+ * via exports["./client"], discovered through the package.json dsh.client
5
+ * 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 `@domitor-syh/dsh-ui-skin-switcher`.
3
+ * @module @domitor-syh/dsh-ui-skin-switcher/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "client-ui-skin-switcher-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,78 @@
1
+ {
2
+ "name": "@domitor-syh/dsh-ui-skin-switcher",
3
+ "description": "Model and reasoning-effort switcher: composer seat with an Off/Max effort slider over session.models / session.selectModel",
4
+ "version": "0.1.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/client/ui-skin-switcher"
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
+ "bundle": {
34
+ "patch": "./cordis.patch.yml"
35
+ },
36
+ "client": {
37
+ "inject": [
38
+ "@deepseek-ai/dsh-client-connection",
39
+ "@deepseek-ai/dsh-client-ui-conversation",
40
+ "@deepseek-ai/dsh-client-ui-slots"
41
+ ],
42
+ "platform": "web"
43
+ }
44
+ },
45
+ "license": "MIT",
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "^4.0.1",
48
+ "@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
49
+ "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
50
+ "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
51
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
52
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
53
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
54
+ "react": "^18.2.0"
55
+ },
56
+ "devDependencies": {
57
+ "@deepseek-ai/cordis": "^4.0.1",
58
+ "@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
59
+ "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
60
+ "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
61
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
62
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
63
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
64
+ "@types/react": "~18.3.1",
65
+ "react": "^18.2.0"
66
+ },
67
+ "files": [
68
+ "lib/index.js",
69
+ "lib/invariant.js",
70
+ "lib/client.js",
71
+ "lib/types/**/*.d.ts",
72
+ "cordis.patch.yml"
73
+ ],
74
+ "scripts": {
75
+ "bundle": "tsdown",
76
+ "watch": "tsdown --watch"
77
+ }
78
+ }