Bundle
dsh-villager-hmm
Plays a vanilla Minecraft villager "hmm" whenever the model hums inside its reasoning chain.
- Source
- nagatoquin33
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 16 hours ago
Readme
# dsh-villager-hmm
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin.
Whenever the model goes *"hmm"* — inside its chain of thought, or in the reply
itself — a Minecraft villager hums back at you.
A villager stands in the top-right corner, bobs on every hit, and carries the
controls: pause, volume, which stream to watch, and the matcher itself.
**Drag the title bar to move it; double-click the bar to send it back to the
corner.** The position is remembered in `localStorage`. It defaults to the
top-right rather than the bottom-right on purpose — the composer is full-width,
so a bottom-anchored overlay would sit on top of the send button.
```
┌──────────┬──────────────────────────────────┐
│ villager │ Villager hmm - │
│ figure │ hits 12 · played 12 │
├──────────┴──────────────────────────────────┤
│ Reasoning 1284 chars Reply 356 chars │
│ Sounds 2 clips │
│ [ Listening · click to pause ] [ Play once ] │
│ Volume ─●─ │
│ Source [ Reasoning only ▾ ] │
│ Matcher [ (?<!…) ] [Apply] [Default] │
│ Recent hmm 嗯 唔 │
└──────────────────────────────────────────────┘
```
The panel follows the interface language: it registers English and Chinese
dictionaries with the shared `locale` service and re-renders when the locale
changes. If that service is unavailable it falls back to English rather than
failing to render.
## Install
```sh
dsh plugin --profile web add dsh-villager-hmm
```
That is the whole install. The package declares `dsh.bundle`, so `dsh plugin`
registers it as a profile layer by itself — there is no configuration file to
edit. Restart the profile when it finishes.
Then fetch the assets, from the profile directory (where the package's `bin` is
linked):
```sh
cd "$DSH_HOME/profiles/web"
npx dsh-villager-hmm-assets
```
Skip this step and the plugin still loads. It shows its panel and prints the
exact command to run — with an absolute path, so it works from any directory —
and never fails silently.
## Assets
**This package ships no Minecraft material.** The two villager sounds and the
villager skin are fetched on your machine into a per-user cache:
```
$DSH_HOME/.dsh-villager-hmm/assets/
idle1.ogg villager "hmm"
idle2.ogg villager "hmm", second take
villager.png the 64x64 villager skin, cropped into the figure by CSS
```
The panel shows the exact command if the cache is empty. Two sources are
supported:
| Flag | Meaning |
| --- | --- |
| *(none)* | Download from a public mirror of the vanilla 1.21.4 client assets |
| `--from <dir>` | Copy from an asset tree you extracted yourself — **the option that matches the game's terms** |
| `--base <url>` | Your own mirror with the same layout |
| `--dir <path>` | Use a different cache directory |
| `--force` | Re-fetch files that are already there |
```sh
# from an extraction you made yourself
node "$DSH_HOME/profiles/web/node_modules/dsh-villager-hmm/scripts/fetch-assets.mjs" \
--from "C:/mc-extract/assets/minecraft"
```
The cache lives outside `node_modules`, so it survives plugin upgrades. The host
half re-reads it per request, so running the fetch script takes effect on a page
reload — no harness restart.
## Configuration
Defaults live in the row. To override them, replace the insert in your own
`$DSH_HOME/profiles/web/cordis.patch.yml`:
```yaml
- insert:
- id: dsh-villager-hmm
name: 'dsh-villager-hmm'
config:
enabled: true
mode: both # 'reasoning' (chain of thought only) | 'both'
pattern: '(?<![0-9A-Za-z])(?:h+m+|mhm+|m{2,})|[嗯唔哼呃]'
assetDir: 'D:/my-assets'
```
Everything except `assetDir` is also editable from the floating panel.
`mode` defaults to `both` on purpose: plenty of models expose no separate
reasoning channel, and a plugin that silently does nothing looks broken. Switch
to `reasoning` once you have confirmed your model streams `reasoning-delta` —
the panel's **Reasoning** counter tells you: if it climbs, you have a reasoning
channel and can narrow the scope.
## The matcher
The default pattern is
`(?<![0-9A-Za-z])(?:h+m+|mhm+|m{2,})|[嗯唔哼呃]`.
It uses a **lookbehind rather than `\b`**, for two reasons:
- `\b` is ASCII-only in JavaScript, so it behaves unpredictably next to CJK
characters — `嗯hmm` would not match.
- The CJK branch deliberately has no boundary at all, because those characters
are always standalone interjections.
`h+m+` covers `hm`, `hmm`, `hmmm…`; `mhm+` covers the closed-mouth variant;
`m{2,}` covers a bare `mm`. None of them fire inside ordinary words — `comma`,
`comment`, `command` and `ahmm` all stay silent.
Write your own in the panel's **匹配式** field and hit **应用**. A pattern that
does not compile is reported inline instead of being swallowed.
## How it works
The host half listens to the `agent/assistant-stream` event and scans
`reasoning-delta` (and optionally `text-delta`) chunks against the matcher. The
browser half polls that counter over the plugin's own HTTP prefix and plays a
sound per hit.
**The plugin only consumes.** It registers one event listener and one HTTP
prefix route; it publishes no service and patches nothing. If the harness stops
emitting the event, or the overlay slot disappears, the plugin degrades to doing
nothing — it does not take the boot or the page with it.
Two details shape the scanner:
**`frame.revision` is not a stream identity.** It increments on *every* frame.
Keying the scan buffer by it resets the buffer on every delta, so the matcher
only ever sees one character and never matches. The buffer is keyed by
`attemptId:turn:step`.
**The scan is inline, not debounced.** Deltas arrive far faster than any
debounce window, so every new delta cancels the pending timer before it can
fire. Because the matcher keeps a consumed-offset cursor, scanning on every
delta is amortised O(n). A match that ends exactly at the buffer end is deferred
unless the attempt is finishing, so `hm` is never counted before the next delta
can turn it into `hmm`.
## Routes
The plugin owns one HTTP prefix, `/dsh-villager-hmm`:
| Route | Purpose |
| --- | --- |
| `GET /state?cursor=N` | counters, asset status, and triggers newer than `N` |
| `GET /config?enabled&mode&pattern` | change settings at runtime |
| `GET /sound/<n>.ogg` | the audio, read from the cache |
| `GET /texture.png` | the villager skin, read from the cache |
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for the test suites, what CI enforces,
and the release process.
## License
The code is MIT (see `LICENSE` — kept verbatim, with nothing appended, so
GitHub detects it as MIT rather than "Other").
No third-party assets are bundled or redistributed: `scripts/fetch-assets.mjs`
downloads them on your machine, at your direction, into a local cache. See
[`NOTICE`](NOTICE) for the asset terms. Minecraft is a trademark of Mojang
Studios; this project is not affiliated with or endorsed by Mojang or
Microsoft.
Install
dsh plugin --profile web add github:nagatoquin33/dsh-villager-hmm
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-villager-hmm from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.