commit 15cc1343103e73ec8e03351ab9149e7cf4543e60 Author: Agent Date: Sat Oct 10 17:37:32 2026 +0000 Initial commit: thinking-level-selector DSH plugin with docs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb38758 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*.bak +*.orig +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff24201 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# thinking-level-selector + +A compact **thinking / reasoning level** picker for the DeepSeek Harness (DSH) Web UI composer. + +## Features + +- Adds a selector to the composer activity bar (slot `conversation.input.activity`) listing the reasoning efforts the current model advertises (e.g. off / low / medium / high / xhigh). +- Uses the same model-selection data the model seat renders from, so it always matches the active provider/model. +- Friendlier labels (`xhigh` is shown as **Extra**); English and Chinese strings built in. + +## How it works + +| File | Role | +| --- | --- | +| `client.js` | Web client module (`@local/thinking-level-selector`). | +| `index.js` | Empty Host half. | +| `cordis.patch.yml` | Bundle patch inserting the plugin row. | + +## Installation + +Requirements: a working DeepSeek Harness (`dsh`) install with the **web** profile (or any profile that includes the Web UI). + +### Option A — install from git (recommended) + +```bash +dsh plugin --profile web add git+https://git.rizzy.dev/DSH-Plugins/thinking-level-selector.git +``` + +You can also paste `git+https://git.rizzy.dev/DSH-Plugins/thinking-level-selector.git` into **Settings → Plugins → Install** in the Web UI, or ask the agent to install it with its `plugin_manager` tool (`install_bundle`). + +The plugin manager adds `@local/thinking-level-selector` to `dependencies` and to `dsh.profile.bundles` in `~/.dsh/profiles/web/package.json`. + +### Option B — local checkout (for development) + +```bash +git clone https://git.rizzy.dev/DSH-Plugins/thinking-level-selector.git ~/dsh-plugins/thinking-level-selector +dsh plugin --profile web add link:$HOME/dsh-plugins/thinking-level-selector +``` + +With a `link:` install, edits in the checkout are picked up directly (client changes reload on page refresh, Host changes after a DSH restart or HMR reload). + +### Enable / verify + +1. Make sure `@local/thinking-level-selector` is listed in `dsh.profile.bundles` of `~/.dsh/profiles/web/package.json` (the installer does this; add it by hand if you edited `package.json` manually). +2. Restart DSH (`dsh web`) or reload the page. +3. Check **Settings → Plugins**: the `@local/thinking-level-selector` bundle should be enabled. + +### Uninstall + +```bash +dsh plugin --profile web remove @local/thinking-level-selector +``` + +### Updating + +Re-run the `add` command from Option A (or `git pull` in your Option B checkout). + +## Configuration + +None. Pick a model that supports reasoning efforts and the selector appears next to the composer input. diff --git a/client.js b/client.js new file mode 100644 index 0000000..63190a9 --- /dev/null +++ b/client.js @@ -0,0 +1,148 @@ +window.__ModuleLoader__.load({ + id: '@local/thinking-level-selector', + factory(require) { + const React = require('react'); + const h = React.createElement; + const NS = 'thinkingLevel'; + + const en = { + label: 'Thinking level', + none: 'Thinking', + default: 'Default', + loading: 'Loading…', + }; + const zh = { + label: '思考强度', + none: '思考', + default: '默认', + loading: '加载中…', + }; + + // DSH derives names from the level key ("xhigh" -> "Xhigh"); show friendlier ones. + const LEVEL_NAMES = { xhigh: 'Extra' }; + const levelName = (e) => LEVEL_NAMES[e.id] ?? e.name; + + // Same shape of data the model seat renders from. + function currentReasoning(state) { + const cur = state.current; + if (!cur) return undefined; + for (const g of state.groups) { + if (g.id !== cur.provider) continue; + const m = g.models.find((x) => x.id === cur.model); + if (m) return m.reasoning; + } + return undefined; + } + + function ThinkingLevel({ locked, available, directory, load, select, t }) { + const state = React.useSyncExternalStore( + (fn) => directory.subscribe(fn), + () => directory.getSnapshot(), + ); + const [open, setOpen] = React.useState(false); + const rootRef = React.useRef(null); + + React.useEffect(() => { if (available) load(); }, [available]); + React.useEffect(() => { + if (!open) return undefined; + const onDown = (e) => { + if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); + }; + const onKey = (e) => { if (e.key === 'Escape') setOpen(false); }; + document.addEventListener('mousedown', onDown); + document.addEventListener('keydown', onKey); + return () => { + document.removeEventListener('mousedown', onDown); + document.removeEventListener('keydown', onKey); + }; + }, [open]); + + const reasoning = currentReasoning(state); + if (!available || !reasoning || reasoning.efforts.length === 0) return null; + + const cur = state.current; + const effective = cur.reasoningEffort ?? reasoning.defaultEffort; + const active = reasoning.efforts.find((e) => e.id === effective); + const label = active ? levelName(active) : t('default'); + const disabled = locked || state.status === 'selecting'; + + const pick = (effort) => { + setOpen(false); + if (effort === effective) return; + select({ + provider: cur.provider, + model: cur.model, + ...(effort === undefined ? {} : { reasoningEffort: effort }), + }); + }; + + const itemStyle = (selected) => ({ + display: 'flex', alignItems: 'center', gap: 8, width: '100%', + padding: '6px 10px', border: 0, borderRadius: 6, cursor: 'pointer', + textAlign: 'left', font: 'inherit', fontSize: 13, whiteSpace: 'nowrap', + background: selected ? 'var(--dsw-alias-interactive-bg-hover)' : 'transparent', + color: 'var(--dsw-alias-label-primary, inherit)', + }); + + return h('div', { ref: rootRef, style: { position: 'relative', display: 'inline-flex' } }, + h('button', { + type: 'button', + disabled, + title: t('label'), + 'aria-label': t('label'), + 'aria-haspopup': 'listbox', + 'aria-expanded': open, + onClick: () => setOpen((v) => !v), + style: { + display: 'inline-flex', alignItems: 'center', gap: 4, + padding: '2px 8px', border: 0, borderRadius: 6, background: 'transparent', + font: 'inherit', fontSize: 13, cursor: disabled ? 'default' : 'pointer', + color: 'var(--dsw-alias-label-secondary, inherit)', + opacity: disabled ? 0.5 : 1, whiteSpace: 'nowrap', + }, + }, label, h('span', { 'aria-hidden': true, style: { fontSize: 10 } }, '▾')), + open && h('div', { + role: 'listbox', + style: { + position: 'absolute', bottom: 'calc(100% + 6px)', left: 0, zIndex: 50, + minWidth: 140, padding: 4, borderRadius: 8, + background: 'var(--dsw-specific-input-major, #fff)', + boxShadow: 'var(--dsw-elevation-soft, 0 4px 16px rgba(0,0,0,.18))', + border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3))', + }, + }, + reasoning.defaultEffort === undefined && h('button', { + type: 'button', role: 'option', 'aria-selected': effective === undefined, + onClick: () => pick(undefined), style: itemStyle(effective === undefined), + }, t('default')), + reasoning.efforts.map((e) => h('button', { + key: e.id, type: 'button', role: 'option', title: e.description, + 'aria-selected': e.id === effective, + onClick: () => pick(e.id), style: itemStyle(e.id === effective), + }, levelName(e), e.id === reasoning.defaultEffort ? h('span', { style: { opacity: 0.55, fontSize: 11 } }, '· ' + t('default')) : null)), + ), + ); + } + + return { + inject: ['slots', 'locale', 'sessions', 'remote', 'remote.session', 'modelDirectories'], + apply(ctx) { + ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'thinking-level: dictionaries'); + ctx.slots.inject('conversation.input.activity', () => ctx.slots.register({ + name: 'conversation.input.activity', + locale: NS, + inject: (sessionId) => { + const directory = ctx.modelDirectories.directoryFor(sessionId); + const available = ctx.sessions.subagentAddress(sessionId) === undefined; + return { + available, + directory: directory.store, + load: () => { if (available) directory.load().catch(() => {}); }, + select: (sel) => available ? directory.select(sel) : Promise.resolve(undefined), + }; + }, + }, ThinkingLevel)); + }, + }; + }, +}); diff --git a/cordis.patch.yml b/cordis.patch.yml new file mode 100644 index 0000000..337ad43 --- /dev/null +++ b/cordis.patch.yml @@ -0,0 +1,3 @@ +- insert: + - id: thinking-level-selector + name: '@local/thinking-level-selector' diff --git a/index.js b/index.js new file mode 100644 index 0000000..9f2dd52 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +/** Host half of the thinking-level selector bundle; the Client module owns the rendering. */ +export function apply() {} diff --git a/package.json b/package.json new file mode 100644 index 0000000..85984b3 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "@local/thinking-level-selector", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { ".": "./index.js", "./client": "./client.js" }, + "dsh": { + "bundle": { "patch": "./cordis.patch.yml" }, + "client": { + "platform": "web", + "immediately": true, + "inject": [ + "@deepseek-ai/dsh-client-ui-conversation", + "@deepseek-ai/dsh-client-ui-model-selection", + "@deepseek-ai/dsh-client-locale" + ] + } + } +}