dsh-reasoning-glow 0.0.0-stage → 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 The dsh-reasoning-glow authors
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 CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # dsh-reasoning-glow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把 DeepSeek Harness(DSH)输入框里的模型选择器,换成一条会发光的推理强度滑块。
4
+
5
+ A glowing reasoning-effort slider for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) composer: drag the pill, the effort changes, and the top level lights up with drifting motes.
6
+
7
+ ![dsh-reasoning-glow](https://raw.githubusercontent.com/WH-kt/dsh-reasoning-glow/main/docs/screenshot.webp)
8
+
9
+ ## 特性 / Features
10
+
11
+ - **胶囊轨道 + 圆形拖钮**:拖动、点击轨道、`←` `→` `↑` `↓` `Home` `End` 都能切换档位,松手才真正提交
12
+ - **最高档特效**:轨道切换为蓝紫渐变,18 颗白色粒子各自上浮、摆动、明暗呼吸
13
+ - **档位标题实时跟随**:拖动过程中标题立即切换到对应档位名
14
+ - **↺ 恢复默认**:一键回到该模型服务商提供的默认档位
15
+ - **模型列表仍在**:点模型名展开分组模型列表,换模型后自动带回当前档位
16
+ - 深浅色主题自动适配;中英文界面;`prefers-reduced-motion` 时粒子静止
17
+ - 单文件浏览器端代码,无构建步骤,无第三方依赖
18
+
19
+ ## 安装 / Install
20
+
21
+ ### 通过 CLI(npm 发布后)
22
+
23
+ ```sh
24
+ dsh plugin add dsh-reasoning-glow
25
+ ```
26
+
27
+ ### 从源码
28
+
29
+ ```sh
30
+ git clone https://github.com/WH-kt/dsh-reasoning-glow.git
31
+ dsh plugin add ./dsh-reasoning-glow
32
+ ```
33
+
34
+ 或者:DSH 桌面端 → 设置 → 插件 → 用插件管理器安装这个本地目录。
35
+
36
+ 安装后插件作为 profile 层生效,**刷新页面**即可看到新的滑块。
37
+
38
+ ## 兼容性 / Compatibility
39
+
40
+ - DSH `0.2.1-rc.2`(Web 与桌面端),Node.js `>=22`
41
+ - 通过 `conversation.input.model` 槽位(`priority: -1`)接管模型选择器
42
+ - 依赖 Client 服务:`slots`、`modelDirectories`、`sessions`、`locale`、`remote`、`remote.session`
43
+ - 模型目录通过槽位的 `inject(sessionId)` 获取,不做任何模型或认证上的假设
44
+
45
+ ## 开发 / Development
46
+
47
+ ```sh
48
+ npm test # test/smoke.mjs:32 项运行时契约 + 渲染检查,不需要浏览器
49
+ ```
50
+
51
+ ```
52
+ .
53
+ ├── client.js # 浏览器端:UI、样式、槽位注册(无构建)
54
+ ├── index.js # Host 端空实现,让加载器看到这个插件
55
+ ├── cordis.patch.yml # bundle 层:把插件插入 profile
56
+ ├── locale/{zh,en}.json # 插件管理器中显示的标题/描述
57
+ ├── icon.svg # 插件图标
58
+ └── test/smoke.mjs # 无浏览器冒烟测试
59
+ ```
60
+
61
+ `test/smoke.mjs` 会用假的 `window.__ModuleLoader__`、React 和 Cordis 上下文加载真实
62
+ `client.js`,验证注入清单、槽位注册契约、`load/select` 通路、滑块提交、模型列表渲染
63
+ 以及最高档的渐变/粒子分支。
64
+
65
+ ## 许可 / License
66
+
67
+ [MIT](LICENSE)
package/client.js ADDED
@@ -0,0 +1,778 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-reasoning-glow",
3
+ factory: (require) => {
4
+ const React = require("react");
5
+ let ReactDOM = null;
6
+ try {
7
+ ReactDOM = require("react-dom");
8
+ } catch {
9
+ ReactDOM = null;
10
+ }
11
+ const h = React.createElement;
12
+
13
+ /** Locale namespace owned by this plugin. */
14
+ const NS = "reasoning-glow";
15
+ /** Slider track height; the thumb is a slightly larger circle so it reads as one. */
16
+ const TRACK = 30;
17
+ /** Slider thumb diameter. */
18
+ const THUMB = 34;
19
+ const HALF = THUMB / 2;
20
+
21
+ /* ------------------------------------------------------------------ */
22
+ /* Styles */
23
+ /* ------------------------------------------------------------------ */
24
+ const CSS = `
25
+ .rg-root { position: relative; display: inline-flex; min-width: 0; }
26
+ .rg-trigger { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 230px; height: 28px; padding: 0 4px 0 8px; border: none; border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer; outline: none; }
27
+ .rg-trigger:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 12%, transparent); }
28
+ .rg-trigger:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, #3aa3ff 45%, transparent); }
29
+ .rg-trigger:disabled { cursor: default; opacity: .55; }
30
+ .rg-name { display: var(--dsh-composer-model-text-display, inline); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31
+ .rg-effort { flex: none; color: #3aa3ff; font-weight: 600; }
32
+ .rg-effort-max { background: linear-gradient(96deg, #7FA6FF 0%, #A99BFF 55%, #C08CFF 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
33
+ .rg-chev { flex: none; opacity: .7; transition: transform .16s ease; }
34
+ .rg-chev-open { transform: rotate(180deg); }
35
+
36
+ .rg-card { --rg-surface: color-mix(in srgb, var(--dsw-alias-bg-base) 82%, var(--dsw-alias-bg-overlay)); position: fixed; z-index: 60; box-sizing: border-box; width: min(264px, calc(100vw - 24px)); padding: 7px 10px 10px; border-radius: 16px; background: var(--rg-surface); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent); box-shadow: 0 20px 44px rgba(0, 0, 0, .34), 0 2px 10px rgba(0, 0, 0, .18); color: var(--dsw-alias-label-primary); font-family: inherit; animation: rg-pop .14s ease-out both; }
37
+ @keyframes rg-pop { from { opacity: 0; transform: translateY(4px) scale(.99); } to { opacity: 1; transform: none; } }
38
+
39
+ .rg-head { position: relative; padding-top: 2px; }
40
+ .rg-title { text-align: center; font-size: 15px; font-weight: 600; line-height: 20px; letter-spacing: .2px; color: #3aa3ff; }
41
+ .rg-title-max { background: linear-gradient(96deg, #8AA9FF 0%, #A99BFF 45%, #C48CFF 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
42
+ .rg-title-muted { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; }
43
+ .rg-reset { position: absolute; top: -3px; right: -4px; display: grid; place-items: center; width: 22px; height: 22px; padding: 0; border: none; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; }
44
+ .rg-reset:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 14%, transparent); color: var(--dsw-alias-label-primary); }
45
+ .rg-reset:disabled { opacity: .35; cursor: default; }
46
+ .rg-model { display: flex; align-items: center; justify-content: center; gap: 1px; max-width: 100%; margin: 0 auto 9px; padding: 2px 6px; border: none; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; }
47
+ .rg-model:hover { color: var(--dsw-alias-label-primary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 10%, transparent); }
48
+ .rg-modelName { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
49
+ .rg-modelChev { flex: none; opacity: .8; }
50
+
51
+ .rg-track { position: relative; height: ${TRACK}px; border-radius: ${TRACK / 2}px; touch-action: none; cursor: grab; outline: none; }
52
+ .rg-track-drag { cursor: grabbing; }
53
+ .rg-track-disabled { opacity: .55; cursor: default; }
54
+ .rg-rail { position: absolute; inset: 0; border-radius: ${TRACK / 2}px; background: color-mix(in srgb, var(--dsw-alias-label-secondary) 20%, transparent); }
55
+ .rg-fill { position: absolute; left: 0; top: 0; bottom: 0; overflow: hidden; border-radius: ${TRACK / 2}px; background: linear-gradient(94deg, #2E8BFF 0%, #3FA5FF 100%); transition: width .2s cubic-bezier(.22, .9, .25, 1); }
56
+ .rg-fill-max { background: linear-gradient(94deg, #7CA4FF 0%, #93A2FF 12%, #A2A6FF 24%, #ABA8FF 34%, #B2A3FE 45%, #AF99FE 56%, #9766E4 68%, #8A5AD8 78%, #714DC7 90%, #6752C9 100%); }
57
+ .rg-glow { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, 0) 62%); pointer-events: none; }
58
+ .rg-sparks { position: absolute; inset: 0; opacity: 0; transition: opacity .3s ease; pointer-events: none; }
59
+ .rg-fill-max .rg-sparks { opacity: 1; }
60
+ .rg-spark { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, .95); box-shadow: 0 0 5px rgba(255, 255, 255, .6); opacity: 0; animation: rg-float 4.6s ease-in-out infinite; }
61
+ @keyframes rg-float {
62
+ 0% { opacity: 0; transform: translate3d(0, 4px, 0) scale(.55); }
63
+ 15% { opacity: .95; }
64
+ 50% { opacity: .45; transform: translate3d(1.6px, -3px, 0) scale(1.06); }
65
+ 78% { opacity: .9; }
66
+ 100% { opacity: 0; transform: translate3d(-1.2px, -6px, 0) scale(.7); }
67
+ }
68
+ .rg-tick { position: absolute; top: calc(50% - 1.5px); width: 3px; height: 3px; margin-left: -1.5px; border-radius: 50%; background: color-mix(in srgb, var(--dsw-alias-label-secondary) 55%, transparent); transition: background .18s ease; }
69
+ .rg-tick-on { background: rgba(214, 236, 255, .88); }
70
+ .rg-thumb { position: absolute; top: ${(TRACK - THUMB) / 2}px; width: ${THUMB}px; height: ${THUMB}px; margin-left: -${HALF}px; border: none; border-radius: 50%; background: radial-gradient(circle closest-side, #ffffff 0 97%, rgba(255, 255, 255, 0) 100%); filter: drop-shadow(0 1px 1.5px rgba(10, 20, 40, .25)) drop-shadow(0 4px 10px rgba(10, 20, 40, .18)); transition: left .2s cubic-bezier(.22, .9, .25, 1), transform .12s ease, filter .12s ease; }
71
+ .rg-thumb-drag { transform: scale(1.05); filter: drop-shadow(0 2px 3px rgba(10, 20, 40, .28)) drop-shadow(0 8px 18px rgba(10, 20, 40, .24)); }
72
+ .rg-track-drag .rg-fill, .rg-track-drag .rg-thumb { transition: none; }
73
+ .rg-track:focus-visible .rg-thumb { filter: drop-shadow(0 0 2px rgba(58, 163, 255, .7)) drop-shadow(0 0 7px rgba(58, 163, 255, .45)) drop-shadow(0 4px 10px rgba(10, 20, 40, .18)); }
74
+
75
+ .rg-hint { margin: 4px 0 2px; text-align: center; font-size: 11.5px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
76
+ .rg-error { margin-top: 8px; padding: 5px 7px; border-radius: 8px; font-size: 11px; line-height: 15px; color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); }
77
+
78
+ .rg-paneHead { display: flex; align-items: center; gap: 4px; padding: 0 0 4px; }
79
+ .rg-back { display: grid; place-items: center; width: 22px; height: 22px; padding: 0; border: none; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; }
80
+ .rg-back:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 14%, transparent); color: var(--dsw-alias-label-primary); }
81
+ .rg-paneTitle { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); }
82
+ .rg-list { max-height: 190px; overflow-y: auto; margin: 0 -6px; padding: 0 6px 2px; }
83
+ .rg-groupTitle { position: sticky; top: 0; z-index: 1; padding: 6px 4px 4px; background: var(--rg-surface, var(--dsw-alias-bg-overlay)); font-size: 10.5px; line-height: 14px; color: var(--dsw-alias-label-secondary); }
84
+ .rg-group + .rg-group { margin-top: 2px; }
85
+ .rg-row { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 29px; padding: 3px 7px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; text-align: left; cursor: pointer; }
86
+ .rg-row:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 10%, transparent); }
87
+ .rg-row:disabled { cursor: default; opacity: .6; }
88
+ .rg-rowName { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
89
+ .rg-check { flex: none; color: #3aa3ff; }
90
+ .rg-status { padding: 9px 6px 5px; font-size: 11.5px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
91
+
92
+ @media (prefers-reduced-motion: reduce) {
93
+ .rg-card { animation: none; }
94
+ .rg-fill, .rg-thumb, .rg-chev, .rg-sparks, .rg-tick { transition: none; }
95
+ .rg-spark { animation: none; opacity: .85; }
96
+ }
97
+ `;
98
+
99
+ /* ------------------------------------------------------------------ */
100
+ /* Dictionaries */
101
+ /* ------------------------------------------------------------------ */
102
+ const zh = {
103
+ "trigger.fallback": "选择模型",
104
+ "trigger.loading": "正在加载模型…",
105
+ "trigger.aria": "选择模型与推理强度,当前 {model} · {effort}",
106
+ "trigger.ariaModel": "选择模型与推理强度,当前 {model}",
107
+ "card.aria": "推理强度",
108
+ "effort.title": "推理强度",
109
+ "effort.default": "默认",
110
+ "effort.reset": "恢复默认档位",
111
+ "effort.none": "当前模型未提供推理等级",
112
+ "effort.single": "当前模型仅支持 {name}",
113
+ "models.title": "模型",
114
+ "models.back": "返回",
115
+ "models.loading": "正在刷新模型列表…",
116
+ "models.empty": "暂无可用模型",
117
+ "models.retry": "重试加载模型",
118
+ "provider.account": "DeepSeek 账号",
119
+ "error.generic": "切换失败,请重试",
120
+ "error.action": "切换失败:{message}"
121
+ };
122
+ const en = {
123
+ "trigger.fallback": "Select model",
124
+ "trigger.loading": "Loading models…",
125
+ "trigger.aria": "Select model and reasoning effort, currently {model} · {effort}",
126
+ "trigger.ariaModel": "Select model and reasoning effort, currently {model}",
127
+ "card.aria": "Reasoning effort",
128
+ "effort.title": "Reasoning effort",
129
+ "effort.default": "Default",
130
+ "effort.reset": "Reset to the default level",
131
+ "effort.none": "This model provides no reasoning levels",
132
+ "effort.single": "This model only supports {name}",
133
+ "models.title": "Model",
134
+ "models.back": "Back",
135
+ "models.loading": "Refreshing models…",
136
+ "models.empty": "No models available",
137
+ "models.retry": "Retry loading models",
138
+ "provider.account": "DeepSeek Account",
139
+ "error.generic": "Could not switch, try again",
140
+ "error.action": "Could not switch: {message}"
141
+ };
142
+
143
+ function interpolate(template, vars) {
144
+ if (vars === undefined) return template;
145
+ return String(template).replace(/\{(\w+)\}/g, (match, key) => vars[key] === undefined ? match : String(vars[key]));
146
+ }
147
+
148
+ function useT(locale, provided) {
149
+ const read = () => {
150
+ try {
151
+ const snapshot = locale && typeof locale.getLocale === "function" ? locale.getLocale() : null;
152
+ return snapshot && typeof snapshot.id === "string" ? snapshot.id : "zh";
153
+ } catch {
154
+ return "zh";
155
+ }
156
+ };
157
+ const [localeId, setLocaleId] = React.useState(read);
158
+ React.useEffect(() => {
159
+ if (!locale || typeof locale.subscribe !== "function") return;
160
+ const update = () => setLocaleId(read());
161
+ const dispose = locale.subscribe(update);
162
+ return typeof dispose === "function" ? dispose : undefined;
163
+ }, [locale]);
164
+ const dict = String(localeId).toLowerCase().startsWith("en") ? en : zh;
165
+ const fallback = React.useCallback((key, vars) => interpolate(dict[key] !== undefined ? dict[key] : key, vars), [dict]);
166
+ return typeof provided === "function" ? provided : fallback;
167
+ }
168
+
169
+ /* ------------------------------------------------------------------ */
170
+ /* Icons (inline, original paths) */
171
+ /* ------------------------------------------------------------------ */
172
+ const svg = (children, size) => h("svg", {
173
+ viewBox: "0 0 24 24",
174
+ width: size || 16,
175
+ height: size || 16,
176
+ fill: "none",
177
+ stroke: "currentColor",
178
+ strokeWidth: 2,
179
+ strokeLinecap: "round",
180
+ strokeLinejoin: "round",
181
+ "aria-hidden": "true",
182
+ focusable: "false"
183
+ }, children);
184
+ const chevronDown = svg(h("path", { d: "M6 9l6 6 6-6" }), 14);
185
+ const chevronRight = svg(h("path", { d: "M9 6l6 6-6 6" }), 13);
186
+ const chevronLeft = svg(h("path", { d: "M15 6l-6 6 6 6" }), 15);
187
+ const resetIcon = svg([
188
+ h("path", { d: "M4.6 9.2A8 8 0 1 1 4 14.2", key: "a" }),
189
+ h("path", { d: "M4.2 4.4v5h5", key: "b" })
190
+ ], 15);
191
+ const checkIcon = svg(h("path", { d: "M20 6L9 17l-5-5" }), 15);
192
+
193
+ /* ------------------------------------------------------------------ */
194
+ /* Decorative sparks for the top level */
195
+ /* ------------------------------------------------------------------ */
196
+ const SPARKS = [
197
+ [5, 32, 2.4, .20, 4.8], [9, 62, 1.8, .70, 5.6], [14, 34, 2.8, .45, 4.2],
198
+ [19, 70, 1.6, .90, 5.9], [24, 44, 2.2, .10, 5.2], [29, 76, 2.6, .60, 4.6],
199
+ [34, 33, 1.8, .35, 6.1], [39, 58, 2.4, .80, 4.4], [44, 38, 1.6, .05, 5.4],
200
+ [50, 68, 2.8, .55, 4.9], [55, 34, 1.8, .95, 5.8], [60, 52, 2.2, .30, 4.3],
201
+ [65, 74, 1.6, .70, 6.2], [70, 36, 2.6, .15, 5.1], [75, 60, 1.8, .50, 4.7],
202
+ [81, 34, 2.2, .85, 5.5], [86, 66, 2.6, .40, 4.1], [92, 42, 1.8, .60, 5.0]
203
+ ].map((entry) => ({
204
+ left: entry[0] + "%",
205
+ top: entry[1] + "%",
206
+ size: entry[2],
207
+ delay: -(entry[3] * entry[4]).toFixed(2),
208
+ duration: entry[4]
209
+ }));
210
+
211
+ const trackPos = (ratio) => "calc((100% - " + THUMB + "px) * " + ratio + " + " + HALF + "px)";
212
+
213
+ /* ------------------------------------------------------------------ */
214
+ /* Slider */
215
+ /* ------------------------------------------------------------------ */
216
+ function EffortSlider(props) {
217
+ const trackRef = React.useRef(null);
218
+ const draggingRef = React.useRef(false);
219
+ const [dragging, setDragging] = React.useState(false);
220
+ const efforts = props.efforts;
221
+ const last = Math.max(0, efforts.length - 1);
222
+ const shown = Math.max(0, Math.min(last, Math.round(props.value)));
223
+ const ratio = last === 0 ? 0 : Math.max(0, Math.min(1, props.value / last));
224
+ const atMax = props.value >= last - 0.0001;
225
+
226
+ const positionOf = (clientX) => {
227
+ const el = trackRef.current;
228
+ if (el === null || last === 0) return 0;
229
+ const rect = el.getBoundingClientRect();
230
+ const travel = rect.width - THUMB;
231
+ if (travel <= 0) return 0;
232
+ return Math.max(0, Math.min(last, ((clientX - rect.left - HALF) / travel) * last));
233
+ };
234
+ const clamp = (position) => Math.max(0, Math.min(last, position));
235
+ const snap = (position) => Math.max(0, Math.min(last, Math.round(position)));
236
+ const magnetize = (position) => {
237
+ const nearest = snap(position);
238
+ return Math.abs(position - nearest) <= 0.12 ? nearest : clamp(position);
239
+ };
240
+ const release = (event) => {
241
+ if (!draggingRef.current) return;
242
+ draggingRef.current = false;
243
+ setDragging(false);
244
+ const target = clamp(positionOf(event.clientX));
245
+ props.onPreview(target);
246
+ props.onCommit(snap(target));
247
+ };
248
+ const down = (event) => {
249
+ if (props.disabled) return;
250
+ if (typeof event.button === "number" && event.button !== 0) return;
251
+ event.preventDefault();
252
+ draggingRef.current = true;
253
+ setDragging(true);
254
+ if (typeof event.currentTarget.setPointerCapture === "function") {
255
+ try { event.currentTarget.setPointerCapture(event.pointerId); } catch { /* pointer capture is best-effort */ }
256
+ }
257
+ props.onPreview(magnetize(positionOf(event.clientX)));
258
+ };
259
+ const move = (event) => {
260
+ if (!draggingRef.current) return;
261
+ props.onPreview(magnetize(positionOf(event.clientX)));
262
+ };
263
+ const key = (event) => {
264
+ if (props.disabled) return;
265
+ let next = null;
266
+ if (event.key === "ArrowRight" || event.key === "ArrowUp") next = Math.min(last, shown + 1);
267
+ else if (event.key === "ArrowLeft" || event.key === "ArrowDown") next = Math.max(0, shown - 1);
268
+ else if (event.key === "Home") next = 0;
269
+ else if (event.key === "End") next = last;
270
+ if (next === null || next === shown) return;
271
+ event.preventDefault();
272
+ props.onPreview(next);
273
+ props.onCommit(next);
274
+ };
275
+
276
+ return h("div", {
277
+ ref: trackRef,
278
+ className: "rg-track" + (dragging ? " rg-track-drag" : "") + (props.disabled ? " rg-track-disabled" : ""),
279
+ role: "slider",
280
+ tabIndex: props.disabled ? -1 : 0,
281
+ "aria-label": props.label,
282
+ "aria-valuemin": 0,
283
+ "aria-valuemax": last,
284
+ "aria-valuenow": shown,
285
+ "aria-valuetext": efforts[shown] ? efforts[shown].name : undefined,
286
+ "aria-disabled": props.disabled ? true : undefined,
287
+ onPointerDown: down,
288
+ onPointerMove: move,
289
+ onPointerUp: release,
290
+ onPointerCancel: release,
291
+ onKeyDown: key
292
+ },
293
+ h("div", { className: "rg-rail" }),
294
+ h("div", {
295
+ className: "rg-fill" + (atMax ? " rg-fill-max" : ""),
296
+ style: { width: trackPos(ratio) }
297
+ },
298
+ h("div", { className: "rg-glow" }),
299
+ h("div", { className: "rg-sparks" }, SPARKS.map((spark, index) => h("span", {
300
+ key: index,
301
+ className: "rg-spark",
302
+ style: {
303
+ left: spark.left,
304
+ top: spark.top,
305
+ width: spark.size + "px",
306
+ height: spark.size + "px",
307
+ animationDelay: spark.delay + "s",
308
+ animationDuration: spark.duration + "s"
309
+ }
310
+ })))),
311
+ efforts.map((effort, index) => h("span", {
312
+ key: effort.id,
313
+ className: "rg-tick" + (index <= shown ? " rg-tick-on" : ""),
314
+ style: { left: trackPos(last === 0 ? 0 : index / last) }
315
+ })),
316
+ h("div", {
317
+ className: "rg-thumb" + (dragging ? " rg-thumb-drag" : ""),
318
+ style: { left: trackPos(ratio) }
319
+ })
320
+ );
321
+ }
322
+
323
+ /* ------------------------------------------------------------------ */
324
+ /* Error boundary: never blank the composer permanently */
325
+ /* ------------------------------------------------------------------ */
326
+ class Boundary extends React.Component {
327
+ constructor(props) {
328
+ super(props);
329
+ this.state = { failed: false };
330
+ }
331
+ static getDerivedStateFromError() {
332
+ return { failed: true };
333
+ }
334
+ componentDidCatch(error) {
335
+ if (typeof console !== "undefined" && typeof console.error === "function") console.error("reasoning-glow: model control failed to render", error);
336
+ }
337
+ render() {
338
+ return this.state.failed ? null : this.props.children;
339
+ }
340
+ }
341
+
342
+ /* ------------------------------------------------------------------ */
343
+ /* Seat */
344
+ /* ------------------------------------------------------------------ */
345
+ const EMPTY_STATE = Object.freeze({
346
+ status: "loading",
347
+ groups: [],
348
+ current: null,
349
+ retainedEffort: undefined,
350
+ pending: null,
351
+ error: null,
352
+ failures: []
353
+ });
354
+
355
+ function Seat(props) {
356
+ const t = useT(props.locale, props.t);
357
+ const sessionId = props.sessionId;
358
+ const available = props.available !== false;
359
+ const locked = props.locked === true;
360
+ const store = props.directory && typeof props.directory.subscribe === "function" ? props.directory : null;
361
+ const load = typeof props.load === "function" ? props.load : null;
362
+ const select = typeof props.select === "function" ? props.select : null;
363
+
364
+ const subscribe = React.useCallback((callback) => store === null ? () => {} : store.subscribe(callback), [store]);
365
+ const getSnapshot = React.useCallback(() => store === null ? EMPTY_STATE : store.getSnapshot() || EMPTY_STATE, [store]);
366
+ const state = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
367
+
368
+ const initialized = React.useRef(null);
369
+ React.useEffect(() => {
370
+ if (!available || load === null) return;
371
+ if (initialized.current === sessionId) return;
372
+ initialized.current = sessionId;
373
+ load();
374
+ }, [available, load, sessionId]);
375
+
376
+ const groups = Array.isArray(state.groups) ? state.groups : [];
377
+ const current = state.current || null;
378
+ let currentChoice = null;
379
+ if (current !== null) {
380
+ for (const group of groups) {
381
+ for (const model of group.models) {
382
+ if (group.id === current.provider && model.id === current.model) currentChoice = { group, model };
383
+ }
384
+ }
385
+ }
386
+ const reasoning = currentChoice ? currentChoice.model.reasoning : undefined;
387
+ const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts : [];
388
+ const defaultEffort = reasoning ? reasoning.defaultEffort : undefined;
389
+ const effectiveEffort = current !== null && current.reasoningEffort !== undefined ? current.reasoningEffort : defaultEffort;
390
+ const foundIndex = efforts.findIndex((level) => level.id === effectiveEffort);
391
+ const baseIndex = foundIndex >= 0 ? foundIndex : (efforts.length > 0 ? Math.floor(efforts.length / 2) : 0);
392
+ const busy = locked || state.pending !== null && state.pending !== undefined || state.status === "selecting";
393
+
394
+ const [preview, setPreview] = React.useState(null);
395
+ const [open, setOpen] = React.useState(false);
396
+ const [pane, setPane] = React.useState("root");
397
+ const [error, setError] = React.useState(null);
398
+ const [position, setPosition] = React.useState(null);
399
+ const rootRef = React.useRef(null);
400
+ const triggerRef = React.useRef(null);
401
+ const menuRef = React.useRef(null);
402
+
403
+ React.useEffect(() => {
404
+ setPreview(null);
405
+ }, [sessionId, foundIndex, efforts.length]);
406
+
407
+ React.useEffect(() => {
408
+ if (error === null) return;
409
+ const timer = setTimeout(() => setError(null), 6000);
410
+ return () => clearTimeout(timer);
411
+ }, [error]);
412
+
413
+ React.useEffect(() => {
414
+ if (!open) return;
415
+ const onPointer = (event) => {
416
+ const target = event.target;
417
+ if (!(target instanceof Node)) return;
418
+ if (rootRef.current && rootRef.current.contains(target)) return;
419
+ if (menuRef.current && menuRef.current.contains(target)) return;
420
+ setOpen(false);
421
+ setPane("root");
422
+ };
423
+ const onKey = (event) => {
424
+ if (event.key !== "Escape") return;
425
+ setOpen(false);
426
+ setPane("root");
427
+ if (triggerRef.current) triggerRef.current.focus();
428
+ };
429
+ document.addEventListener("mousedown", onPointer, true);
430
+ document.addEventListener("keydown", onKey, true);
431
+ return () => {
432
+ document.removeEventListener("mousedown", onPointer, true);
433
+ document.removeEventListener("keydown", onKey, true);
434
+ };
435
+ }, [open]);
436
+
437
+ React.useLayoutEffect(() => {
438
+ if (!open) {
439
+ setPosition(null);
440
+ return;
441
+ }
442
+ const place = () => {
443
+ const trigger = triggerRef.current;
444
+ if (trigger === null) return;
445
+ const rect = trigger.getBoundingClientRect();
446
+ const card = menuRef.current;
447
+ const width = card ? card.offsetWidth : 264;
448
+ const height = card ? card.offsetHeight : 170;
449
+ const margin = 12;
450
+ const left = Math.min(Math.max(rect.left, margin), Math.max(margin, window.innerWidth - width - margin));
451
+ let top = rect.top - 10 - height;
452
+ if (top < margin) top = Math.min(window.innerHeight - height - margin, rect.bottom + 10);
453
+ setPosition({ left: Math.round(left), top: Math.round(Math.max(margin, top)) });
454
+ };
455
+ place();
456
+ window.addEventListener("resize", place);
457
+ window.addEventListener("scroll", place, true);
458
+ let observer = null;
459
+ if (typeof ResizeObserver !== "undefined" && menuRef.current !== null) {
460
+ observer = new ResizeObserver(place);
461
+ observer.observe(menuRef.current);
462
+ }
463
+ return () => {
464
+ window.removeEventListener("resize", place);
465
+ window.removeEventListener("scroll", place, true);
466
+ if (observer !== null) observer.disconnect();
467
+ };
468
+ }, [open, pane]);
469
+
470
+ if (!available) return null;
471
+
472
+ const value = preview === null ? baseIndex : preview;
473
+ const shown = Math.max(0, Math.min(efforts.length - 1, Math.round(value)));
474
+ const atMax = efforts.length > 1 && value >= efforts.length - 1 - 0.0001;
475
+
476
+ const reportError = (cause) => {
477
+ let message = "";
478
+ if (typeof cause === "string") message = cause;
479
+ else if (cause && typeof cause === "object") {
480
+ if (cause.error && typeof cause.error.message === "string") message = cause.error.message;
481
+ else if (typeof cause.message === "string") message = cause.message;
482
+ }
483
+ setPreview(null);
484
+ setError(message ? t("error.action", { message }) : t("error.generic"));
485
+ };
486
+
487
+ const submit = (selection) => {
488
+ if (select === null) return;
489
+ let result;
490
+ try {
491
+ result = select(selection);
492
+ } catch (cause) {
493
+ reportError(cause);
494
+ return;
495
+ }
496
+ Promise.resolve(result).then((value) => {
497
+ if (value && value.ok === false) reportError(value.error);
498
+ }).catch(reportError);
499
+ };
500
+
501
+ const effortName = (effortId) => {
502
+ const level = efforts.find((item) => item.id === effortId);
503
+ return level ? level.name : undefined;
504
+ };
505
+ const effortLabel = reasoning === undefined
506
+ ? undefined
507
+ : effectiveEffort === undefined
508
+ ? t("effort.default")
509
+ : effortName(effectiveEffort) || t("effort.default");
510
+ const modelName = currentChoice ? currentChoice.model.name : (state.status === "loading" ? t("trigger.loading") : t("trigger.fallback"));
511
+
512
+ const labelFor = (provider, model, placement, selected) => {
513
+ const renderSlot = props.renderSlot;
514
+ if (typeof renderSlot !== "function") return model.name;
515
+ try {
516
+ const rendered = renderSlot("conversation.input.model.label", { provider, model, placement, selected }, { fallback: model.name });
517
+ return rendered === undefined || rendered === null ? model.name : rendered;
518
+ } catch {
519
+ return model.name;
520
+ }
521
+ };
522
+
523
+ const chooseEffort = (index) => {
524
+ if (current === null || busy) return;
525
+ const level = efforts[Math.round(index)];
526
+ if (!level) return;
527
+ if (level.id === effectiveEffort) {
528
+ setPreview(null);
529
+ return;
530
+ }
531
+ setPreview(Math.round(index));
532
+ const selection = { provider: current.provider, model: current.model, reasoningEffort: level.id };
533
+ submit(selection);
534
+ };
535
+
536
+ const canReset = current !== null && reasoning !== undefined && current.reasoningEffort !== undefined && current.reasoningEffort !== defaultEffort;
537
+ const resetEffort = () => {
538
+ if (!canReset || busy) return;
539
+ setPreview(null);
540
+ const selection = { provider: current.provider, model: current.model };
541
+ if (defaultEffort !== undefined) selection.reasoningEffort = defaultEffort;
542
+ submit(selection);
543
+ };
544
+
545
+ const chooseModel = (group, model) => {
546
+ if (current !== null && current.provider === group.id && current.model === model.id) {
547
+ setPane("root");
548
+ return;
549
+ }
550
+ const targetReasoning = model.reasoning;
551
+ const targetEfforts = targetReasoning && Array.isArray(targetReasoning.efforts) ? targetReasoning.efforts : [];
552
+ let effort;
553
+ if (current !== null && current.reasoningEffort !== undefined && targetEfforts.some((level) => level.id === current.reasoningEffort)) effort = current.reasoningEffort;
554
+ else if (targetReasoning && targetReasoning.defaultEffort !== undefined) effort = targetReasoning.defaultEffort;
555
+ else if (targetEfforts.length > 0) effort = targetEfforts[Math.floor(targetEfforts.length / 2)].id;
556
+ const selection = { provider: group.id, model: model.id };
557
+ if (effort !== undefined) selection.reasoningEffort = effort;
558
+ setPreview(null);
559
+ setPane("root");
560
+ submit(selection);
561
+ };
562
+
563
+ const triggerAria = effortLabel === undefined
564
+ ? t("trigger.ariaModel", { model: modelName })
565
+ : t("trigger.aria", { model: modelName, effort: effortLabel });
566
+
567
+ const providerOf = (group) => ({ id: group.id, name: group.id === "deepseek-account" ? t("provider.account") : group.name });
568
+
569
+ const slider = efforts.length > 1
570
+ ? h(EffortSlider, {
571
+ efforts,
572
+ value,
573
+ disabled: busy,
574
+ label: t("effort.title"),
575
+ onPreview: (index) => setPreview(index),
576
+ onCommit: (index) => chooseEffort(index)
577
+ })
578
+ : h("div", { className: "rg-hint" }, efforts.length === 1
579
+ ? t("effort.single", { name: efforts[0].name })
580
+ : t("effort.none"));
581
+
582
+ const loadError = state.error || (Array.isArray(state.failures) && state.failures.length > 0
583
+ ? state.failures.map((failure) => failure.error?.message || failure.message || String(failure.provider || failure.id || "")).join("; ")
584
+ : null);
585
+ const errorMessage = error || loadError;
586
+ const errorEl = errorMessage === null ? null : h("div", { className: "rg-error", role: "alert" },
587
+ errorMessage,
588
+ loadError ? h("button", {
589
+ type: "button", className: "rg-row",
590
+ onClick: () => { if (load !== null) load(); }
591
+ }, t("models.retry")) : null);
592
+ /** The title follows the dragged position, not only the committed level. */
593
+ const titleLabel = reasoning === undefined
594
+ ? t("effort.title")
595
+ : efforts.length > 0 && efforts[shown] !== undefined ? efforts[shown].name : effortLabel;
596
+
597
+ const rootPane = h(React.Fragment, null,
598
+ h("div", { className: "rg-head" },
599
+ h("button", {
600
+ type: "button",
601
+ className: "rg-reset",
602
+ title: t("effort.reset"),
603
+ "aria-label": t("effort.reset"),
604
+ disabled: !canReset || busy,
605
+ onClick: resetEffort
606
+ }, resetIcon),
607
+ h("div", {
608
+ className: "rg-title" + (reasoning === undefined ? " rg-title-muted" : atMax ? " rg-title-max" : ""),
609
+ "aria-live": "polite"
610
+ }, titleLabel),
611
+ h("button", {
612
+ type: "button",
613
+ className: "rg-model",
614
+ title: modelName,
615
+ onClick: () => setPane("models")
616
+ },
617
+ h("span", { className: "rg-modelName" }, currentChoice ? labelFor(providerOf(currentChoice.group), currentChoice.model, "menu", true) : modelName),
618
+ h("span", { className: "rg-modelChev" }, chevronRight))
619
+ ),
620
+ slider,
621
+ errorEl
622
+ );
623
+
624
+ const modelRows = groups.map((group) => h("div", { className: "rg-group", key: group.id },
625
+ h("div", { className: "rg-groupTitle" }, providerOf(group).name),
626
+ group.models.map((model) => {
627
+ const selected = current !== null && current.provider === group.id && current.model === model.id;
628
+ return h("button", {
629
+ key: model.id,
630
+ type: "button",
631
+ role: "menuitemradio",
632
+ "aria-checked": selected,
633
+ className: "rg-row",
634
+ title: model.name,
635
+ disabled: busy,
636
+ onClick: () => chooseModel(group, model)
637
+ },
638
+ h("span", { className: "rg-rowName" }, labelFor(providerOf(group), model, "option", selected)),
639
+ selected ? h("span", { className: "rg-check" }, checkIcon) : null);
640
+ })
641
+ ));
642
+
643
+ const listStatus = state.status === "loading" && groups.length === 0
644
+ ? h("div", { className: "rg-status" }, t("models.loading"))
645
+ : groups.length === 0
646
+ ? h("div", { className: "rg-status" }, t("models.empty"))
647
+ : null;
648
+
649
+ const modelsPane = h(React.Fragment, null,
650
+ h("div", { className: "rg-paneHead" },
651
+ h("button", {
652
+ type: "button",
653
+ className: "rg-back",
654
+ title: t("models.back"),
655
+ "aria-label": t("models.back"),
656
+ onClick: () => setPane("root")
657
+ }, chevronLeft),
658
+ h("span", { className: "rg-paneTitle" }, t("models.title"))
659
+ ),
660
+ h("div", { className: "rg-list" }, modelRows, listStatus),
661
+ errorEl
662
+ );
663
+
664
+ const card = open ? h("div", {
665
+ ref: menuRef,
666
+ className: "rg-card",
667
+ role: "dialog",
668
+ "aria-label": t("card.aria"),
669
+ style: position === null
670
+ ? { left: 0, top: 0, visibility: "hidden" }
671
+ : { left: position.left + "px", top: position.top + "px" },
672
+ onMouseDown: (event) => {
673
+ if (event.target instanceof Element && event.target.closest("button") !== null) event.preventDefault();
674
+ }
675
+ }, pane === "models" ? modelsPane : rootPane) : null;
676
+
677
+ const portaled = card !== null && typeof document !== "undefined" && ReactDOM !== null && typeof ReactDOM.createPortal === "function"
678
+ ? ReactDOM.createPortal(card, document.body)
679
+ : card;
680
+
681
+ return h("div", { ref: rootRef, className: "rg-root" },
682
+ h("button", {
683
+ ref: triggerRef,
684
+ type: "button",
685
+ className: "rg-trigger",
686
+ "aria-haspopup": "dialog",
687
+ "aria-expanded": open,
688
+ "aria-label": triggerAria,
689
+ title: effortLabel === undefined ? modelName : modelName + " · " + effortLabel,
690
+ disabled: locked,
691
+ onClick: () => {
692
+ if (open) {
693
+ setOpen(false);
694
+ setPane("root");
695
+ return;
696
+ }
697
+ setPane("root");
698
+ setPreview(null);
699
+ setError(null);
700
+ setOpen(true);
701
+ if (load !== null) load();
702
+ }
703
+ },
704
+ h("span", { className: "rg-name" }, currentChoice ? labelFor(providerOf(currentChoice.group), currentChoice.model, "trigger", true) : modelName),
705
+ effortLabel === undefined ? null : h("span", { className: "rg-effort" + (atMax ? " rg-effort-max" : "") }, effortLabel),
706
+ h("span", { className: "rg-chev" + (open ? " rg-chev-open" : "") }, chevronDown)
707
+ ),
708
+ portaled
709
+ );
710
+ }
711
+
712
+ /* ------------------------------------------------------------------ */
713
+ /* Registration */
714
+ /* ------------------------------------------------------------------ */
715
+ function insertStyles(ctx) {
716
+ if (typeof document === "undefined") return;
717
+ const run = () => {
718
+ const existing = document.querySelector('style[data-plugin-css="' + NS + '"]');
719
+ if (existing !== null) {
720
+ existing.textContent = CSS;
721
+ return undefined;
722
+ }
723
+ const tag = document.createElement("style");
724
+ tag.dataset.plugin = "dsh-reasoning-glow";
725
+ tag.dataset.pluginCss = NS;
726
+ tag.textContent = CSS;
727
+ document.head.appendChild(tag);
728
+ return () => {
729
+ if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
730
+ };
731
+ };
732
+ if (typeof ctx.effect === "function") ctx.effect(run, "reasoning-glow: stylesheet");
733
+ else run();
734
+ }
735
+
736
+ /**
737
+ * Services required by this Client plugin. `remote` and `remote.session`
738
+ * back the slot's standard props (session and access hooks); omitting them
739
+ * crashes the `conversation.input.model` occupant during render.
740
+ */
741
+ const INJECT = ["slots", "modelDirectories", "sessions", "locale", "remote", "remote.session"];
742
+
743
+ function apply(ctx) {
744
+ ctx.inject(INJECT, mount);
745
+ }
746
+
747
+ function mount(ctx) {
748
+ const slots = ctx.slots;
749
+ const models = ctx.modelDirectories;
750
+ const sessions = ctx.sessions;
751
+ const locale = ctx.locale;
752
+
753
+ insertStyles(ctx);
754
+ ctx.effect(() => locale.register(NS, { zh, en }), "reasoning-glow: dictionaries");
755
+
756
+ slots.inject("conversation.input.model", () => slots.register({
757
+ name: "conversation.input.model",
758
+ priority: -1,
759
+ locale: NS,
760
+ inject: (sessionId) => {
761
+ const directory = models.directoryFor(sessionId);
762
+ const available = sessions.subagentAddress(sessionId) === undefined;
763
+ return {
764
+ sessionId,
765
+ available,
766
+ directory: directory.store,
767
+ load: () => {
768
+ if (available) directory.load().catch(() => {});
769
+ },
770
+ select: (selection) => available ? directory.select(selection) : Promise.resolve(undefined)
771
+ };
772
+ }
773
+ }, (props) => h(Boundary, { key: props.sessionId }, h(Seat, Object.assign({}, props, { locale })))));
774
+ }
775
+
776
+ return { inject: INJECT, apply };
777
+ }
778
+ });
@@ -0,0 +1,5 @@
1
+ # reasoning-glow bundle patch: mounts the reasoning-effort slider plugin.
2
+ # Installed as a profile layer through `dsh.bundle.patch`.
3
+ - insert:
4
+ - id: reasoning-glow
5
+ name: dsh-reasoning-glow
package/icon.svg ADDED
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Reasoning Glow">
2
+ <defs>
3
+ <linearGradient id="rg-track" x1="0" y1="0" x2="1" y2="0">
4
+ <stop offset="0" stop-color="#7CA4FF"/>
5
+ <stop offset="0.55" stop-color="#A79CFF"/>
6
+ <stop offset="1" stop-color="#6F4FC9"/>
7
+ </linearGradient>
8
+ <linearGradient id="rg-plate" x1="0" y1="0" x2="0" y2="1">
9
+ <stop offset="0" stop-color="#2E3138"/>
10
+ <stop offset="1" stop-color="#202228"/>
11
+ </linearGradient>
12
+ </defs>
13
+ <rect x="2" y="2" width="60" height="60" rx="16" fill="url(#rg-plate)"/>
14
+ <rect x="10" y="29" width="44" height="10" rx="5" fill="#0d0f13" opacity="0.55"/>
15
+ <rect x="10" y="29" width="44" height="10" rx="5" fill="url(#rg-track)"/>
16
+ <circle cx="12" cy="41" r="7" fill="#fff"/>
17
+ <circle cx="36" cy="31" r="1.6" fill="#fff" opacity="0.95"/>
18
+ <circle cx="43" cy="37" r="1.2" fill="#EFE6FF" opacity="0.9"/>
19
+ <circle cx="49" cy="31.5" r="1.4" fill="#fff" opacity="0.85"/>
20
+ <circle cx="30" cy="36" r="1" fill="#E4DBFF" opacity="0.8"/>
21
+ </svg>
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reasoning-glow plugin, node half. Pure UI plugin: the empty apply keeps the
3
+ * plugin visible in the host composition, while the browser half ships through
4
+ * exports["./client"], discovered from the package.json `dsh.client` section.
5
+ */
6
+
7
+ /** Host plugin body — no host-side behavior for this surface plugin. */
8
+ export function apply() {}
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Reasoning Glow",
4
+ "description": "Replaces the composer model selector with a gradient reasoning-effort slider; the top level glows with drifting sparks."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "推理光轨",
4
+ "description": "把输入框的模型选择器换成带渐变光轨的推理强度滑块:拖动即可切换档位,最高档显示流光与星点。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "dsh-reasoning-glow",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Glowing reasoning-effort slider for the DeepSeek Harness composer",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "reasoning",
10
+ "slider"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "main": "./index.js",
15
+ "exports": {
16
+ ".": "./index.js",
17
+ "./client": "./client.js",
18
+ "./cordis.patch.yml": "./cordis.patch.yml",
19
+ "./locale/*.json": "./locale/*.json",
20
+ "./icon": "./icon.svg"
21
+ },
22
+ "files": [
23
+ "index.js",
24
+ "client.js",
25
+ "cordis.patch.yml",
26
+ "locale/*.json",
27
+ "icon.svg"
28
+ ],
29
+ "scripts": {
30
+ "test": "node test/smoke.mjs"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/WH-kt/dsh-reasoning-glow.git"
35
+ },
36
+ "homepage": "https://github.com/WH-kt/dsh-reasoning-glow#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/WH-kt/dsh-reasoning-glow/issues"
39
+ },
40
+ "dsh": {
41
+ "bundle": {
42
+ "patch": "./cordis.patch.yml"
43
+ },
44
+ "client": {
45
+ "platform": "web",
46
+ "inject": [
47
+ "@deepseek-ai/dsh-client-ui-model-selection",
48
+ "@deepseek-ai/dsh-client-ui-slots",
49
+ "@deepseek-ai/dsh-client-locale",
50
+ "@deepseek-ai/dsh-api-session-controller"
51
+ ]
52
+ }
53
+ }
54
+ }