# whale-girl: a QQ-pet style companion wired to DSH sessions

> A bundle plugin for the DSH Web GUI that floats a QQ-pet style character in the bottom-right corner, reacts to session events across fifteen states, and ships an optional standalone Tauri shell. The accumulation ledger behind seniority levels and titles is deliberately sealed, and every install needs a web restart.

**vlln/whale-girl** — DSH Web GUI 桌面宠物插件（QQ 宠物形态）：右下角悬浮、可拖拽/投喂/玩耍的积累型伙伴。

- Repository: https://github.com/vlln/whale-girl
- Stars: 342 · Forks: 21
- Language: JavaScript
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/vlln-whale-girl

## A bundle plugin with a cordis patch, and why the restart is mandatory

whale-girl is not a script dropped into a page. The root package.json carries a `dsh` block: `bundle.patch` points at `./cordis.patch.yml` and `client.platform` is set to `web`, so DSH treats the project as an official bundle plugin and manages it through a profile. Three sources are accepted, and they behave differently afterwards:

```sh
dsh plugin --profile web add "github:vlln/whale-girl#main"   # single-line git source (build artifacts committed)
# or npm source: dsh plugin --profile web add whale-girl@0.1.0
# or local directory: dsh plugin --profile web add <path-to-whale-girl>
```

The git source pins nothing except `#main`, and build artifacts are committed, so the install reflects whatever the branch head holds at that moment. The npm source pins a published version, and the only one named anywhere is `whale-girl@0.1.0`. The local directory source installs a checkout you already have, which is the one to use while you are editing `lib/`.

A restart is mandatory after the add, and again after every update, because bundle layers compose at startup. Updates go through `dsh plugin --profile web update whale-girl` or a manual switch of the git ref, then the same restart. Nothing in the README covers removal, so uninstalling means finding your plugin manager's own path for it.

Once the web layer is up, the pet floats in the bottom-right corner. Click it for a menu with feed and play entries, drag it to move it, hover for a status bar carrying seniority level, task count, and recent shared memories. Onboarding pages hide it, so a brand new profile will not show the pet on its first screen.

## The presence contract keeps the desktop pet from duplicating the in-page one

The `desktop/` directory is a second, optional home for the same character: a standalone companion app with a Node engine and a Tauri shell and zero runtime dependencies. It is not installed by `dsh plugin`, so nothing composes it for you and you enable it by hand.

```sh
# Prereqs: Node ≥18; the rendering shell needs Rust (cargo)
npm install -g whale-girl-desktop   # npm install (engine + Tauri shell source included)
whale-girl-desktop --headless       # headless: presence heartbeat + state polling + SSE
cd "$(npm root -g)/whale-girl-desktop/src-tauri" && cargo build --release  # first build ~5-15 min; artifact target/release/whale-girl-desktop (~12MB)
./target/release/whale-girl-desktop   # transparent always-on-top desktop pet (defaults to local DSH on 3080)
# WHALE_GIRL_BASE_URL=http://IP:PORT points at a non-local DSH
```

Prereqs are Node 18 or newer for the engine and cargo for the rendering shell, and the first `cargo build --release` takes roughly 5 to 15 minutes before the ~12MB artifact exists. Tauri v2 is the recommended shell; the legacy Electron shell is still kept and needs `npm i -D electron` pulled in as a dev dependency. The release binary runs transparent and always on top, aimed at a local DSH on port 3080 by default. `WHALE_GIRL_BASE_URL=http://IP:PORT` retargets it at another host, and `--headless` gives you the presence heartbeat, state polling, and the SSE stream with no window at all.

What keeps the two halves honest is the presence contract. The companion talks to `/state`, `/events`, `/presence`, `/interact`, `/config`, and `/assets`, all public endpoints, without touching the plugin. While it runs the in-page pet hides, and if the process exits or crashes the in-page pet returns on a 45 second TTL.

## Fifteen states, each hanging off a specific trigger

Fifteen states cover everything the pet does, and each one hangs off an event rather than a timer you tune. `idle` is the default standby, with random blinks and turns thrown in. Sixty seconds without interaction moves it to `sleep`, and any interaction wakes it through the `wake` transition. Dragging stretches the sprite diagonally into `drag`. Feeding enters `eat`, playing enters `play` as a ball toss, and both settle into `joy`.

The work-driven states come from session activity. Task completion, a level-up, a new title, or a finished round all fire `celebrate`. A failed task or a request error fires `error`, followed by a short `disappointed` beat. A new session brings up `welcome`. While a session runs or thinks the pet keeps `think` company, with an occasional `working` spell. When the session stops at an approval prompt it switches to `wait` and holds there. Periodically it wanders in `walk`.

Priorities between those states, the transitions, and the exact triggers live in docs/state-machine.md rather than in the settings surface, so deciding how often one state interrupts another means reading that document. The same split shows up in the growth side: docs/growth-system.md is where the seniority curve is written down, and neither file is editable from the plugin card.

## Two configuration layers, and a semantic core that is sealed

Settings arrive in two layers. Settings, Plugins, Whale Girl is an in-page card holding the high-frequency subset: show on page, size, opacity, wandering, sleep delay, and the feed and play reply pools, one pool per line. Changes there save and apply live with no restart, which makes the card the only part of the configuration you can touch without bouncing the web process.

```yaml
whale-girl:
  enabled: true      # web render toggle (false disables the in-page pet while a desktop companion runs)
  size: 110          # pet size px (64–160)
  opacity: 1         # default opacity (0.2–1)
  walk:
    enabled: true    # wandering toggle
  sleepAfterMs: 60000
```

The full option list stays in the `whale-girl:` section of settings.yaml under your DSH home, and that is where the advanced knobs such as window durations live, since they have no card control. `size` accepts 64 to 160 pixels, `opacity` runs from 0.2 to 1, `sleepAfterMs` defaults to 60000, and `enabled: false` turns off the in-page render while a desktop companion is on screen.

The part that is not configurable at all is the semantic layer. lib/src/config.mjs holds the complete option list and the reason the core stays closed: changing XP or title thresholds would break the accumulation ledger. The numbers behind seniority levels and titles are therefore sealed rather than exposed, which leaves you in charge of which states you see, how large and how transparent they are, and what the pet says when you feed it.

## A character is a manifest entry plus fifteen sprite sheets

Every character ships all fifteen states, and that is a read-only contract rather than a suggestion: the full spec is docs/sprites-spec.md. The Switch Character button cycles whatever the manifest holds, or you can pin one by setting `whale-girl:character` in localStorage. With a single-character manifest the button is greyed out and reads 'No other characters available', which is the state the repository ships in today.

Adding a character means writing fifteen sheets, adding the manifest entries, and satisfying `verify-assets`; docs/adding-a-character.md holds the quick guide. The artwork under originals/ is the source the generated sprites come from, and the credit goes to ZipZipPipe for the Whale Girl sticker character that the sprites were generated from.

Packaging narrows what you can add. The `files` array in package.json ships lib/index.mjs, lib/client.js, lib/client, lib/src, lib/assets/characters, lib/assets/manifest.json, cordis.patch.yml, and README.zh.md, so an asset folder living outside those paths never reaches an installed plugin. The `exports` map adds three more entry points for consumers: the plugin entry, the client bundle, and the patch file itself.

## Gates, decision records, and a layout meant to be copied

The repository presents itself as a complete exemplar of the bundle plugin format, and the layout is the argument. `lib/` holds the entry point, the logic, the client, and the assets, kept separate from `docs/`, `decisions/`, and `scripts/`. `cordis.patch.yml` is the patch the bundle block points at, and root AGENTS.md plus docs/AGENTS.md carry the conventions: every non-trivial change wants a decision record under decisions/, gate self-checks, and single-purpose commits.

The npm scripts are those gates, copied straight out of the root package.json:

```json
"gates": "node scripts/gates/run.mjs",
"gates:ci": "node scripts/gates/run.mjs --group ci",
"test": "node --test 'tests/*.test.mjs'",
"build:client": "node scripts/build-client.mjs",
"check:client": "node scripts/build-client.mjs --check",
```

`gates` runs everything and `gates:ci` runs the ci group only. `test` hands tests/*.test.mjs to node's own test runner. `build:client` and `check:client` both go through scripts/build-client.mjs, with the check form passing `--check` so a committed client bundle that has drifted from its source fails instead of shipping. esbuild is the only dev dependency; schemastery ^3.18.0 is the single runtime dependency, the schema library behind the config block. `.githooks/` at the root is where those checks get wired into commits.

If you are writing your own DSH plugin, the guidance this repository points at lives in plugin-registry: its make-dsh-plugin skill, a creating-a-plugin cookbook, and a gotchas reference.

## Version 0.1.0, no releases, last commit on 31 August 2026

Version 0.1.0 is what package.json declares, and it is the same version the npm install line names, while the git source tracks `#main` and takes whatever the branch head holds. The repository has no GitHub releases, so there is no tag to pin and no changelog to read: the git ref is the closest thing to a version boundary, and the npm route stays at 0.1.0 until a new version is published. The last commit landed on 31 August 2026, the project is not archived, and the tracker carries seven open issues against 339 stars and 18 forks.

Two gaps are worth naming before you commit to it. Nothing covers removal, only add and update, so there is no documented path back out. And the thresholds that decide level-ups and titles are surfaced nowhere at all, defensible for an accumulation ledger but opaque if you want to predict when a level arrives.

The license is MIT. A Chinese README ships alongside the English one, which is why README.zh.md appears in the published file list even though the plugin itself is a single language codebase.

## Conclusion

whale-girl makes sense for a DSH Web GUI user who wants session activity turned into something visible, and who is willing to restart the web process after every install or update. What holds up on inspection is the trigger mapping: fifteen states wired to real session events, a settings card that applies live, and a presence contract with a 45 second return so the in-page pet cannot end up duplicated on screen. Before you install, check that your profile accepts a bundle plugin carrying a cordis.patch.yml patch, read docs/state-machine.md if reaction timing matters to you, and decide between the git source and the pinned npm version knowing that only one of the two tracks main. Skip the desktop companion unless cargo is already installed, since its first build costs 5 to 15 minutes.

## FAQ

### Does whale-girl need a restart after installing the plugin?

Yes. Bundle layers compose at startup, so the web process has to start again before the pet appears bottom-right. The same restart is required after a later update, which runs as `dsh plugin --profile web update whale-girl`.

### Can whale-girl run outside the browser?

The desktop/ directory holds a standalone companion with a Node engine and a Tauri shell, installed on its own with `npm install -g whale-girl-desktop` and built with cargo. It targets a local DSH on port 3080 unless WHALE_GIRL_BASE_URL says otherwise.

### Why can I not change the XP or title thresholds in whale-girl?

They are sealed on purpose. Changing XP or title thresholds would break the accumulation ledger, so lib/src/config.mjs keeps the growth numbers out of the configurable surface entirely.

### Why is the Switch Character button greyed out in whale-girl?

The manifest ships a single character, so the button reads 'No other characters available'. Every new character has to ship all fifteen sprite states and pass verify-assets before it shows up in that cycle.

### What happens to the in-page whale-girl pet while the desktop companion runs?

The in-page pet hides while the companion runs and returns 45 seconds after it exits or crashes, which is the presence contract. The companion uses public endpoints such as /state and /presence without touching the plugin itself.

### How do I point the whale-girl desktop pet at a remote DSH host?

Set the base URL to that host and port, written as WHALE_GIRL_BASE_URL=http://IP:PORT. Left unset, the companion falls back to a local DSH on port 3080.

## Sources

- [Issues](https://github.com/vlln/whale-girl/issues)
- [License: MIT](https://github.com/vlln/whale-girl/blob/main/LICENSE)
- [README](https://github.com/vlln/whale-girl/blob/main/README.md)
- [vlln/whale-girl on GitHub](https://github.com/vlln/whale-girl)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/vlln-whale-girl
