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