Skip to content
dsh.fish
Bundle

@deepseek-ai/dsh-client-ui-whale-pet

Frame-wide 3D whale desktop pet with edge habitat, hover reactions, and smooth shape-aware dragging

Source
rongzi5
stars
16 stars
License
MIT
Updated
Updated 2 days ago

Readme

# @deepseek-ai/dsh-client-ui-whale-pet

English | [中文](README.zh.md)

Persistent browser plugin for the DeepSeek Harness 3D whale desktop pet.

The plugin registers one additive `whale-pet` entry in `shell.overlay`. It renders the procedural whale directly with the bundled `three@0.147.0`; it does not use a CDN, an iframe, a host RPC, or a workspace-absolute path.

## Behavior

- Rests at the viewport edge and patrols only at long, randomized intervals.
- Looks toward the pointer and shows a short heart reaction on hover or activation.
- Uses several body-shaped hit regions for dragging instead of a rectangular canvas target.
- Uses a continuous model yaw, so look, drag, patrol, and celebration all follow their movement direction instead of snapping left/right.
- Runs screen motion and Three.js rendering in one `requestAnimationFrame` loop.
- Uses exponential, non-overshooting drag follow and frame-rate-independent release inertia.
- Integrates animation phase over time so changing speed cannot jump the body, tail, or fins.

## Interactions and persistence

- **Click recap** — clicking the **body** cycles a speech bubble through the
  name/days entry and recent session events (completed long turns, goal/plan
  milestones, tool failures with tool name and exit code, typing greetings).
  Other hit zones do something else: **tail** a heart plus an immediate
  patrol, **dorsal** an immediate patrol, **fin** a bubble.
- **Right-click menu** — chat with the pet (LLM, see below), inspect or
  delete remembered facts ("鲸鲸记得什么…"), rename the pet, resize it, toggle
  corner snapping, glide back to a corner, or hide it. The menu is
  keyboard-accessible (Enter/Space) and closes on outside click or Escape.
- **Corner snapping** — real drags (beyond the click threshold) glide to the
  nearest corner on release; toggle it from the menu.
- **Size** — the menu entry "调整大小…(100%)" opens an inline slider control
  (50%–150%, 5% steps) with −/+ step buttons and a reset to the authored size.
  Size is **render-only**: it scales the pet box around its own anchor, so the
  resting edge, patrol path, drag clamping and celebration ellipse are
  identical at every size — enlarging the whale never changes where it goes.
  The trade-off is deliberate: because the resting position is left untouched
  too, above 100% the tail can poke past the viewport edge while the pet rests
  against it, and below 100% the silhouette sits slightly inside the corner.
- **Drag dizziness** — a forceful drag (at least 420px of pointer travel at an
  average 550px/s) or a prolonged drag (at least 4.5s with 24px of travel)
  leaves the whale dizzy for about 4s. It rolls belly-up, weakly wobbles, and
  shows three stars orbiting an ellipse around its belly with front/back depth.
  Hover, click, context-menu, and further drag reactions stay locked until it
  recovers; holds without movement and cancelled gestures do not trigger it.
- **`Ctrl`/`Cmd` + `Alt` + `W`** — toggle the pet's visibility from anywhere;
  the shortcut works even when the pet is hidden.
- **Persistence** — the pet's name, position, size, hidden state and snap
  preference survive reloads through `localStorage` (guarded against private
  mode), and the recap tracks the days you have spent together.

## LLM chat and memory

The right-click **"和鲸鲸聊天…"** entry opens an inline input bubble next to
the pet (Enter to send, Esc/outside click to close; 200-character input, no
transcript UI), and the pet replies in its speech bubble. Replies stream in
when the host supports SSE (`Accept: text/event-stream`); the same bubble
grows as tokens arrive instead of popping a new one per delta. The bubble
carries a **model selector** and a **reasoning
effort selector**: the model list comes from the DSH LLM service
(`GET /api/whale-pet/models`), and models that expose multiple reasoning
levels (e.g. deepseek-reasoner low/high) show an effort dropdown. Choices
persist to `localStorage` (`dsh.whale-pet.chat-prefs.v1`) and ride along with
every request.

Architecture: the browser pet never talks to an upstream LLM directly — the
host-side entry of this package registers the `/api/whale-pet` prefix on the
web server, and the client calls the same-origin `POST /api/whale-pet/chat`
endpoint. The API key stays on the server.

**Backend selection**: when the dsh `ctx.llm` service is available the proxy
uses it (DSH-configured providers, credentials, retries and reasoning
efforts); otherwise it falls back to the direct OpenAI-compatible upstream,
whose key resolves per request:

1. plugin `config.apiKey` (patch entry `config:` field)
2. env `DSH_WHALE_API_KEY` (fallback `DEEPSEEK_API_KEY`)
3. the dsh credentials service (`DEEPSEEK_API_KEY`) — the pet works with the
   same key the agent already uses, zero extra configuration

Other configuration (direct mode only): `DSH_WHALE_API_BASE` (default
`https://api.deepseek.com`, OpenAI-compatible upstream origin) and
`DSH_WHALE_API_MODEL` (default `deepseek-chat`).

Memory: the pet keeps long-term facts about you plus a bounded recent
conversation under the `dsh.whale-pet.memory.v1` localStorage key (same guarded
storage channel as the rest of the pet state). The right-click **"鲸鲸记得什么…"**
panel lists those facts, lets you delete one, or type a new one. The chat box
itself stays history-free. Facts arrive three ways: the model may still emit a
`[记住] <fact>` line, the coordinator also extracts first-person statements
from the user ("我叫… / 我喜欢… / 记住:…"), and the panel can add a fact
directly. The 60-character cap applies only to the spoken bubble, not the
`[记住]` line. Marker lines are stripped before the bubble is shown.
While a request is in flight the pet holds the `thinking` mood (an external
override the session observer respects) and reacts with an error mood and sweat
drops if the proxy is unreachable or unconfigured (no key → HTTP 503).

### Session progress (read-only, probed on ask)

The pet can answer "进度如何了": **when asked**, the pet actively probes the
current progress and appends a compact **read-only snapshot** to its own
system prompt, so it truthfully reports long-task progress. The probe has
three layers — the live projection state (running tools, turn duration, node
count, goal/plan phase), a fine-grained summary from the host event log
(current step, latest activity like "运行 bash:npm test", latest result
summary), and the **jobs registry's real state** (running task labels,
elapsed time, output tail like "进度 45%"; served at
`GET /api/whale-pet/progress?session=<id>`). It only reads — it **never
writes to the DSH conversation**, so long chats are not disturbed.

While the agent is busy, a plain **click on the pet** bubbles a playful but
factual progress line ("正在鼓捣终端(bash),已经 3 分钟" / "正在深度思考…";
a running background job wins: "正在后台跑 npm run build(已 5 分钟)")
without typing.

The pet's own context stays **bounded**: up to 24 memory facts (80 chars
each) + the last 8 turns (240 chars each) + the progress block, worst case
≈ 4.4 KB (~1.3k tokens). When a turn overflows the 8-turn window, evicted
old turns are not dropped — they are **compacted** into a capped summary
(`summary`, 400 chars) so long conversations keep a coarse digest of what
was discussed.

### Task dispatch (subagent)

When the user asks the pet for something that needs **real execution**
(writing code, running commands, research, fixing bugs…), the pet does not
fake an answer: it replies with a `[TASK] <description>` marker. Only that
marker dispatches work — a casual reply that merely contains execution verbs
stays a direct answer. The client then calls `POST /api/whale-pet/task`, and
the host dispatches a **real subagent conversation** through `ctx.subagents`
(the same machinery as the agent's
`subagent` tool: its own session, tools and results). The child appears in
the DSH subagent view so the user can open it directly; when it finishes the
pet summarizes the outcome in its bubble (on 1-min timeout it reports the child
session id). The parent is the active agent (`currentInitiator`); when idle,
a fresh agent is created as the parent identity.

Debug: `GET /api/whale-pet/health` reports `{ ok, configured }`.

## Session integration

The plugin observes the current DSH session through `ctx.sessions` and drives
the pet's mood from the conversation snapshot and the `goal`/`plan`
projections. The bridge retries until the sessions service is available and
absorbs history-window lag after binding, so old errors never trigger a
reaction.

| Session state | Pet reaction |
|---|---|
| Assistant tokens or tool calls are running | `working`/`thinking`: faster swimming, input-area gaze, periodic bubbles |
| One turn runs longer than 20s | `focused`: a slight dive posture |
| Tool fails (non-zero exit code or error node) | `error`: pouring sweat drops, a trembling body, a pulsing red "!" mark and wide eyes for 3s |
| Long turn (≥15s), goal completion, or plan exit | `celebrating`: a 360° elliptical lap with continuous yaw and screen-space depth, while hearts stream every 650ms |
| You are composing a reply in the chat input | `listening`: gazes at the input area, shows a floating "?", and recaps "在呢,我听着~" on click |
| Session is blocked on you (`pendingInteraction`: approval / question / plan-review) | `awaiting`: same gaze and "?" as listening; click recaps "有个审批等你拍板" / "有个问题等你回答" / "有个计划等你过目". Wins over `working` because the turn often stays marked running. |
| Forceful or prolonged drag release | `dizzy`: freezes pointer interaction, rolls belly-up, weakly wobbles, and shows a depth-aware elliptical star orbit for about 4s |
| Hover or drag while sleeping | Wakes immediately and resets the idle clock |
| No activity for 60s | `sleeping`: closed eyes, slow breathing, z-z-z |
| First idle/sleep of a local calendar day | one unsolicited greeting ("今天也在~" / "又见面啦,第 N 天"); at most once a day |
| A compaction node lands after the bind settle window | one "记忆被压扁了一点,我还在~" bubble; never an LLM daemon |

Debug attributes on the pet element:

- `data-whale-activity` — current mood (`idle`, `thinking`, `working`, `focused`, `celebrating`, `error`, `dizzy`, `sleeping`, `listening`, `awaiting`)
- `data-whale-bridge` — session bridge state (`off`, `waiting`, `bound`)
- `data-whale-size` — current visible size as whole percent (`50`–`150`)

## Installation

### DeepSeek Harness Desktop (0.2.0-rc.2)

Desktop owns the reserved `desktop` profile, so install this bundle from the
application's Plugins page rather than the ordinary `dsh plugin` CLI. Build and
pack the plugin first:

```sh
npm install
npm run build
npm pack
```

In DeepSeek Harness Desktop, open the Plugins page, choose to install a bundle,
and paste the full path to the generated `.tgz` file. Confirm the install, then
restart Desktop if prompted. The package ships a bundle patch, so the manager
activates its Host and browser entries together. Do not use
`install-profile.mjs desktop` or run the regular CLI against that profile.

### Plug-and-play (prebuilt, recommended)

For a regular CLI/Web profile, the repository ships prebuilt runtime artifacts
in `lib/`. This legacy installer requires Node.js 22+ on the machine running
`dsh` and is not the supported path for the packaged Desktop profile.

1. Download the repository (GitHub ZIP or `git clone`).
2. Run the profile installer:

   ```sh
   node install-profile.mjs web
   ```

   This copies the package to `$DSH_HOME/profiles/web/plugins/ui-whale-pet`,
   adds it to the profile manifest and node_modules, and appends the
   `ui-whale-pet` Cordis row to `$DSH_HOME/profiles/web/cordis.patch.yml`.

   This helper is for Node-based CLI/Web profiles only; do not target Desktop.
3. Configure the chat proxy API key (see [LLM chat and memory](#llm-chat-and-memory))
   or the pet chat reports an unconfigured error.
4. Restart `dsh web` and hard-refresh the browser.

### Build from source (standalone)

The repository builds standalone (no pnpm workspace, no dsh checkout):

```sh
npm install            # dev toolchain: typescript, esbuild, vitest, three…
npm run build          # tsc declarations + esbuild host/client bundles → lib/
npm test               # vitest suite (195 tests)
node install-profile.mjs web
```

The build is self-contained: host bundles inline everything (only type-only
imports), so the cordis loader needs no node_modules next to the plugin, and
the client bundle keeps the DSH `__ModuleLoader__.load` browser format with
`react`/`react/jsx-runtime` external.

## Architecture

- `src/client/activity.ts` — pure mood/effect vocabulary and the view snapshot type.
- `src/client/motion.ts` — pure frame-rate-independent screen-space motion, including the celebration loop path and corner snapping.
- `src/client/persistence.ts` — guarded `localStorage` state (name, position, size, hidden, snap preference, first-run date).
- `src/client/runtime/scheduler.ts` — the single `requestAnimationFrame` clock.
- `src/client/runtime/whale-pet-controller.ts` — owns the DOM listeners, scheduler, and per-frame rendering; composes the Three.js scene from `src/client/whale`.
- `src/client/runtime/whale-pet-service.ts` — observable runtime service (`ctx.whalePet`) with activity, transient effects, recap history and persisted state.
- `src/client/runtime/session-observer.ts` — subscribes to the current session snapshot (with a low-frequency polling fallback) and maps session state to moods/effects and user typing.
- `src/client/whale/config.ts` — shared geometry/animation constants and SVG contours.
- `src/client/whale/geometry.ts` — pure SVG/contour helpers and BufferGeometry builders.
- `src/client/whale/materials.ts` — material factories, including the blue/white body-mask shader.
- `src/client/whale/animation.ts` — frame pose animation (swim, tail, fins, eyes, float, error/sleep reactions).
- `src/client/whale/scene.ts` — `createWhaleScene` factory composing config, geometry, materials and animation into a `WhaleScene` handle.
- `src/client/WhalePet.tsx` — thin view consuming the service snapshot through `useSyncExternalStore`; owns the DOM focus listeners for typing detection, the context menu and the chat bubble (model/effort selectors).
- `src/client/memory.ts` — long-term memory store (facts + recent turns) with the `[记住]` extraction protocol, persisted through the guarded storage channel.
- `src/client/llm.ts` — browser transport for the same-origin chat proxy (`/api/whale-pet/chat`, `/api/whale-pet/models`), injectable for headless tests.
- `src/client/runtime/whale-pet-chat.ts` — chat coordinator: thinking override, reply bubble, memory persistence, error reactions, chat preferences.
- `src/chat-proxy.ts` — pure host-side proxy logic: backend interface, direct upstream forwarding, HTTP handler for `/health`, `/models`, `/chat`.
- `src/llm-backend.ts` — dsh-llm backend: model catalog with reasoning efforts, streaming completion via `ctx.llm`.
- `src/index.ts` — host entry: mounts the `/api/whale-pet` prefix on `ctx.webServer`, picks the backend (dsh llm service first, direct upstream fallback).

## Model source and acknowledgements

The original whale model and visual reference come from the Bilibili video [BV17Buf69EVV](https://www.bilibili.com/video/BV17Buf69EVV/). Thanks to the original creator for publishing the model demonstration and design reference.

This plugin ports and packages the model for the DeepSeek Harness interaction and lifecycle system. This acknowledgement does not grant additional rights to the original model or video; downstream users remain responsible for complying with the original creator's terms.

## Lifecycle

The overlay registration, DOM listeners, animation frame, WebGL renderer, geometries, materials, and textures all unwind with the owning Cordis fiber. The host chat-proxy route unwinds with its fiber too; the API key never enters the browser.

Install

dsh plugin --profile web add github:rongzi5/dsh-whale-pet

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