Skip to content
dsh.fish
Bundle

@dsh-external/bubble-explain

在 harness 对话中框选任意文字后点击「解释」按钮,弹出 Markdown 实时流式解释气泡,支持递归追问,配置常驻在设置→通用页顶部

Source
Hanmiao33
stars
7 stars
License
BSD-3-Clause
Updated
Updated 7 hours ago

Readme

# 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

<p align="center">
  <video src="docs/promo/dsh-bubble-explain-promo.mp4" poster="docs/promo/poster.png" controls width="720"></video>
</p>

[Open the video file](docs/promo/dsh-bubble-explain-promo.mp4)

## 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:

```bash
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:

```bash
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

1. In a conversation, select any text with the mouse.
2. Click the **解释** button that appears.
3. An explanation bubble streams in next to the selection.
4. 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:

```yaml
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):

```bash
DSH_CHECKOUT=<checkout> bash scripts/build.sh
```

Client bundle:

```bash
npm run build:client    # tsdown → lib/client.js
```

Checks that need no checkout:

```bash
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](LICENSE)

Install

dsh plugin --profile web add github:Hanmiao33/dsh-bubble-explain

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source