Initial commit: thinking-level-selector DSH plugin with docs
This commit is contained in:
commit
15cc134310
6 files changed
+236
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
.DS_Store
|
||||||
@@ -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.
|
||||||
@@ -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));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- insert:
|
||||||
|
- id: thinking-level-selector
|
||||||
|
name: '@local/thinking-level-selector'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/** Host half of the thinking-level selector bundle; the Client module owns the rendering. */
|
||||||
|
export function apply() {}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user