dsh-pet

August 26, 2026 · View on GitHub

English | 中文

An independent desktop-pet plugin for DeepSeek Harness (dsh, a Cordis-based agent shell). The pet lives in the corner of the Web UI and grows alongside your agent: every assistant/message sums the four token buckets (input/cacheRead/cacheWrite/output) into XP, levels unlock a title ladder, and the agent's live status (thinking / calling-tool / tool-error / answered …) drives the pet's current animation via a millisecond-latency SSE push. Name the pet, swap its spritesheet, scale and drag it anywhere; the hover panel is minimalist monochrome.

The pet floating over the DeepSeek Web UI, growing with token usage

The full local dev→deploy loop (two-repo sync, packing, profile install, verification, troubleshooting) lives in DEVELOPMENT.md.

Spritesheet downloads are available at codex-pets.net (under the pixel tag).

Features

  • Grows with token usage — consumption is XP: level, title ladder (a monochrome brightness ramp encodes prestige), and an XP progress bar
  • Live status sync — the host projects session events onto coarse statuses (thinking / calling-tool / tool-ok / tool-error / answered / turn-end) and pushes them over the /pet/events SSE stream, so the pet switches animations in step with the page's streaming output (a 3s poll remains as fallback)
  • Status → action mapping — pick which spritesheet row plays for each status; saved locally
  • Custom spritesheets — upload a single image plus a JSON layout config; the host validates cell/frame geometry and rebuilds the manifest
  • Rename / resize / drag — pet name, size (50%–200%), and position all persist locally
  • Consumption cannot be wiped — tokens/XP/level are recomputed from durable session logs on every boot (ground truth); /pet/reset accepts the sprite scope only
  • Minimalist monochrome UI — pure black/white card, serif masthead, mono micro-labels, zero radius, no shadows
  • Intranet P2P gomoku (五子棋) — hovering the pet spins out an orbit dock with a gear (settings) and a gomoku glyph; the board opens a 15×15 two-player game over a WebRTC DataChannel with no signaling server (two peers on the same LAN connect by exchanging two short base64 codes by hand)

Install (one command)

npx @deepseek-ai/dsh plugin --profile web add @dsh-pet/pet
# then start the Web UI (the same command as the official quickstart)
npx @deepseek-ai/dsh web

Everything ships in one dual-face package: @dsh-pet/pet declares dsh.bundle (its cordis.patch.yml inserts the pet row into the composed tree, so installing it activates the layer automatically — no hand-edited yml), mounts the host half (the /pet/* routes plus the SSE stream), and declares dsh.client for the browser dock, auto-discovered by dsh-client-modules and served at /plugins/@dsh-pet/pet/client.js. Removal is the same command with remove.

Upgrading

Re-run the add command — bare names resolve to the latest published versions and rewrite the profile's pins (reconcile re-checks the layer list against the installed state):

npx @deepseek-ai/dsh plugin --profile web add @dsh-pet/pet

Then restart dsh web and hard-refresh the browser tab (the boot graph is injected per page load).

  • From the three-package era (≤ 0.2.1: @dsh-pet/bundle + @dsh-pet/host + @dsh-pet/ui-pet, or 0.1.0's @dsh-pet/client) — drop the old set first, then add the one package: npx @deepseek-ai/dsh plugin --profile web remove @dsh-pet/bundle @dsh-pet/host @dsh-pet/ui-pet @dsh-pet/client (unknown names are fine to include), then the add above.
  • 0.1.1+ requires the rc.2 harness line — upgrade @deepseek-ai/dsh together (npx @deepseek-ai/dsh web already runs the latest).
  • Pet data survives upgrades: XP/level/name live in ~/.dsh/storages plus the session logs (recomputed from ground truth on boot); the browser-side zoom/position/status-map settings are per-origin localStorage. A saved zoom is not re-defaulted by an upgrade.

Repository layout

This repo is decoupled from the upstream monorepo: it never depends on the deepseek-harness source tree, only on its published npm packages (pinned exact to the 0.1.1-rc.2 line; the package ships at 0.3.0; framework on @deepseek-ai/cordis@4.0.1). Upstream updates never change this repo's behavior until you re-pin. Since 0.3.0 everything ships as the single package @dsh-pet/pet (the 0.1.x line was split into host/ui-pet/bundle; the original @dsh-pet/client name was unusable under the rc.2 harness — see DEVELOPMENT.md).

packagepathrole
@dsh-pet/petpackages/pet/The whole pet, dual-face: host half (src/ — economy, storage domain, spritesheet validation, /pet/* routes, /pet/events SSE), browser half (src/client/ — the bottom-right dock, a dsh.client module), and the self-installing cordis.patch.yml (dsh.bundle)
dsh-pet/
├─ package.json / pnpm-workspace.yaml / vitest.config.ts / tsconfig.base*.json
├─ config/tsdown-client.ts        # standalone client-bundle renderer (module-loader closure, CSS inline)
└─ packages/pet/                  # the single dual-face package (@dsh-pet/pet)
   ├─ package.json                # dsh.bundle + dsh.client declarations
   ├─ cordis.patch.yml            # inserts the `pet` row — self-installing
   ├─ src/                        # host half: {index,spec,types,economy,catalog,usage,default-sprite,spritesheet,invariant}.ts
   ├─ src/client/                 # browser half: {PetCompanion,PetCompanion.module.css,agent-status,titles,locales,pet-api,slots,spritesheet,auto-sprite-config,Gomoku,gomoku-net,OrbitDock,index}
   ├─ assets/default-sprite.webp
   └─ tests/                      # host + client specs (91 tests)

Commands

pnpm install        # resolves all @deepseek-ai/* deps from npm (pinned)
pnpm build          # tsc host → lib/types · tsc client · tsdown client bundle → lib/client.js
pnpm typecheck      # tsc --noEmit, both packages
pnpm test           # vitest: host economy/usage/spritesheet/catalog + jsdom pet spec (85 tests)

How the client half is served

The browser half is a standalone client module (dsh.client declaration + lib/client.js). At runtime dsh-client-modules serves it on demand at /plugins/<package-name>/client.js; the SPA lazy-fetches it, so installing a new client package does not require recompiling the web bundle. Published client packages ship only lib/; ./src/* exports are source-launch placeholders and are not loadable from a published tarball — deep …/src/… imports are removed here.

Gomoku — intranet P2P

A 15×15 gomoku board hides behind the pet's hover orbit dock. It is peer-to-peer over a WebRTC DataChannel with no signaling server — two browsers on the same intranet connect by exchanging two short codes by hand:

  1. HostGenerate invite code → copies the invite code (base64 of the offer + host ICE candidates) to the guest.
  2. GuestJoin → pastes the invite code → copies the answer code (base64 of the answer + guest ICE candidates) back to the host.
  3. Host → pastes the answer code → Connect → the DataChannel opens.

iceServers: [] gathers host candidates only (no STUN/TURN, no external dependency), so this works on an isolated intranet. Modern browsers obfuscate private IPs as mDNS *.local hostnames; the same machine in two tabs is the most reliable test, or disable chrome://flags/#enable-webrtc-hide-local-ips-with-mdns for raw-IP candidates. Moves flow as {t:'move',x,y,c} over the channel; each side keeps its own board and judges five-in-a-row locally, with the winning stones pulsing until the host rematches. Closing the panel only hides it (the peer connection and board state persist); the Leave room button tears the session down. Full details live in packages/pet/README.md.

Publishing (maintainer-run — needs npm credentials)

All three packages publish to the @dsh-pet scope. Use pnpm publish (not npm publish): pnpm normalizes the intra-workspace workspace: specs (client → host, and the bundle's peers) to real versions.

./scripts/publish.sh            # build+test gate, pack, content assertions, publish

Once on the registry, the local install command switches from the file: tarball to the bare package name (no pnpm file:-cache quirks).

Live boot

The end-to-end run (dsh web → the floating pet appears bottom-right → send a message → XP grows and titles change → the pet switches animations in step with the streaming output → upload a spritesheet / rename / resize) requires a running harness and an API key. Everything short of the live provider call is exercised by the test suite; the full local-tarball→profile install chain has been verified live (see the curl checklist in DEVELOPMENT.md §3.7).

Version policy

Dependencies are pinned in each package's package.json. To absorb a harness release: bump the @deepseek-ai/dsh-* pins and the framework lines together, re-run pnpm install && pnpm build && pnpm test, and re-verify that the client bundle's external platform-module set (in config/tsdown-client.ts) still matches what the new harness serves.