@nkj/dsh-ui-pet
A canvas chibi cat pet for the DeepSeek Harness web GUI. It is anchored to the bottom-right viewport corner (the body does not chase the pointer — the head turns toward its gaze and the pupils track the pointer) and is styled as a background decoration in the shell.overlay layer: position:fixed (no layout impact), fully click-through (pointer-events:none), a low z-index (below other overlay entries and dialogs), and < 1 opacity so the page content shows through.
The cat is drawn procedurally on a DPR-aware canvas (no asset required) and reacts to activity, detected purely through DOM observation of stable data-* hooks (so it stays decoupled from the host's internal session/streaming state):
- Typing — while you type in the composer, the cat leans toward the input, looks at it, and shows an animated "typing dots" bubble plus a pulsing activity dot; its tail sways faster. It returns to idle after a short pause.
- Streaming / working — while the assistant streams a response, the cat tilts into a contemplative pose, looks up toward the conversation flow, and shows the same busy bubble + dot.
- Idle — the cat breathes, blinks, its eyes follow the pointer, its head subtly turns toward the gaze, and its tail sways gently.
How it plugs in
The package is a dsh bundle (dsh.bundle → cordis.patch.yml) that is also a client plugin (dsh.client). Installing it into a profile with dsh plugin --profile web add <pkg> auto-registers it (the CLI reconciles installed dependencies that declare dsh.bundle into dsh.profile.bundles), then the client module system serves /plugins/@nkj/dsh-ui-pet/client.js on the next boot. No manual cordis.yml edit.
Built-in dev loop
# once: install build deps
pnpm install
# build both artifacts (lib/index.js host half + lib/client.js browser bundle)
pnpm run build
# iterate: rebuild the browser bundle on source change (the dsh host stat-polls
# the served bundle and broadcasts a reload, so the browser refreshes on write)
pnpm exec tsdown --watch
Verify locally (before publishing)
dsh plugin --profile web add /absolute/path/to/dsh-ui-pet
pnpm dsh web # bind http://127.0.0.1:3080
In the browser at http://127.0.0.1:3080, confirm:
- In the tree —
dsh --profile web --dump-configcontains theui-petrow. - Bundle served — DevTools → Network shows
/plugins/@nkj/dsh-ui-pet/client.js(200); orcurl http://127.0.0.1:3080/plugins/@nkj/dsh-ui-pet/client.js. - Rendered, and visible — DevTools → Elements: the
<canvas>is inside theshell.overlaylayer,position:fixed,pointer-events:none, low z-index, andopacity < 1. It is anchored to the bottom-right viewport corner. - Behavior — the body stays put while the pointer moves (only the eyes follow it). Type in the composer: the pet leans toward the input and shows the busy bubble/dot. Send a message: while the assistant streams, the pet tilts into a working pose. The overlay never blocks clicks (the layer and canvas are
pointer-events:none).
While SPRITE is empty in src/client/sprite.ts, the pet draws a colorful placeholder blob, so step 4 is verifiable without any asset.
Publish
pnpm run build
npm pack # confirm lib/, cordis.patch.yml, and src are in the tarball
npm publish --access public
After publishing, a user installs it with:
dsh plugin --profile web add @nkj/dsh-ui-pet
Customizing the pet
- The cat look is parameterised in
src/client/sprite.ts: the fur/ear/nose/chest palette (FUR_TOP,FUR_BOTTOM,FUR_DARK,INNER_EAR,NOSE_PINK,CHEST_WHITE,WHISKER) and the head/body proportions (BODY_RX,BODY_RY,HEAD_RX,HEAD_RY,HEAD_OFFSET_Y,SIZE). - Animations live in
src/client/Pet.tsx: the idle bob, head-turn toward the gaze, per-state body lean, pupil track, tail sway, the "typing dots" speech bubble, and the pulsing activity dot. - Make a sprite sheet to replace the vector cat: set
SPRITE(asset path or URL) and tuneFRAME_COLS/FRAME_ROWS/FRAME_W/FRAME_H/FPSinsrc/client/sprite.ts, and setVECTOR=false. - Busy-state detection tunables live in
src/client/sprite.ts:TYPING_IDLE_MS(hold after the last composer keystroke) andWORKING_IDLE_MS(hold after the last conversation-flow mutation). - DOM hooks are the only host coupling:
[data-composer-card](composer, for typing) and[data-chat-flow](conversation stream, for working). Both are optional — if absent, the pet simply idles.
Model Experience
None. The pet manages browser-only presentation; nothing reaches a model request.
KV Cache effect
None; this package neither assembles nor sends a provider request.
Target version
Runtime-compatible with dsh 0.1.1-rc.2 (the web profile). The host dsh install supplies the runtime react, @deepseek-ai/cordis, and the slots service.
The published package is intentionally build-dependency-free: the source uses no @deepseek-ai/* imports (the DSH-specific types are left structural/untyped), so the standalone build needs only react + tsdown + typescript. The DSH integration is by convention: apply receives a ctx with a slots service, and the bundle is the standard window.__ModuleLoader__.load closure factory.
Known Limitations and Deferred Work
- The pet lives in the
shell.overlayfloating layer, so it paints above the app content (it is deliberately semi-transparent and small so it does not obscure it). Rendering it genuinely behind the content is impossible from a plugin without DSH source changes: the conversation/details/sidebar surfaces are opaque and would fully cover a behind-content canvas. Anchoring it to a corner (rather than chasing the pointer) is what keeps it clear of the text you are reading. - Busy detection is DOM-based and therefore heuristic. "Working" is inferred from conversation-flow DOM mutations, so brief non-streaming churn (e.g., a tool disclosure, or history loading on open) can also trigger the working pose for the debounce window. Distinguishing streaming specifically would require reading the host's
running/stream state through a session hook. - The
ctx/ slot types are structural and untyped to keep the standalone build self-contained. When developing inside the dsh monorepo, restore the realClientContext/PropsRuntime<'shell.overlay'>types for full checking. - The bundle format (closure factory + React externalization) is reproduced from the dsh
clientBundlepreset; iterate against a livedsh webto confirm exact output.
No comments yet. Be the first to write one.