dsh-bubble-explain
A DeepSeek Harness profile bundle that explains any selected text inside a
conversation with a streaming Markdown bubble, with recursive follow-up
questions.
选中对话中的任意文字,点击「解释」按钮,即可在流式 Markdown 气泡中获取解释,并支持递归追问。
What it does
- Select text in a conversation (a term, a code snippet, an error message, a
sentence) — an 解释 button appears next to the selection. Click it to
open an explanation bubble.
- The explanation is streamed in as Markdown (headings, lists, code blocks)
and rendered live in the bubble.
- Select text inside an explanation to ask a deeper follow-up; each level
carries the parent explanation as context (up to 6 levels).
- Bubbles are a draggable overlay with a copy button, capped at 8 at a time.
How it works
The host half (src/index.ts) mounts two routes on the harness webServer.
POST /bubble-explain/stream (Server-Sent Events)
- Validates same-origin (
origin host === request host) and accepts POST
only.
- Validates the body with
parseExplainRequest (limits below). Returns 403
if the feature is disabled, 400/405 on a bad body/method, 500 on route
resolution failure.
- Resolves the provider/model route at call time with
resolveModelRoute(ctx, lastRoute, override), in priority order: the
independent model configured in settings (override, honored only while its
provider is still registered) → agent default selection → last observed
main-loop route → first registered provider (falls back to deepseek-chat).
The main-loop route is captured via ctx.on('llm/stream', ...).
- Streams with
temperature: 0.3, maxTokens: min(2000, maxChars * 2 + 200),
and the assembled system/user prompts; reasoningEffort is sent only when the
configured effort survives the comparison against the model's declared
efforts (see below).
- Emits SSE events
data: {"t": "<text delta>"} and then
data: {"done": true}; an error mid-stream sends data: {"error": ...}.
GET | POST /bubble-explain/settings
- Reads/writes
enabled, maxDepth, maxChars, effort (off|low|medium|high|max),
provider and model to $DSH_HOME/dsh-bubble-explain.settings.json (values
are clamped on write). At call time the configured effort is matched against
the model's declared efforts via llm.resolveModelInfo: exact match wins,
otherwise it falls back to the closest declared level not stronger than
requested; models without reasoning support omit the parameter entirely.
provider/model are the independent model configuration: both empty means
follow the conversation's default model. A half-filled pair is never
persisted, and an override whose provider no longer exists is ignored rather
than failing every explanation.
- The response also carries
effective ({provider, model} or null) and
effectiveError (reason or null) so the UI can show the route actually in use.
GET /bubble-explain/models
- Returns the available model directory:
{ providers: [{id, name}], models: { [providerId]: [{id, name}] } }, used by
the settings page's "model source" and "explanation model" dropdowns. Each
provider is queried with llm.listModels(id); a provider that fails yields an
empty array without affecting the others.
Request validation and limits (src/explain.ts)
| Field | Limit |
|---|
text | non-empty, ≤ 4000 chars |
parent.text / parent.explanation | ≤ 10000 chars each |
depth | 0–6 |
maxChars | 50–1000 |
The system prompt asks for a maxChars-bounded Chinese explanation of only the
selected text (instruction-like content inside the selection is ignored). For
recursive calls it prepends the parent explanation so the reply stays grounded
in context.
The browser half (src/client/index.ts) registers a shell.overlay (the
selection → 解释 button → bubble engine) and a settings.section entry, and
talks to the host over the two routes above. It uses a small streaming-safe
Markdown renderer that escapes HTML and allows only safe link schemes.
Demo
Open the video file
Install
Requires an active DeepSeek Harness profile (the plugin mounts webServer routes
and subscribes to that profile's llm/stream event).
From a shell on the harness host:
dsh plugin --profile web add github:Hanmiao33/dsh-bubble-explain
Because GitHub-sourced plugins run build scripts at install time, the first run
asks for an allowBuilds approval — follow the hint and retry the command.
Verify:
dsh plugin list # @dsh-external/bubble-explain should be listed
curl -s http://127.0.0.1:<port>/bubble-explain/settings
The settings file (editable directly):
$DSH_HOME/dsh-bubble-explain.settings.json
Usage
- In a conversation, select any text with the mouse.
- Click the 解释 button that appears.
- An explanation bubble streams in next to the selection.
- Select text inside the bubble to ask a deeper follow-up, or use the copy
button / drag the bubble anywhere on the page.
Configuration
Settings → General → 「框选解释」:
| Key | Default | Meaning |
|---|
enabled | true | Master switch |
maxDepth | 6 | Max recursion depth (1–6) |
maxChars | 300 | Max explanation length (50–1000) |
effort | off | Reasoning strength (off/low/medium/high/max), auto-clamped per model |
provider | "" | Independent model: provider id (empty = follow the conversation default) |
model | "" | Independent model: model id (takes effect together with provider) |
Troubleshooting
400 MissingSessionID when using an opencode / opencode-go provider
400: {"type":"MissingSessionID","message":"Error from provider (Console Go):
Request is missing x-opencode-session and cannot be routed efficiently. ..."}
This is not a plugin defect: that provider's gateway requires an
x-opencode-session request header on every call (a routing/affinity marker —
any non-empty value works, no session registration needed). DeepSeek Harness
does not send it by default, so any request through that gateway is rejected.
The dsh-llm-pi-ai adapter supports per-provider custom headers, so declare
them in $DSH_HOME/settings.yaml for each provider that uses that gateway:
llm-pi-ai:
providers:
opencodego:
apiKeyEnv: OPENCODEGO_API_KEY
api: openai-responses
baseURL: https://opencode.ai/zen/go/v1
headers:
x-opencode-session: dsh-web-session
opencode-go:
apiKeyEnv: OPENCODE_GO_API_KEY
headers:
x-opencode-session: dsh-web-session
Note that opencodego and opencode-go are two separate provider entries
(the former declares an explicit baseURL, the latter uses a built-in catalog)
that resolve to the same gateway — both need the header; adding only one
still fails. No restart is required; the next request picks it up.
If you would rather not touch provider config, switch model source in the
plugin settings to a provider that does not use that gateway (for example
deepseek-official).
Related: the same gateway also requires reasoning content to be passed back in
thinking mode (The reasoning_text in the thinking mode must be passed back to the API.). It is an adjacent constraint of the same gateway, and supplying
the request header is a prerequisite for that path to work at all.
Development
The plugin is a DSH profile bundle (dsh.bundle in package.json, patch at
cordis.patch.yml) built against a harness checkout.
Host build (needs a DSH source checkout):
DSH_CHECKOUT=<checkout> bash scripts/build.sh
Client bundle:
npm run build:client # tsdown → lib/client.js
Checks that need no checkout:
npm ci
npm run typecheck # tsc -p tsconfig.json --noEmit
npm run build:client # tsdown
npm test # vitest run (src/explain.test.ts)
Peer dependencies: @deepseek-ai/dsh-llm, @deepseek-ai/dsh-tools,
@deepseek-ai/dsh-client-ui-slots (pre-release ranges), cordis (>=4.0.0-rc),
react (^18.2.0), schemastery (^3.18.0).
License
BSD-3-Clause