dsh-model-accordion 0.1.1

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 dsh-model-accordion contributors
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,54 @@
1
+ # dsh-model-accordion
2
+
3
+ Provider-folded model selector for the DeepSeek Harness Web composer.
4
+
5
+ ## What it does
6
+
7
+ - Replaces the single composer model seat with a provider-folded selector.
8
+ - Keeps provider groups collapsed by default and opens models on demand.
9
+ - Reads reasoning-effort choices from each model's resolved catalog metadata instead of maintaining a hard-coded effort list.
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.
12
+ - Keeps the popup height-bounded with an internal scroll area and wraps effort controls below the model name.
13
+ - Uses DSH semantic theme tokens for colors and supports light/dark theme changes.
14
+
15
+ ## Requirements
16
+
17
+ - DeepSeek Harness Web `0.1.0-rc.8` or newer.
18
+ - A Web profile with the official model-selection package and model-directory service.
19
+ - React 18 supplied by the DSH Web runtime.
20
+
21
+ This is a Web UI plugin. It does not add a Host service, model provider, vision backend, or model catalog. Provider availability and image-session admission remain controlled by DSH and any installed provider/vision plugins.
22
+
23
+ ## Install from a package or repository
24
+
25
+ ```sh
26
+ dsh plugin --profile web add dsh-model-accordion
27
+ ```
28
+
29
+ For a local checkout during development:
30
+
31
+ ```sh
32
+ dsh plugin --profile web add file:/absolute/path/to/dsh-model-accordion
33
+ ```
34
+
35
+ 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
+
37
+ ## Behavior and limitations
38
+
39
+ - The selector submits the exact provider, model, and catalog-declared reasoning effort returned by the DSH model directory.
40
+ - 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 optional `识图镜像(带图会话)` section is shown when provider names identify vision-router mirror routes. It is kept collapsed to avoid duplicating every provider in the main list.
42
+ - The plugin currently uses English fallback status text and a Chinese label for the vision-mirror section; full locale integration is planned for a later release.
43
+
44
+ ## Development checks
45
+
46
+ ```sh
47
+ node --check lib/client.js
48
+ node --check lib/index.js
49
+ npm pack --dry-run --ignore-scripts
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,4 @@
1
+ # Persistent local Web client plugin registration.
2
+ - insert:
3
+ - id: ui-model-accordion
4
+ name: dsh-model-accordion
package/lib/client.js ADDED
@@ -0,0 +1,223 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-model-accordion",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ const React = require("react");
8
+
9
+ // CSS is self-contained and theme-token driven (mirrors the shipped
10
+ // model-selection geometry). The popup scroll region carries its own
11
+ // max-height so the menu is ALWAYS height-bounded and scrolls internally,
12
+ // independent of any ancestor flex/layout context.
13
+ const CSS = `
14
+ .dshma-root{position:relative;min-width:0}
15
+ .dshma-trigger{box-sizing:border-box;display:flex;align-items:center;gap:4px;height:28px;max-width:min(360px,45cqw);padding:0 4px 0 8px;border:none;border-radius:24px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer;outline:none;min-width:0}
16
+ .dshma-trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
17
+ .dshma-trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
18
+ .dshma-trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
19
+ .dshma-trigger-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
20
+ .dshma-trigger-effort{color:var(--dsw-alias-label-caption);flex:none}
21
+ .dshma-chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}
22
+ .dshma-chevron-open{transform:rotate(180deg)}
23
+ .dshma-menu{box-sizing:border-box;z-index:50;position:absolute;bottom:calc(100% + 8px);right:0;width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);padding:4px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);border-radius:12px;box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;overflow:hidden}
24
+ .dshma-menu-scroll{box-sizing:border-box;max-height:min(340px,calc(100vh - 120px));overflow-y:auto;display:flex;flex-direction:column;min-width:0;-webkit-overflow-scrolling:touch}
25
+ .dshma-status,.dshma-empty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}
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
+ .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
+ .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
+ .dshma-provider-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
31
+ .dshma-provider-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
32
+ .dshma-count{color:var(--dsw-alias-label-caption);font-size:11px;font-weight:500;flex:none}
33
+ .dshma-provider-chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}
34
+ .dshma-provider-chevron-open{transform:rotate(180deg)}
35
+ .dshma-models{padding:0 4px 4px}
36
+ .dshma-option{box-sizing:border-box;width:100%;padding:7px 8px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;display:block}
37
+ .dshma-option:hover{background:var(--dsw-alias-interactive-bg-hover)}
38
+ .dshma-option-selected{background:var(--dsw-alias-interactive-bg-hover)}
39
+ .dshma-option-row{display:flex;align-items:center;gap:8px;min-width:0;width:100%;border:none;background:transparent;padding:0;color:inherit;cursor:pointer;font-family:inherit;text-align:left}
40
+ .dshma-option-main{flex:1;min-width:0}
41
+ .dshma-option-name{font-size:13px;font-weight:500;line-height:18px;overflow-wrap:anywhere;display:block}
42
+ .dshma-option-desc{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;overflow-wrap:anywhere;display:block}
43
+ .dshma-check{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px}
44
+ .dshma-efforts{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;padding-left:0}
45
+ .dshma-effort{box-sizing:border-box;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l3);border-radius:8px;padding:2px 7px;font-size:11px;line-height:16px;font-family:inherit}
46
+ .dshma-effort:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
47
+ .dshma-effort-selected{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary)}
48
+ .dshma-failure{color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:7px 8px;font-size:12px;line-height:18px}
49
+ `;
50
+
51
+ function Chevron(props) {
52
+ return React.createElement("svg", { className: props.className || "dshma-chevron", width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": "true" },
53
+ React.createElement("path", { d: "M3.5 5.25 7 8.75l3.5-3.5", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" })
54
+ );
55
+ }
56
+
57
+ function useStore(store) {
58
+ return React.useSyncExternalStore(
59
+ (fn) => store.subscribe(fn),
60
+ () => store.getSnapshot(),
61
+ () => store.getSnapshot()
62
+ );
63
+ }
64
+
65
+ function effortOptions(model) {
66
+ const es = model && model.reasoning && Array.isArray(model.reasoning.efforts) ? model.reasoning.efforts : [];
67
+ return es.filter((e) => e && typeof e.id === "string");
68
+ }
69
+
70
+ function Accordion(props) {
71
+ const state = useStore(props.directory);
72
+ const [open, setOpen] = React.useState(false);
73
+ const [expanded, setExpanded] = React.useState({});
74
+ const [busy, setBusy] = React.useState(false);
75
+ const [failure, setFailure] = React.useState(null);
76
+ const groups = Array.isArray(state.groups) ? state.groups : [];
77
+ const rootRef = React.useRef(null);
78
+ const [visionOpen, setVisionOpen] = React.useState(false);
79
+ // Vision-router twin detection: only treat a group as the auto-vision
80
+ // wrapper when its display name carries the 自动识图 token or its provider
81
+ // id ends in "-vision" (official vision-router uses route id
82
+ // "deepseek-vision" + display "DeepSeek + 自动识图").
83
+ const isTwin = (g) => /自动识图/.test(g.name || "") || /(?:-|\b)vision$/i.test((g.id || "").trim());
84
+ const mainGroups = groups.filter((g) => !isTwin(g));
85
+ const visionGroups = groups.filter(isTwin);
86
+
87
+ React.useEffect(() => {
88
+ if (!open) return;
89
+ const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
90
+ const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
91
+ document.addEventListener("mousedown", onDown);
92
+ document.addEventListener("keydown", onKey);
93
+ return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); };
94
+ }, [open]);
95
+
96
+ React.useEffect(() => { if (open && props.available) props.load(); }, [open, props.available]);
97
+
98
+ const select = (group, model, effort) => {
99
+ if (!props.available || busy) return;
100
+ setBusy(true);
101
+ setFailure(null);
102
+ const sel = { provider: group.id, model: model.id };
103
+ if (effort !== void 0) sel.reasoningEffort = effort;
104
+ props.select(sel).then((ok) => {
105
+ if (!ok) throw new Error("model selection failed");
106
+ setOpen(false);
107
+ }).catch((e) => {
108
+ setFailure(e && e.message ? e.message : "model selection failed");
109
+ }).finally(() => setBusy(false));
110
+ };
111
+
112
+ const current = state.current;
113
+ const curGroup = groups.find((g) => g.id === current?.provider);
114
+ const curModel = curGroup ? curGroup.models.find((m) => m.id === current?.model) : void 0;
115
+ const label = curModel ? curModel.name : (current ? `${current.provider}/${current.model}` : "Select model");
116
+ const curEffort = curModel && current && current.provider === curGroup.id && current.model === curModel.id
117
+ ? (current.reasoningEffort || (curModel.reasoning && curModel.reasoning.defaultEffort))
118
+ : void 0;
119
+
120
+ const renderGroup = (group) => {
121
+ const isOpen = expanded[group.id] === true;
122
+ return React.createElement("div", { className: "dshma-provider", key: group.id },
123
+ React.createElement("button", { type: "button", className: "dshma-provider-head", onClick: () => setExpanded((old) => ({ ...old, [group.id]: !isOpen })), "aria-expanded": isOpen },
124
+ React.createElement(Chevron, { className: `dshma-provider-chevron${isOpen ? " dshma-provider-chevron-open" : ""}` }),
125
+ React.createElement("span", { className: "dshma-provider-name" }, group.name || group.id),
126
+ React.createElement("span", { className: "dshma-count" }, `${group.models.length}`)
127
+ ),
128
+ isOpen ? React.createElement("div", { className: "dshma-models" }, group.models.map((model) => {
129
+ const selected = current && current.provider === group.id && current.model === model.id;
130
+ const efforts = effortOptions(model);
131
+ const defEffort = model.reasoning && model.reasoning.defaultEffort;
132
+ // A "provider default" chip clears a pinned effort so the provider's
133
+ // built-in default applies again (mirrors the official provider-default
134
+ // entry in dsh-client-ui-model-selection @315-324).
135
+ const defaultChip = defEffort !== void 0 ? [{ id: "__provider_default__", name: "默认" }] : [];
136
+ 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
+ const pinnedEffort = selected ? current.reasoningEffort : void 0;
140
+ 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: () => {
142
+ // Re-clicking the already-selected model must NOT re-submit and
143
+ // thereby wipe a custom (pinned) effort — just close the menu.
144
+ // Mirrors dsh-client-ui-model-selection @397-400 / @718.
145
+ if (selected) { setOpen(false); return; }
146
+ select(group, model, defEffort);
147
+ }, disabled: busy },
148
+ React.createElement("span", { className: "dshma-option-main" },
149
+ React.createElement("span", { className: "dshma-option-name" }, model.name || model.id),
150
+ model.description ? React.createElement("span", { className: "dshma-option-desc" }, model.description) : null
151
+ ),
152
+ selected ? React.createElement("span", { className: "dshma-check", "aria-label": "selected" }, "✓") : null
153
+ ),
154
+ chips.length ? React.createElement("div", { className: "dshma-efforts", "aria-label": "Reasoning effort" }, chips.map((it) => React.createElement("button", { type: "button", key: it.id, className: `dshma-effort${selected && (it.id === "__provider_default__" ? !current.reasoningEffort : pinnedEffort === it.id) ? " dshma-effort-selected" : ""}`, onClick: () => select(group, model, it.id === "__provider_default__" ? void 0 : it.id), disabled: busy }, it.name || it.id))) : null
155
+ );
156
+ })) : null
157
+ );
158
+ };
159
+
160
+ return React.createElement("div", { className: "dshma-root", ref: rootRef },
161
+ 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
+ React.createElement("span", { className: "dshma-trigger-label" }, label),
163
+ curEffort ? React.createElement("span", { className: "dshma-trigger-effort" }, `· ${curEffort}`) : null,
164
+ React.createElement(Chevron, { className: `dshma-chevron${open ? " dshma-chevron-open" : ""}` })
165
+ ),
166
+ open ? React.createElement("div", { className: "dshma-menu", role: "menu" },
167
+ React.createElement("div", { className: "dshma-menu-scroll" },
168
+ failure ? React.createElement("div", { className: "dshma-error" }, failure) : null,
169
+ state.error ? React.createElement("div", { className: "dshma-error" }, state.error) : null,
170
+ state.status === "loading" ? React.createElement("div", { className: "dshma-status" }, "Loading models…") : null,
171
+ (state.status === "selecting" || busy) ? React.createElement("div", { className: "dshma-status" }, "Selecting…") : null,
172
+ state.status !== "loading" && mainGroups.length === 0 && visionGroups.length === 0 ? React.createElement("div", { className: "dshma-empty" }, "No models available") : null,
173
+ mainGroups.map(renderGroup),
174
+ visionGroups.length ? React.createElement("div", { className: "dshma-vision", key: "__vision__" },
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
182
+ )
183
+ ) : null
184
+ );
185
+ }
186
+
187
+ const inject = ["slots", "modelDirectories", "sessions"];
188
+
189
+ function injectCss() {
190
+ if (typeof document === "undefined") return () => {};
191
+ const prev = document.querySelector('style[data-dshma-css]');
192
+ if (prev) prev.remove();
193
+ const tag = document.createElement("style");
194
+ tag.setAttribute("data-dshma-css", "1");
195
+ tag.textContent = CSS;
196
+ document.head.appendChild(tag);
197
+ return () => tag.remove();
198
+ }
199
+
200
+ function apply(ctx) {
201
+ const cssDispose = ctx.effect(injectCss, "dsh-model-accordion: styles");
202
+ ctx.slots.inject("conversation.input.model", () => ctx.slots.register({
203
+ name: "conversation.input.model",
204
+ priority: -1,
205
+ inject: (sessionId) => {
206
+ const directory = ctx.modelDirectories.directoryFor(sessionId);
207
+ const available = ctx.sessions.subagentAddress(sessionId) === void 0;
208
+ return {
209
+ available,
210
+ directory: directory.store,
211
+ load: () => { if (available) directory.load().catch(() => {}); },
212
+ select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false)
213
+ };
214
+ }
215
+ }, Accordion));
216
+ return cssDispose;
217
+ }
218
+
219
+ exports.apply = apply;
220
+ exports.inject = inject;
221
+ return module.exports;
222
+ }
223
+ });
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ const name = "dsh-model-accordion";
2
+
3
+ function apply() {}
4
+
5
+ const inject = [];
6
+
7
+ export { apply, inject, name };
8
+ export default { apply, inject, name };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "dsh-model-accordion",
3
+ "version": "0.1.1",
4
+ "description": "Provider-folded model selector for the DSH Web composer with catalog-driven reasoning effort choices.",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "model-selector",
10
+ "reasoning-effort"
11
+ ],
12
+ "type": "module",
13
+ "main": "lib/index.js",
14
+ "exports": {
15
+ ".": "./lib/index.js",
16
+ "./client": "./lib/client.js",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "dsh": {
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ },
23
+ "client": {
24
+ "inject": [
25
+ "@deepseek-ai/dsh-client-runtime",
26
+ "@deepseek-ai/dsh-client-locale",
27
+ "@deepseek-ai/dsh-client-ui-theme",
28
+ "@deepseek-ai/dsh-client-ui-model-selection"
29
+ ],
30
+ "platform": "web"
31
+ }
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^18.2.0"
35
+ },
36
+ "files": [
37
+ "lib",
38
+ "cordis.patch.yml",
39
+ "README.md",
40
+ "LICENSE",
41
+ "package.json"
42
+ ],
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": ">=22"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/lakabuji-arch/dsh-model-accordion.git"
50
+ },
51
+ "homepage": "https://github.com/lakabuji-arch/dsh-model-accordion#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/lakabuji-arch/dsh-model-accordion/issues"
54
+ },
55
+ "publishConfig": {
56
+ "registry": "https://registry.npmjs.org/"
57
+ },
58
+ "scripts": {
59
+ "check": "node --check lib/client.js && node --check lib/index.js",
60
+ "test": "node --test tests/package.test.js"
61
+ }
62
+ }