dsh-model-accordion 0.1.1 → 0.2.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/README.md +42 -6
- package/lib/client.js +208 -33
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,9 +6,8 @@ Provider-folded model selector for the DeepSeek Harness Web composer.
|
|
|
6
6
|
|
|
7
7
|
- Replaces the single composer model seat with a provider-folded selector.
|
|
8
8
|
- Keeps provider groups collapsed by default and opens models on demand.
|
|
9
|
-
- Reads reasoning-effort choices from each model's resolved catalog metadata
|
|
10
|
-
- Sends model and reasoning-effort selections through DSH's shared model-directory service
|
|
11
|
-
- Wraps vision-router mirror providers in one collapsed `识图镜像(带图会话)` section when those providers are present.
|
|
9
|
+
- Reads reasoning-effort choices from each model's resolved catalog metadata, with a built-in official-effort table as a fallback. A single automatic rule applies: a model with configured/backend-declared efforts uses them, and an unconfigured model falls back to the built-in official table. A user who configured their own levels keeps them; no per-model effort configuration is needed for the official fallback to work.
|
|
10
|
+
- Sends model and reasoning-effort selections through DSH's shared model-directory service; a picked effort the model does not genuinely support is dropped so the request uses the model's default instead of failing with `UNSUPPORTED_REASONING_EFFORT`.
|
|
12
11
|
- Keeps the popup height-bounded with an internal scroll area and wraps effort controls below the model name.
|
|
13
12
|
- Uses DSH semantic theme tokens for colors and supports light/dark theme changes.
|
|
14
13
|
|
|
@@ -34,12 +33,49 @@ dsh plugin --profile web add file:/absolute/path/to/dsh-model-accordion
|
|
|
34
33
|
|
|
35
34
|
Refresh the Web UI after installation. The package declares a `dsh.bundle` patch and a Web client entry, so it is installed and loaded as a persistent profile bundle rather than a temporary runtime extension.
|
|
36
35
|
|
|
36
|
+
## Customizing reasoning effort for a model
|
|
37
|
+
|
|
38
|
+
DSH persists model/provider configuration in your main `settings.yaml`, and this
|
|
39
|
+
plugin reads the same resolved model metadata the backend produces, so
|
|
40
|
+
**declared/backend effort configuration always wins over the built-in table**.
|
|
41
|
+
The effective priority is:
|
|
42
|
+
|
|
43
|
+
model's configured `reasoningEfforts` > provider-level default > built-in official table
|
|
44
|
+
|
|
45
|
+
To give a specific model — for one your relay exposes under a name this plugin's
|
|
46
|
+
table does not know — your own effort ladder, declare it on that model in
|
|
47
|
+
`settings.yaml` and the plugin uses exactly those levels:
|
|
48
|
+
|
|
49
|
+
providers:
|
|
50
|
+
my-relay:
|
|
51
|
+
api: openai-completions
|
|
52
|
+
baseURL: https://…
|
|
53
|
+
models:
|
|
54
|
+
- id: my-unknown-model
|
|
55
|
+
api: openai-completions
|
|
56
|
+
contextWindow: 200000
|
|
57
|
+
maxTokens: 32000
|
|
58
|
+
input: [text]
|
|
59
|
+
reasoningEfforts:
|
|
60
|
+
off: null # optional: lets the user turn thinking off
|
|
61
|
+
low: low
|
|
62
|
+
high: high
|
|
63
|
+
max: max
|
|
64
|
+
|
|
65
|
+
The exact fields follow `dsh-llm-pi-ai`'s model schema (see that package's
|
|
66
|
+
docs); `reasoningEfforts` maps each offered level to its wire spelling, and you
|
|
67
|
+
may set a provider-level default with the `reasoning:` key. Because this lives
|
|
68
|
+
in your DSH settings file, an agent — or you — can edit it with the file tools
|
|
69
|
+
and DSH reloads it on save; the browser never sees your API key or relay URL.
|
|
70
|
+
|
|
37
71
|
## Behavior and limitations
|
|
38
72
|
|
|
39
|
-
|
|
73
|
+
The selector submits the exact provider, model, and reasoning effort returned by the DSH model directory (configured efforts first, built-in official table as fallback).
|
|
74
|
+
- A picked reasoning effort that the model does not support is silently dropped so the request falls back to the model's default (no `UNSUPPORTED_REASONING_EFFORT` error). A relay/gateway model configured without an effort level keeps the model but does not force an unsupported level.
|
|
40
75
|
- A selection can still be rejected by the DSH host when the session contains images and the selected model does not declare image input. That is a host/model-capability rule, not a UI override.
|
|
41
|
-
- The
|
|
42
|
-
- The
|
|
76
|
+
- The model search box filters provider lists by name, id, or description while keeping the accordion grouping; a provider group auto-expands while a query is active.
|
|
77
|
+
- The built-in official-effort table keys on the model's name/id and is cross-checked against the installed pi-ai catalog and each vendor's API docs. It covers only models whose vendors expose a real effort ladder: GPT-5.6 (full ladder incl. `max`), gpt-5.2+ (`xhigh`), gpt-5.1, gpt-5/mini/nano, o1/o3/o4, gpt-oss, Grok 4.x, GLM-5.3 (`low/high/max`) and GLM-5.2+ (`xhigh/max`), DeepSeek-V4 (`low/high/max`), Kimi K3, Qwen3.8, Gemini 3+ (`low/high`), Claude 4.6+ adaptive, Gemma 4. Families whose native control is not an effort ladder (Gemini 2.5 / Claude ≤4.5 token budgets, GLM ≤5.1 / Kimi K2 / MiniMax / Mistral thinking switches, Qwen open-source, DeepSeek ≤V3) are deliberately excluded. Unmatched models offer only the efforts their configured catalog declares — see "Customizing reasoning effort for a model" to add your own.
|
|
78
|
+
- The plugin uses English status and search text; full locale integration is planned for a later release.
|
|
43
79
|
|
|
44
80
|
## Development checks
|
|
45
81
|
|
package/lib/client.js
CHANGED
|
@@ -25,7 +25,6 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
.dshma-status,.dshma-empty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}
|
|
26
26
|
.dshma-error{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px}
|
|
27
27
|
.dshma-provider{border-radius:10px;overflow:hidden;flex:none}
|
|
28
|
-
.dshma-vision{margin-top:4px;border-top:1px solid var(--dsw-alias-border-l3);padding-top:4px}
|
|
29
28
|
.dshma-provider-head{box-sizing:border-box;width:100%;display:flex;align-items:center;gap:7px;padding:8px;border:none;border-radius:10px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;line-height:18px;text-align:left;cursor:pointer;font-family:inherit}
|
|
30
29
|
.dshma-provider-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
31
30
|
.dshma-provider-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -46,6 +45,11 @@ window.__ModuleLoader__.load({
|
|
|
46
45
|
.dshma-effort:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
47
46
|
.dshma-effort-selected{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary)}
|
|
48
47
|
.dshma-failure{color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:7px 8px;font-size:12px;line-height:18px}
|
|
48
|
+
.dshma-search-wrap{flex:none;padding:4px;position:relative}
|
|
49
|
+
.dshma-search{box-sizing:border-box;width:100%;height:28px;padding:0 28px 0 10px;border:1px solid var(--dsw-alias-border-l3);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;outline:none;font-family:inherit}
|
|
50
|
+
.dshma-search:focus-visible{border-color:var(--dsw-alias-border-l3);box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
|
|
51
|
+
.dshma-search-clear{position:absolute;top:50%;right:7px;transform:translateY(-50%);width:18px;height:18px;border:none;border-radius:50%;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;font-size:13px;line-height:1}
|
|
52
|
+
.dshma-search-clear:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
49
53
|
`;
|
|
50
54
|
|
|
51
55
|
function Chevron(props) {
|
|
@@ -62,9 +66,108 @@ window.__ModuleLoader__.load({
|
|
|
62
66
|
);
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
//#region Official reasoning-effort divisions
|
|
70
|
+
// Built-in, provider-independent reasoning-effort divisions keyed on the
|
|
71
|
+
// model's name/id. This is what lets the selector offer sensible effort
|
|
72
|
+
// choices even when a model's configured catalog (or a third-party relay /
|
|
73
|
+
// gateway) declares no `reasoningEfforts` — the common npm-install case.
|
|
74
|
+
// The divisions below are sourced from each vendor's public API docs, cross-
|
|
75
|
+
// checked against the installed pi-ai catalog (providers/data/*.json) and the
|
|
76
|
+
// vendor docs listed in the plugin changelog. Levels are the canonical pi-ai
|
|
77
|
+
// ids (off/minimal/low/medium/high/xhigh/max — see THINKING_LEVELS in
|
|
78
|
+
// dsh-llm-pi-ai); `default` is the official default and is used only when the
|
|
79
|
+
// backend leaves it unset.
|
|
80
|
+
//
|
|
81
|
+
// IMPORTANT — only models whose vendors expose a real reasoning-effort ladder
|
|
82
|
+
// are listed. Families whose native control is NOT an effort ladder (Claude
|
|
83
|
+
// ≤4.5 & Gemini 2.5 token budgets, GLM ≤5.1 / Kimi K2 / MiniMax / Mistral
|
|
84
|
+
// two-state thinking, Qwen open-source enable_thinking, DeepSeek ≤V3 / R1) are
|
|
85
|
+
// deliberately excluded so we never surface a ladder the API would reject.
|
|
86
|
+
// Official-family models are normally covered by the installed pi-ai catalog
|
|
87
|
+
// (backend first layer) anyway; this table only backs up manually-declared /
|
|
88
|
+
// relay models the backend leaves unconfigured.
|
|
89
|
+
//
|
|
90
|
+
// Entries are checked top-down; the first match wins.
|
|
91
|
+
// gpt-5.6 (Sol/Terra/Luna): full ladder incl. max (default high ⚠ TBD)
|
|
92
|
+
// gpt-5.2+ / 5.3-codex: low/medium/high/xhigh (default high ⚠ TBD)
|
|
93
|
+
// gpt-5.1: minimal/low/medium/high (default low)
|
|
94
|
+
// gpt-5 / mini / nano: minimal/low/medium/high (default medium)
|
|
95
|
+
// o1/o3/o4 families: low/medium/high (default medium)
|
|
96
|
+
// gpt-oss: low/medium/high (default medium)
|
|
97
|
+
// grok 4.x (responses): low/medium/high (default high)
|
|
98
|
+
// glm-5.3 / 5.3-FLASH: low/high/max (thinking un-offable)(default max)
|
|
99
|
+
// glm-5.2+: low/medium/high/xhigh/max (default max)
|
|
100
|
+
// deepseek-v4: low/high/max (default high)
|
|
101
|
+
// kimi-k3: low/high/max (default max)
|
|
102
|
+
// qwen3.8: xhigh/medium/low (default xhigh)
|
|
103
|
+
// gemini-3+ (Pro/Flash): low/high (always on) (default high)
|
|
104
|
+
// claude 4.6+ adaptive: low/medium/high (default high)
|
|
105
|
+
// gemma-4: minimal/high (default high)
|
|
106
|
+
const OFFICIAL_EFFORTS = [
|
|
107
|
+
{ test: /gpt-5\.6/i, efforts: ["minimal", "low", "medium", "high", "xhigh", "max"], def: "high" },
|
|
108
|
+
{ test: /gpt-5\.2[^0-9]|gpt-5\.[3-9]|gpt-5\.2\b/i, efforts: ["low", "medium", "high", "xhigh"], def: "high" },
|
|
109
|
+
{ test: /gpt-5\.1/i, efforts: ["minimal", "low", "medium", "high"], def: "low" },
|
|
110
|
+
{ test: /gpt-5(?:-mini|-nano)?\b/i, efforts: ["minimal", "low", "medium", "high"], def: "medium" },
|
|
111
|
+
{ test: /o[134](-mini|-pro)?\b/i, efforts: ["low", "medium", "high"], def: "medium" },
|
|
112
|
+
{ test: /gpt-oss/i, efforts: ["low", "medium", "high"], def: "medium" },
|
|
113
|
+
{ test: /grok/i, efforts: ["low", "medium", "high"], def: "high" },
|
|
114
|
+
{ test: /glm-5\.3/i, efforts: ["low", "high", "max"], def: "max" },
|
|
115
|
+
{ test: /glm-5(?:\.[2-9]|\.\d+)/i, efforts: ["low", "medium", "high", "xhigh", "max"], def: "max" },
|
|
116
|
+
{ test: /deepseek-v4/i, efforts: ["low", "high", "max"], def: "high" },
|
|
117
|
+
{ test: /kimi-k3/i, efforts: ["low", "high", "max"], def: "max" },
|
|
118
|
+
{ test: /qwen3\.8/i, efforts: ["xhigh", "medium", "low"], def: "xhigh" },
|
|
119
|
+
{ test: /gemini-3/i, efforts: ["low", "high"], def: "high" },
|
|
120
|
+
{ test: /claude-(opus|sonnet|haiku|fable)-(4\.[6-9]|5)\b/i, efforts: ["low", "medium", "high"], def: "high" },
|
|
121
|
+
{ test: /gemma-4/i, efforts: ["minimal", "high"], def: "high" }
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
function officialEffortsFor(model) {
|
|
125
|
+
if (!model) return void 0;
|
|
126
|
+
const hay = `${model.name || ""} ${model.id || ""}`;
|
|
127
|
+
for (const entry of OFFICIAL_EFFORTS) {
|
|
128
|
+
if (entry.test.test(hay)) return entry;
|
|
129
|
+
}
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Reason offered for a model: the backend/configured-catalog efforts when the
|
|
134
|
+
// model has them, otherwise the built-in official table as a fallback. This
|
|
135
|
+
// single "auto" rule is all there is — a user who configured their own levels
|
|
136
|
+
// keeps them, and an unconfigured model still gets sensible official choices.
|
|
137
|
+
function resolveEfforts(model) {
|
|
138
|
+
const backend = model && model.reasoning && Array.isArray(model.reasoning.efforts)
|
|
139
|
+
? model.reasoning.efforts.filter((e) => e && typeof e.id === "string")
|
|
140
|
+
: [];
|
|
141
|
+
if (backend.length > 0) return backend;
|
|
142
|
+
const official = officialEffortsFor(model);
|
|
143
|
+
if (!official) return [];
|
|
144
|
+
return official.efforts.map((id) => ({ id, name: `${id.charAt(0).toUpperCase()}${id.slice(1)}` }));
|
|
145
|
+
}
|
|
146
|
+
// The default effort id to surface for a model (trigger caption + default
|
|
147
|
+
// chip highlight): the backend-declared default wins, and the official table's
|
|
148
|
+
// default is used only when the backend left it unset.
|
|
149
|
+
function resolveDefaultEffort(model) {
|
|
150
|
+
const backendDefault = model && model.reasoning && model.reasoning.defaultEffort != null
|
|
151
|
+
? model.reasoning.defaultEffort
|
|
152
|
+
: void 0;
|
|
153
|
+
if (backendDefault !== void 0) return backendDefault;
|
|
154
|
+
const official = officialEffortsFor(model);
|
|
155
|
+
return official ? official.def : void 0;
|
|
156
|
+
}
|
|
157
|
+
// Whether an effort id is genuinely supported by the model — used by the
|
|
158
|
+
// submit path to decide whether to send reasoningEffort.
|
|
159
|
+
function effortSupported(model, id) {
|
|
160
|
+
return resolveEfforts(model).some((e) => e.id === id);
|
|
161
|
+
}
|
|
162
|
+
|
|
65
163
|
function effortOptions(model) {
|
|
66
|
-
|
|
67
|
-
|
|
164
|
+
return resolveEfforts(model);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function effortName(model, id) {
|
|
168
|
+
const es = resolveEfforts(model);
|
|
169
|
+
const hit = es.find((e) => e.id === id);
|
|
170
|
+
return hit && hit.name ? hit.name : id;
|
|
68
171
|
}
|
|
69
172
|
|
|
70
173
|
function Accordion(props) {
|
|
@@ -75,14 +178,26 @@ window.__ModuleLoader__.load({
|
|
|
75
178
|
const [failure, setFailure] = React.useState(null);
|
|
76
179
|
const groups = Array.isArray(state.groups) ? state.groups : [];
|
|
77
180
|
const rootRef = React.useRef(null);
|
|
78
|
-
const [
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
181
|
+
const [query, setQuery] = React.useState("");
|
|
182
|
+
// Roving focus registry: every visible model-row button registers itself here
|
|
183
|
+
// (keyed by group+model), so arrow/Home/End navigate in accordion order.
|
|
184
|
+
const itemKeys = React.useRef({});
|
|
185
|
+
const registerItem = (key, el) => { if (el) itemKeys.current[key] = el; else delete itemKeys.current[key]; };
|
|
186
|
+
const searchRef = React.useRef(null);
|
|
187
|
+
|
|
188
|
+
// When the menu opens with NO provider expanded yet, surface the provider
|
|
189
|
+
// holding the current model so a fresh open is not a blank, fully-collapsed
|
|
190
|
+
// list. Crucially this only fires while `expanded` is empty: re-running it on
|
|
191
|
+
// every open (selecting a model closes the menu, so compare-testing reopens
|
|
192
|
+
// repeatedly) would otherwise accumulate every tested provider into
|
|
193
|
+
// `expanded`, leaving them all open once the search filter is cleared. When
|
|
194
|
+
// the user has already opened providers (browse state) we leave them alone.
|
|
195
|
+
React.useEffect(() => {
|
|
196
|
+
if (!open) return;
|
|
197
|
+
if (Object.values(expanded).some(Boolean)) return;
|
|
198
|
+
const g = groups.find((gg) => gg.id === state.current?.provider);
|
|
199
|
+
if (g) setExpanded((old) => ({ ...old, [g.id]: true }));
|
|
200
|
+
}, [open]);
|
|
86
201
|
|
|
87
202
|
React.useEffect(() => {
|
|
88
203
|
if (!open) return;
|
|
@@ -93,6 +208,11 @@ window.__ModuleLoader__.load({
|
|
|
93
208
|
return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); };
|
|
94
209
|
}, [open]);
|
|
95
210
|
|
|
211
|
+
// Populate store.current as soon as the model slot becomes available
|
|
212
|
+
// (mount/startup), so the trigger shows the last-used model instead of
|
|
213
|
+
// falling back to "Select model" until the user opens the menu.
|
|
214
|
+
React.useEffect(() => { if (props.available) props.load(); }, [props.available, props.load]);
|
|
215
|
+
// Refresh the store whenever the menu is opened (mirrors official reload).
|
|
96
216
|
React.useEffect(() => { if (open && props.available) props.load(); }, [open, props.available]);
|
|
97
217
|
|
|
98
218
|
const select = (group, model, effort) => {
|
|
@@ -100,7 +220,14 @@ window.__ModuleLoader__.load({
|
|
|
100
220
|
setBusy(true);
|
|
101
221
|
setFailure(null);
|
|
102
222
|
const sel = { provider: group.id, model: model.id };
|
|
103
|
-
|
|
223
|
+
// Submit-degradation: only send reasoningEffort when the picked level is
|
|
224
|
+
// one the model genuinely supports. When the backend catalog declares
|
|
225
|
+
// efforts, an "official" level it does not list is dropped so the request
|
|
226
|
+
// falls back to the model's default instead of failing with
|
|
227
|
+
// UNSUPPORTED_REASONING_EFFORT. NOTE: when the backend declares no efforts
|
|
228
|
+
// at all the official table is the sole authority, so any official level is
|
|
229
|
+
// sent as-is — an under-configured relay may still reject it.
|
|
230
|
+
if (effort !== void 0 && effortSupported(model, effort)) sel.reasoningEffort = effort;
|
|
104
231
|
props.select(sel).then((ok) => {
|
|
105
232
|
if (!ok) throw new Error("model selection failed");
|
|
106
233
|
setOpen(false);
|
|
@@ -113,12 +240,41 @@ window.__ModuleLoader__.load({
|
|
|
113
240
|
const curGroup = groups.find((g) => g.id === current?.provider);
|
|
114
241
|
const curModel = curGroup ? curGroup.models.find((m) => m.id === current?.model) : void 0;
|
|
115
242
|
const label = curModel ? curModel.name : (current ? `${current.provider}/${current.model}` : "Select model");
|
|
116
|
-
|
|
117
|
-
|
|
243
|
+
// Show a `· effort` caption ONLY when a reasoning effort is actually pinned
|
|
244
|
+
// on the current selection (persisted in `current.reasoningEffort`). The
|
|
245
|
+
// official default is deliberately NOT surfaced here: showing it before the
|
|
246
|
+
// user pins anything reads as "already set", which is misleading.
|
|
247
|
+
const curEffort = curModel && current && current.provider === curGroup.id && current.model === curModel.id && current.reasoningEffort
|
|
248
|
+
? effortName(curModel, current.reasoningEffort)
|
|
118
249
|
: void 0;
|
|
119
250
|
|
|
251
|
+
// Model search: filter each provider's models by the query while keeping
|
|
252
|
+
// the accordion grouping structure. Matches on name, id, and description.
|
|
253
|
+
const q = query.trim().toLowerCase();
|
|
254
|
+
// When the filter is cleared (✕ / Escape / manual delete), collapse the
|
|
255
|
+
// accordion to the provider holding the CURRENT model. Selecting a model
|
|
256
|
+
// never writes `expanded`, so without this the menu would fall back to
|
|
257
|
+
// whatever was expanded before the search — not the model you just picked.
|
|
258
|
+
// Because selection does not accumulate into `expanded`, this stays focused
|
|
259
|
+
// on one group and never re-introduces the "everything open" problem.
|
|
260
|
+
const prevQuery = React.useRef("");
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
const prev = prevQuery.current;
|
|
263
|
+
prevQuery.current = q;
|
|
264
|
+
if (q === "" && prev !== "") {
|
|
265
|
+
const cur = state.current;
|
|
266
|
+
setExpanded(cur && groups.some((g) => g.id === cur.provider) ? { [cur.provider]: true } : {});
|
|
267
|
+
}
|
|
268
|
+
}, [q]);
|
|
269
|
+
const matches = (m) => q === "" || [m.name, m.id, m.description].some((s) => typeof s === "string" && s.toLowerCase().includes(q));
|
|
270
|
+
const visibleGroups = q === ""
|
|
271
|
+
? groups
|
|
272
|
+
: groups.map((g) => ({ ...g, models: g.models.filter(matches) })).filter((g) => g.models.length > 0);
|
|
273
|
+
const hasAnyModel = visibleGroups.some((g) => g.models.length > 0);
|
|
274
|
+
|
|
120
275
|
const renderGroup = (group) => {
|
|
121
|
-
const isOpen = expanded[group.id] === true;
|
|
276
|
+
const isOpen = expanded[group.id] === true || q !== "";
|
|
277
|
+
if (!group.models.length) return null;
|
|
122
278
|
return React.createElement("div", { className: "dshma-provider", key: group.id },
|
|
123
279
|
React.createElement("button", { type: "button", className: "dshma-provider-head", onClick: () => setExpanded((old) => ({ ...old, [group.id]: !isOpen })), "aria-expanded": isOpen },
|
|
124
280
|
React.createElement(Chevron, { className: `dshma-provider-chevron${isOpen ? " dshma-provider-chevron-open" : ""}` }),
|
|
@@ -128,17 +284,16 @@ window.__ModuleLoader__.load({
|
|
|
128
284
|
isOpen ? React.createElement("div", { className: "dshma-models" }, group.models.map((model) => {
|
|
129
285
|
const selected = current && current.provider === group.id && current.model === model.id;
|
|
130
286
|
const efforts = effortOptions(model);
|
|
131
|
-
const defEffort = model
|
|
132
|
-
// A "
|
|
133
|
-
//
|
|
134
|
-
//
|
|
287
|
+
const defEffort = resolveDefaultEffort(model);
|
|
288
|
+
// A "default" chip clears a pinned effort so the model's effective
|
|
289
|
+
// default applies again (mirrors the official provider-default entry
|
|
290
|
+
// in dsh-client-ui-model-selection @315-324). It appears only when a
|
|
291
|
+
// default effort is known (backend-declared or official-table).
|
|
135
292
|
const defaultChip = defEffort !== void 0 ? [{ id: "__provider_default__", name: "默认" }] : [];
|
|
136
293
|
const chips = defaultChip.concat(efforts);
|
|
137
|
-
// reason about the currently active effort: a pinned explicit effort
|
|
138
|
-
// wins over the provider's default; otherwise provider default applies.
|
|
139
294
|
const pinnedEffort = selected ? current.reasoningEffort : void 0;
|
|
140
295
|
return React.createElement("div", { className: `dshma-option${selected ? " dshma-option-selected" : ""}`, key: model.id },
|
|
141
|
-
React.createElement("button", { type: "button", className: "dshma-option-row", onClick: () => {
|
|
296
|
+
React.createElement("button", { type: "button", className: "dshma-option-row", ref: (el) => registerItem(`${group.id}\u0000${model.id}`, el), onClick: () => {
|
|
142
297
|
// Re-clicking the already-selected model must NOT re-submit and
|
|
143
298
|
// thereby wipe a custom (pinned) effort — just close the menu.
|
|
144
299
|
// Mirrors dsh-client-ui-model-selection @397-400 / @718.
|
|
@@ -157,28 +312,48 @@ window.__ModuleLoader__.load({
|
|
|
157
312
|
);
|
|
158
313
|
};
|
|
159
314
|
|
|
315
|
+
// Roving arrow-key navigation across the rendered model rows (mirrors the
|
|
316
|
+
// official ModelSelect moveFocus). Enter/Space activate the focused row
|
|
317
|
+
// natively; Arrow keys / Home / End move between rows. ArrowDown from the
|
|
318
|
+
// search field lands on the first row.
|
|
319
|
+
const onMenuKey = (e) => {
|
|
320
|
+
// Escape: first press with text clears the search (keeping focus), second
|
|
321
|
+
// press closes. stopPropagation prevents the document-level handler from
|
|
322
|
+
// closing the menu while we are only clearing.
|
|
323
|
+
if (e.key === "Escape" && query !== "") {
|
|
324
|
+
e.preventDefault(); e.stopPropagation(); setQuery(""); searchRef.current && searchRef.current.focus(); return;
|
|
325
|
+
}
|
|
326
|
+
const els = Object.values(itemKeys.current)
|
|
327
|
+
.filter((el) => el && el.offsetParent !== null);
|
|
328
|
+
if (!els.length) return;
|
|
329
|
+
const idx = els.indexOf(document.activeElement);
|
|
330
|
+
let next = null;
|
|
331
|
+
if (e.key === "ArrowDown") { e.preventDefault(); next = idx === -1 ? els[0] : els[(idx + 1) % els.length]; }
|
|
332
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); next = idx === -1 ? els[els.length - 1] : els[(idx - 1 + els.length) % els.length]; }
|
|
333
|
+
else if (e.key === "Home") { e.preventDefault(); next = els[0]; }
|
|
334
|
+
else if (e.key === "End") { e.preventDefault(); next = els[els.length - 1]; }
|
|
335
|
+
if (next) next.focus();
|
|
336
|
+
};
|
|
337
|
+
|
|
160
338
|
return React.createElement("div", { className: "dshma-root", ref: rootRef },
|
|
161
339
|
React.createElement("button", { type: "button", className: "dshma-trigger", disabled: props.locked || !props.available || busy, onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu" },
|
|
162
340
|
React.createElement("span", { className: "dshma-trigger-label" }, label),
|
|
163
341
|
curEffort ? React.createElement("span", { className: "dshma-trigger-effort" }, `· ${curEffort}`) : null,
|
|
164
342
|
React.createElement(Chevron, { className: `dshma-chevron${open ? " dshma-chevron-open" : ""}` })
|
|
165
343
|
),
|
|
166
|
-
open ? React.createElement("div", { className: "dshma-menu", role: "menu" },
|
|
344
|
+
open ? React.createElement("div", { className: "dshma-menu", role: "menu", "aria-label": "Model selector", onKeyDown: onMenuKey },
|
|
345
|
+
React.createElement("div", { className: "dshma-search-wrap" },
|
|
346
|
+
React.createElement("input", { type: "text", className: "dshma-search", placeholder: "Search models…", value: query, onChange: (e) => setQuery(e.target.value), "aria-label": "Search models", autoFocus: true, ref: searchRef }),
|
|
347
|
+
query !== "" ? React.createElement("button", { type: "button", className: "dshma-search-clear", onClick: () => { setQuery(""); searchRef.current && searchRef.current.focus(); }, "aria-label": "Clear search", title: "Clear search" }, "✕") : null
|
|
348
|
+
),
|
|
167
349
|
React.createElement("div", { className: "dshma-menu-scroll" },
|
|
168
350
|
failure ? React.createElement("div", { className: "dshma-error" }, failure) : null,
|
|
169
351
|
state.error ? React.createElement("div", { className: "dshma-error" }, state.error) : null,
|
|
170
352
|
state.status === "loading" ? React.createElement("div", { className: "dshma-status" }, "Loading models…") : null,
|
|
171
353
|
(state.status === "selecting" || busy) ? React.createElement("div", { className: "dshma-status" }, "Selecting…") : null,
|
|
172
|
-
state.status !== "loading" &&
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
React.createElement("button", { type: "button", className: "dshma-provider-head", onClick: () => setVisionOpen((v) => !v), "aria-expanded": visionOpen },
|
|
176
|
-
React.createElement(Chevron, { className: `dshma-provider-chevron${visionOpen ? " dshma-provider-chevron-open" : ""}` }),
|
|
177
|
-
React.createElement("span", { className: "dshma-provider-name" }, "识图镜像(带图会话)"),
|
|
178
|
-
React.createElement("span", { className: "dshma-count" }, `${visionGroups.length}`)
|
|
179
|
-
),
|
|
180
|
-
visionOpen ? React.createElement("div", { className: "dshma-models" }, visionGroups.map(renderGroup)) : null
|
|
181
|
-
) : null
|
|
354
|
+
state.status !== "loading" && !hasAnyModel && q === "" ? React.createElement("div", { className: "dshma-empty" }, "No models available") : null,
|
|
355
|
+
q !== "" && !hasAnyModel ? React.createElement("div", { className: "dshma-empty" }, "No matches") : null,
|
|
356
|
+
visibleGroups.map(renderGroup)
|
|
182
357
|
)
|
|
183
358
|
) : null
|
|
184
359
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-accordion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Provider-folded model selector for the DSH Web composer with catalog-driven reasoning effort choices.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/lakabuji-arch/dsh-model-accordion.git"
|
|
49
|
+
"url": "git+https://github.com/lakabuji-arch/dsh-model-accordion.git"
|
|
50
50
|
},
|
|
51
51
|
"homepage": "https://github.com/lakabuji-arch/dsh-model-accordion#readme",
|
|
52
52
|
"bugs": {
|