@lolannn/dsh-effort-slider 0.0.0-stage → 0.2.6
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.md +197 -2
- package/client.js +2363 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +36 -0
- package/index.js +11 -0
- package/package.json +58 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 suyunxing
|
|
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,198 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-effort-slider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A reasoning-effort slider that **replaces the shipped model selector** in the dsh
|
|
4
|
+
web GUI composer. One control owns both halves of the model choice: the model
|
|
5
|
+
list and that model's reasoning effort, which becomes a glowing rail with a
|
|
6
|
+
starfield behind it.
|
|
7
|
+
|
|
8
|
+
**中文**:把 dsh 输入框里的模型选择器换成一条"星光滑轨"——模型列表与思考强度由同一个控件管,切到不支持当前档位的模型时自动落到最接近的一档,不再报错。
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
dsh plugin add @lolannn/dsh-effort-slider # from npm
|
|
12
|
+
dsh plugin add git+https://github.com/suyunxing/dsh-effort-slider # from source
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Screenshots
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
The rail is drawn from the model's own `reasoning.efforts`, so the tick labels
|
|
22
|
+
change with the model — `off / low / high / max` on one, `low / medium / high /
|
|
23
|
+
xhigh / max` on another — and the knob lands on the level the session is
|
|
24
|
+
actually using.
|
|
25
|
+
|
|
26
|
+
## What it does
|
|
27
|
+
|
|
28
|
+
- Takes over `conversation.input.model` (a single slot, so this shadows the
|
|
29
|
+
shipped selector rather than sitting next to it) at `priority: -1` — a single
|
|
30
|
+
slot renders the **lowest** priority first.
|
|
31
|
+
- Reads the model catalogue at runtime: the model list comes from
|
|
32
|
+
`catalog.groups` (grouped by provider) and the levels are exactly the
|
|
33
|
+
`reasoning.efforts` the current model declares (typically `off / low / high /
|
|
34
|
+
max`). No hard-coded list.
|
|
35
|
+
- Shows the Session's **real** selection, not the deployment default, by reading
|
|
36
|
+
the durable `modelSelection` projection.
|
|
37
|
+
- Picking a level or a model calls the session `selectModel` remote with the
|
|
38
|
+
explicit `{ provider, model, reasoningEffort }` triple, so the choice applies to
|
|
39
|
+
the Session and is saved as the default in the background.
|
|
40
|
+
- Switching to a model that does not declare the level in hand is resolved on the
|
|
41
|
+
client first: the closest level that model *does* declare is sent instead, and
|
|
42
|
+
the status line says which substitution happened. A model with no reasoning at
|
|
43
|
+
all is switched to with the field omitted, so the Host applies that model's own
|
|
44
|
+
default. Nothing errors, nothing is silently dropped.
|
|
45
|
+
- The rail is animated: drifting particles, soft ribbon waves, iridescent
|
|
46
|
+
four-point stars, a breathing star core, and a three-armed galaxy that
|
|
47
|
+
collapses out of the rail at the top level. Reaching the top fires a shockwave
|
|
48
|
+
burst, a star-warp starfield and a light-blade flash.
|
|
49
|
+
- Models without reasoning efforts are tagged `无档位` in the model list and
|
|
50
|
+
degrade gracefully once selected: the rail is dimmed and the status line says so
|
|
51
|
+
instead of pretending to switch.
|
|
52
|
+
|
|
53
|
+
## Surfaces
|
|
54
|
+
|
|
55
|
+
| Slot | Content |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `conversation.input.model` | the trigger (`模型 · 档位`) and, while open, the popup: model row, grouped model list, effort rail, level scale, status line |
|
|
58
|
+
| `conversation.input.model.label` | untouched — the shipped non-interactive label stays owner-managed |
|
|
59
|
+
|
|
60
|
+
The popup renders through `react-dom`'s `createPortal` into `document.body`,
|
|
61
|
+
because the composer row can live inside a transformed/overflowing ancestor that
|
|
62
|
+
would clip a `position: fixed` child.
|
|
63
|
+
|
|
64
|
+
## Interaction
|
|
65
|
+
|
|
66
|
+
- Click the trigger, then drag the rail or press `←` `→` `↑` `↓` `Home` `End`.
|
|
67
|
+
- The rail snaps to the nearest level on release (elastic overshoot).
|
|
68
|
+
- Reaching **high** turns the starfield into warp: the stars stream, the rail
|
|
69
|
+
becomes a slow galaxy swirl, and it stays lit for as long as the dial is at
|
|
70
|
+
high or above — so `xhigh` and `max` keep the look instead of only the top.
|
|
71
|
+
Dropping back below high switches it off. The top level still adds the full
|
|
72
|
+
celebration (flash, quake, caption).
|
|
73
|
+
- The level names under the rail are buttons too — same edge math as the thumb,
|
|
74
|
+
so each label sits exactly under its stop.
|
|
75
|
+
- Click the 模型 row to expand the model list; picking a model carries the current
|
|
76
|
+
effort across, remapping it when that model lacks the level (see below).
|
|
77
|
+
- Outside click or `Esc` closes. Direction keys are ignored while a text field
|
|
78
|
+
has focus, so typing in the composer never changes the effort.
|
|
79
|
+
|
|
80
|
+
## Files
|
|
81
|
+
|
|
82
|
+
- `index.js` — Host half. The bundle row exists so the browser half has an entry
|
|
83
|
+
to attach to; it declares no service, tool or config.
|
|
84
|
+
- `client.js` — the whole feature: stylesheet injection, catalogue + projection
|
|
85
|
+
mapping, canvas engine, controller and the slot registration.
|
|
86
|
+
- `icon.svg` — original artwork (pinched four-point star over a lit rail).
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- All canvas work is `requestAnimationFrame`-driven and stops on unmount.
|
|
91
|
+
- `prefers-reduced-motion: reduce` disables the burst, the warp and the quake.
|
|
92
|
+
- Only transforms, `opacity` and canvas are animated; no layout thrash.
|
|
93
|
+
- The canvas is sized from the card (`getBoundingClientRect` + `ResizeObserver`),
|
|
94
|
+
not from the viewport.
|
|
95
|
+
- The warp gate is the level named `high`; a model that declares no `high`
|
|
96
|
+
warps from the last level at or below it on the shared ladder, and a model
|
|
97
|
+
with one level never warps.
|
|
98
|
+
- Opening the popup while already on the top level arms the burst guard instead of
|
|
99
|
+
firing it, so the celebration only plays when you actually move into the top
|
|
100
|
+
level.
|
|
101
|
+
- The popup opens upward, but flips downward and caps its height when the trigger
|
|
102
|
+
sits too low for that (landscape phone, small window).
|
|
103
|
+
|
|
104
|
+
## Why a model switch remaps the effort
|
|
105
|
+
|
|
106
|
+
The Host validates the triple, it does not normalize it. In
|
|
107
|
+
`@deepseek-ai/dsh-llm` → `resolveCallWithInfo`:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const reasoning = info.reasoning;
|
|
111
|
+
const requested = defaulted.reasoningEffort;
|
|
112
|
+
if (reasoning === void 0) {
|
|
113
|
+
if (requested !== void 0) throw new LlmError(`... does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
114
|
+
} else {
|
|
115
|
+
const effective = requested ?? reasoning.defaultEffort;
|
|
116
|
+
if (effective !== void 0 && !reasoning.efforts.some((e) => e.id === effective)) throw new LlmError(..., "UNSUPPORTED_REASONING_EFFORT");
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The comment above it is explicit: *"no clamping or aliasing is performed."* A
|
|
121
|
+
model with three rungs and a model with seven therefore reject each other's
|
|
122
|
+
levels, and the session remote wraps that rejection as
|
|
123
|
+
`RemoteError("session/model-unavailable", …)` — which used to surface as a red
|
|
124
|
+
`切换失败:provider "…" model "…" does not support reasoning effort "…"`.
|
|
125
|
+
|
|
126
|
+
So `resolveEffort(model, wantedId)` in `client.js` maps the level onto the target
|
|
127
|
+
model before the request is built:
|
|
128
|
+
|
|
129
|
+
| Case | What is sent | Status line |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| target declares the level | it, unchanged | nothing |
|
|
132
|
+
| target declares other levels | the closest one | `该模型没有「medium」档,已自动切到最接近的「low」` |
|
|
133
|
+
| target declares no reasoning | the field is **omitted** | `「<model>」没有思考强度档位,已按模型默认运行` |
|
|
134
|
+
|
|
135
|
+
"Closest" is measured on one shared scale rather than per-provider lists, because
|
|
136
|
+
every adapter names its rungs from the same vocabulary:
|
|
137
|
+
`off · none · minimal · low · medium/med · high · xhigh · max · ultra`. A tie
|
|
138
|
+
resolves to the **stronger** rung — losing reasoning power is the worse of the two
|
|
139
|
+
surprises (`high → xhigh` rather than `high → medium` when both are one step
|
|
140
|
+
away). A rung whose id is not on that scale is placed by its position in its own
|
|
141
|
+
list, and when the id being carried is itself unknown the target's
|
|
142
|
+
`defaultEffort` (then its strongest rung) is used instead, because its distance
|
|
143
|
+
cannot be measured.
|
|
144
|
+
|
|
145
|
+
The last real choice is remembered in `lastEffort`, so
|
|
146
|
+
`max → 无档位模型 → back` returns to `max` instead of that model's default: the
|
|
147
|
+
intent survives the models that cannot express it.
|
|
148
|
+
|
|
149
|
+
## Why every visible label is passed through `safeLabel`
|
|
150
|
+
|
|
151
|
+
dsh-pocket installs a document-level click guard for phones: any click on a
|
|
152
|
+
`<button>`/`<a>` whose **text content** looks like a file path is cancelled with
|
|
153
|
+
`preventDefault()` + `stopImmediatePropagation()` and replaced by a toast
|
|
154
|
+
("手机上无法直接打开电脑上的文件"). Its matcher includes
|
|
155
|
+
`/[\w.\-]+\/[\w.\-]+\.\w{1,12}/`, which a model id like
|
|
156
|
+
`workbuddy-ai/deepseek-v4.1-flash` matches — so the trigger became unclickable on
|
|
157
|
+
mobile while working fine on the desktop.
|
|
158
|
+
|
|
159
|
+
Every label rendered as text therefore runs through `safeLabel()`, which turns
|
|
160
|
+
`/` and `\` into ` · `. `aria-label` and `title` keep the raw identifier, because
|
|
161
|
+
they are attributes and the guard only reads `textContent`.
|
|
162
|
+
|
|
163
|
+
## Where the "current" model comes from
|
|
164
|
+
|
|
165
|
+
`modelCatalog().default` is the *deployment* default and can differ from what a
|
|
166
|
+
Session really runs (a Session-local override). Spreading it into a commit
|
|
167
|
+
silently switched the model while the user only meant to change the effort.
|
|
168
|
+
|
|
169
|
+
So the controller follows the Session's durable projection — the same source the
|
|
170
|
+
shipped selector reads:
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
sessions.binding(sessionId).session.projections.faceOf("modelSelection")
|
|
174
|
+
// -> ObservableSnapshot<{ lastUsed, next }>, next = pending ?? lastUsed
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`model/selection` carries the user's intent immediately (`pending`), and
|
|
178
|
+
`request/header` records what a request actually used (`lastUsed`). Two details
|
|
179
|
+
matter:
|
|
180
|
+
|
|
181
|
+
- The face is subscribed **before** the first `getSnapshot()`; reading a
|
|
182
|
+
projection nobody observes can return no value at all.
|
|
183
|
+
- The catalogue default is a fallback only, used until the projection has a value.
|
|
184
|
+
|
|
185
|
+
## Verification
|
|
186
|
+
|
|
187
|
+
- `conversation.input.model` resolves to this plugin (`priority: -1`, active) and
|
|
188
|
+
the shipped registrant goes inactive.
|
|
189
|
+
- Trigger reads `选择模型与思考强度 — <model> · <effort>`; changing the level
|
|
190
|
+
propagates through `selectModel` into the projection and back into the label.
|
|
191
|
+
- The popup's model list exposes every routable model, grouped per provider.
|
|
192
|
+
- Switching `medium` (on a seven-rung model) to an `off/low/high/max` model sends
|
|
193
|
+
`low` and reports the substitution; switching to a no-reasoning model sends no
|
|
194
|
+
`reasoningEffort` at all; switching back restores `medium`.
|
|
195
|
+
- Checked in a real client at 360×780, 360×400 (short) and 1280×800: a real click
|
|
196
|
+
opens the popup, it stays inside the viewport, the level labels sit under the
|
|
197
|
+
thumb stops, the selected model is marked, and opening does not replay the
|
|
198
|
+
top-level burst.
|