openpetswithchatandmcp/docs/mapping.md
2026-05-16 13:00:43 +02:00

12 KiB
Raw Permalink Blame History

OpenPets reaction, speech, and Claude hook mapping

This document explains how OpenPets decides which pet reaction is shown, when the pet says something, and how Claude Code hooks and OpenCode plugin events map to reactions/messages.

Important terms

  • Reaction: one of the allowed OpenPets reaction names, for example thinking, success, or error.
  • Speech/message: a short text bubble sent with openpets_say or generated by a Claude hook.
  • Hook speech: automatic short speech generated by Claude Code hook events.
  • Target pet: the pet that receives the reaction/message. This is either the desktop default pet or a project/agent-selected pet through a lease.

Allowed reactions

OpenPets currently accepts these reactions:

Reaction Intended meaning
idle Neutral / no special task state.
thinking The agent is thinking or starting to process a prompt.
working Generic tool/work activity.
editing The agent is editing or writing files.
running The agent is running a command that is not classified as tests.
testing The agent is running tests.
waiting The agent is waiting for permission or another blocking state.
waving The pet waves for notifications/attention.
success The agent completed successfully.
error The agent failed or hit a problem.
celebrating Positive manual reaction; not currently emitted by Claude hooks.

Source of truth: packages/client/src/protocol.ts and apps/desktop/src/local-ipc-protocol.ts.

What a reaction visually does today

Reactions drive the pet spritesheet animation and can also show temporary bubble text above the pet. For example, thinking uses the review/thinking animation row and, when sent as a reaction-only event, displays a short randomized status line such as Thinking it through or Checking the clues.

The defaults below are defined in apps/desktop/src/reaction-animation-mapping.ts. Users can override reaction-to-animation defaults in Settings → Reaction animations. Overrides apply globally to the default pet and explicit/agent pet windows. Drag-only rows (running-left, running-right) are never user-selectable because they are controlled by pet movement.

The universal Codex/OpenPets spritesheet has these animation rows:

Row Spritesheet state Trigger/reaction mapping
0 idle No active reaction, message-only speech, or idle. Slow 5500ms loop.
1 running-right Drag right via motion state run-right.
2 running-left Drag left via motion state run-left.
3 waving waving, Claude Notification.
4 jumping success, celebrating.
5 failed error.
6 waiting waiting, testing, PermissionRequest.
7 running working, editing, running.
8 review thinking.

Drag motion overrides reaction animation while the pet is being moved. When dragging stops, the sprite returns to the active reaction animation until the transient display clears, then returns to idle.

One-shot feedback animations do not loop for the full 4-second bubble lifetime:

Spritesheet state Iterations
waving 2 loops
jumping 2 loops
failed 2 loops

Long-running states (idle, waiting, running, review, and drag run states) continue looping while active. If a user override maps a reaction to a different animation state, the selected state's one-shot or looping behavior applies.

Bubble priority

When a transient display has both a message and a reaction, the message wins.

Input Bubble text shown
Reaction only: openpets_react({ reaction: "testing" }) A stable randomized status line from the testing message pool, for example Running the checks.
Message only: openpets_say({ message: "Done" }) Done
Message + reaction: openpets_say({ message: "Done", reaction: "success" }) Done

The reaction is still sent with the message, but the visible bubble text is the message.

Source of truth: apps/desktop/src/pet-window.ts (createBubbleMarkup).

How long bubbles stay visible

Reaction/message bubbles are transient and clear after a length-aware duration:

Bubble type Duration
Most reactions/messages At least 4 seconds.
success / error reactions At least 5 seconds.
Longer explicit messages Extended by message length, capped at 12 seconds.

Source of truth: apps/desktop/src/pet-window.ts (getTransientDisplayDurationMs).

Claude hook event mapping

When OpenPets Claude hooks are installed, these Claude Code hook events are registered:

UserPromptSubmit
PreToolUse
PermissionRequest
Notification
Stop
StopFailure

Source of truth: packages/claude/src/hook-settings.ts.

Hook event to reaction/message table

Claude hook event Condition Reaction sent Does it speak? Possible speech text
UserPromptSubmit Any user prompt submitted. thinking Yes, throttled. Thinking it through, Let me check, On it, Working it out
PreToolUse Tool is Edit, Write, or MultiEdit. editing No.
PreToolUse Tool is Bash and command looks like a test command. testing No.
PreToolUse Tool is Bash and command does not look like a test command. running No.
PreToolUse Any other tool. working No.
PermissionRequest Claude asks for permission/approval. waiting Yes, shorter cooldown. Approval needed
Notification Claude sends a notification. waving No.
Stop Claude completes normally. success Yes, throttled. Done, That worked, All set, Nice, finished
StopFailure Claude run fails/stops with failure. error Yes, throttled. Something failed, Needs another look, Hit a snag, Not quite there
Unknown event Hook event name exists but is not recognized. None. No.
Invalid/missing event Payload is invalid or has no event name. None. No.

Source of truth: packages/claude/src/hooks.ts and packages/claude/src/hook-messages.ts.

Bash test detection

For PreToolUse with tool_name: "Bash", OpenPets classifies the command as testing if the command contains one of these patterns:

test
vitest
jest
pytest
npm test
pnpm test
yarn test
cargo test
go test

Otherwise Bash maps to running.

Hook speech throttling

Hooks do not speak on every matching event. Speech is throttled so the pet does not spam bubbles.

Speech category Events that use it Cooldown
thinking UserPromptSubmit 20 seconds
success Stop 20 seconds
error StopFailure 20 seconds
permission PermissionRequest 3 seconds

If speech is throttled, OpenPets still sends the reaction with pet.react. Example: if Stop happens twice within 20 seconds, the second one sends success as a reaction only instead of saying another success phrase.

Source of truth: packages/claude/src/hooks.ts (speechCooldownMs, permissionCooldownMs, shouldSendSpeech).

OpenCode plugin event mapping

When OpenPets OpenCode setup is installed, OpenCode loads the @open-pets/opencode plugin. Plugin hooks return immediately and schedule OpenPets IPC calls in the background so OpenCode is not blocked by pet activity.

OpenCode hook/event Condition Reaction sent Does it speak?
chat.message User/chat activity. thinking Yes, throttled.
tool.execute.before Edit/write/patch-like tool name. editing No.
tool.execute.before Shell/bash-like tool name and test-like command category. testing No.
tool.execute.before Shell/bash-like tool name, not test-like. running No.
tool.execute.before Other tools. working No.
event Bus event permission.asked. waiting Yes, short approval-needed speech.
Internal/self OpenPets tools OpenPets MCP/status/say/react calls. None. No.

OpenCode speech uses the same safe shared message pools as Claude hooks. It never includes prompt text, commands, output, code, logs, paths, URLs, or secrets.

Source of truth: packages/opencode/src/opencode-plugin-runtime.ts and packages/agent-events/src/index.ts.

Hook speech safety rules

Hook-generated speech must be safe before it is sent. It must be:

  • 1140 characters.
  • Single-line.
  • Not code-like.
  • Not a URL.
  • Not a path.
  • Not secret-like.

If a generated speech message fails validation, the hook ignores the error and does not break Claude Code.

Source of truth: packages/claude/src/hooks.ts, packages/opencode/src/opencode-plugin-runtime.ts, and packages/agent-events/src/index.ts.

MCP tool mapping

The public MCP tools are intentionally small:

MCP tool Input Effect
openpets_status none Checks whether OpenPets is running and which pet is targeted. Does not change pet state.
openpets_react { reaction } Drives the reaction animation and may show a short randomized temporary bubble from that reaction's message pool.
openpets_say { message, reaction? } Sends a message bubble, optionally with a reaction. The message is what appears in the bubble.

MCP message validation

openpets_say messages must be:

  • 1140 characters after trimming.
  • Single-line.
  • Not code-like.
  • Not a URL.
  • Not path-like.
  • Not secret-like.

Source of truth: packages/mcp/src/tools.ts and apps/desktop/src/local-ipc-protocol.ts.

Target pet routing

OpenPets decides which pet gets the reaction/message as follows:

Situation Target
No selected/configured pet. Desktop default pet.
Selected/configured pet is the built-in pet or current default pet. Desktop default pet.
Selected/configured pet is installed and not broken. Explicit agent pet window for that pet.
Selected/configured pet is invalid, missing, or broken. Falls back to desktop default pet.

Claude hooks, OpenCode plugin events, and MCP use a short-lived lease when a project/configured pet is specified. That lease routes say/react to the explicit pet window.

Source of truth: apps/desktop/src/local-ipc.ts (resolveLeaseTarget), packages/claude/src/hooks.ts (acquireHookLease), and packages/opencode/src/opencode-plugin-runtime.ts.

Paused behavior

If the default pet is paused:

  • Default-pet reactions/messages are not shown and return reason: "paused".
  • Explicit agent pet reactions/messages also check the default paused state and are not shown when paused.

Source of truth: apps/desktop/src/default-pet-controller.ts and apps/desktop/src/local-ipc.ts.

Visibility behavior

Default pet:

  • A reaction/message refreshes the default pet content.
  • The default pet is only shown for external events if it is already visible or openDefaultPetOnLaunch is enabled.

Explicit agent pet:

  • A reaction/message opens/shows that agent pet window.
  • The agent pet window is closed when its explicit lease expires and no explicit lease remains.

Source of truth: apps/desktop/src/default-pet-controller.ts, apps/desktop/src/agent-pet-controller.ts, and apps/desktop/src/local-ipc.ts.

Current product implications

  1. Hook emotions now change sprite animation rows; bubbles are still used for short visible text.
  2. Only some hooks/events make the pet speak. Claude prompt/permission/stop events and OpenCode chat/permission events can produce speech. Tool-use hooks only react.
  3. Speech is intentionally short and safe. Hooks/plugins never display prompts, command output, code, logs, paths, URLs, or secrets.
  4. Messages override reaction text visually. If a message is present, the bubble shows the message, not a randomized reaction line, while the reaction still controls animation.
  5. celebrating exists as an allowed manual reaction but is not emitted by Claude hooks today.