Skip to content

Architecture: PlayPets app ​

Scope ​

This describes the current structure of one PlayPets app install. It covers the yard window, the desktop overlay windows, the Rust backend, the files the app reads and writes, and the outside touchpoints (AI, coding agents, GitHub Releases). The commands and HTTP spec of the agent integration are in Architecture: coding agent integration, and the pet file format is in the Pet file format standard. Building and distribution are covered by the release procedure.

Diagram ​

A container-level diagram. It shows the three parts inside the app and the touchpoints outside it.

Components ​

ComponentLocationResponsibility
Yard windowsrc/index.html, src/app.js, src/yard/, src/panel/, src/core/, src/styles/Draws the scenery and friends, and opens the side panel (Friend and Invite, beside the yard); Settings opens as a sheet over the yard. ES modules the browser reads with no build tool
Yard contentsrc/content/catalog.json, src/content/yard.json, src/content/events.json, src/content/worlds.json, src/art/What the yard is made of, apart from the code: every kind (fish, bugs, flowers, gifts, the garden's stages), the scenery's composition (curves, field, lake, layers), the seasonal events, and the drawings. Loaded at start by src/yard/content.js; the backend builds the catalogue in (pets/content.rs). How to add, design
Yard lifesrc/yard/The painted backdrop, painted only when the light, the season or the size changes, layer by layer as the composition lists them (brush.js, palette.js, layout.js, trees.js, lake.js, clouds.js, backdrop.js with its painters, easel.js; design); the lake's shape is lakeShape in layout.js, which the painter and fishing (catch.js) share. Drawings (art.js) and gift effects (effects.js), the field and its sizes (field.js), the scene now (world.js), what friends say on their own (remarks.js), props on the field (props.js), the weather canvas (scenery.js), the scene and the clock (sky.js, scene.js). Friends as they stand (views.js: each friend is a body among the garden's plants and gifts, ordered by depth, and a label (bubble, name tag, agent dot) in a layer above everything on the field) and one frame of their life (motion.js). Trips and tasks (trips.js: sendTo/release for a friend's trip, setDoing for what it does where it stands, isFree, nearestFree, rest; each belongs to whoever started it, so anything that takes the friend ends them). The line above the yard (hint.js; which line is the pure hintKey and busyLine), the first screen of an empty yard (welcome.js), seasonal events (events.js holds their rules, festival.js shows them), treats and games (care.js, play.js, games.js, wander.js), the garden and gifts (garden.js, growth.js, decor.js), fishing and bugs (catch.js, catalog.js, creatures.js, card.js), closeness (bond.js) and the yard's sounds (sound.js, soundmix.js, soundscape.js; design). app.js runs the frame loop and calls each feature's tick in a fixed order (YARD_LIFE between games), and sends each backend event to its handler from one table (ON_EVENT); keyboard shortcuts are one table in core/keys.js, the same keys as the macOS menu bar (menu.rs; keys.test.mjs keeps them alike); things that come now and then use cadence, and things that follow the clock use everyMinute, both from core/ticker.js. The limits the page shows before the backend's state arrives are in core/limits.js, the same values as pets::limits()
Desktop windowsrc/overlay.html, src/overlay/One transparent window per monitor. Draws friends where the backend places them and reports presses and drags
Commandssrc-tauri/src/commands.rsThe single command the page calls (call) and the COMMANDS table: one row per name, saying which windows may ask for it and what it does. The page's bridge, the access check and the dispatch all come from that table (how to add one)
App statesrc-tauri/src/app.rs, with loading in app/load.rs, the person's switches in app/prefs.rs and sprites in app/sprite.rsAppData (friends, pet list, AI connections, the switches in Prefs (desktop, roaming, language, chatter, sound, the mini window's place: one field, one line to read it back and one to write it), the conversations and the answers on their way in chat::Talks, status, the garden in pets/garden.rs, the album in pets/album.rs, closeness in pets/bond.rs), loading and saving, announcing changes to every window (a command that changes the state goes through commit, which saves and announces once; a change made in place uses save_and_show; a change that could not be saved still stands and is shown), serving sprites. The state is taken with app::lock and every other mutex with app::hold; both take a poisoned lock as it is (backend locks). Saved friends whose pet file was not found at this start are parked (AppData.parked_pets): not shown, written back as they were with their closeness, so they come back once the file does; connections this version cannot read are parked the same way (parked_providers). Days the page names are checked against the latest date it is anywhere on Earth (pets::latest_day, has_come), and a day saved from a clock set ahead is forgotten at start (forget_days_after), so it cannot hold back the days after. state.json carries a format number (schema); when an older version opens a file from a newer one, it keeps a copy and preserves unknown keys and connections as they are (a carried key never replaces one this version writes)
Chatsrc-tauri/src/chat/Chat with one friend or several together. Keeps Claude CLI sessions alive and runs the Codex CLI (cli/: the session manager in mod.rs, starting a CLI in launch.rs, each CLI in claude.rs and codex.rs; a stopped CLI ends with every process it started, as a process group on Unix and a job object on Windows), HTTP AI, prompts. Everything that differs per AI kind (request and response shape, key rules, default address) is one table in kind.rs
Petssrc-tauri/src/pets/Unpacks the starter pets bundled with the app, reads and checks pet folders (in one place, catalog.rs), imports from community pages (importer/: trusted adapters in sites.rs, ordered discovery links in src/content/communities.json), creates and edits friends. The garden and its gifts (garden.rs), the album (album.rs) and closeness (bond.rs) keep their rules here, so the page cannot hand out seeds, catches or points of its own
Agentsrc-tauri/src/agent/The CLI mode of the same executable (hooks, MCP, status commands) and the local HTTP bridge inside the app. The agent list is one table in kinds.rs; the settings snippets to paste are in snippets.rs
Desktopsrc-tauri/src/desktop/Opens and closes the overlay windows, computes friends' strolls and drags, and makes a window take clicks only while the cursor is over a friend
Windows, tray, menus, OS openwindow.rs, tray.rs, menu.rs, system.rsThe main window and mini mode (whose place is kept in state.json as miniRect), the tray menu, built again from the state on every change (check-marked switches that appear only when they apply, a mark on the icon when a coding agent waits) and the macOS menu bar (one handler, menu::act, for both), opening folders and addresses through the OS
Stringssrc-tauri/src/i18n/<language>.rs, src/core/strings/<language>.jsPer-language sentence tables. Registered in i18n/mod.rs and core/strings.js
Import linkssrc-tauri/src/links.rsTakes playpets://import?url=…, imports the pet through the same path as a pasted address, and opens the Invite tab
Saved filessrc-tauri/src/store.rsReads and writes the JSON files in the data folder atomically. A file that cannot be parsed is kept as a timestamped copy beside it (state.json.corrupt-…) so nothing is lost
Auto-updatesrc-tauri/src/update.rsChecks latest.json on the latest release, renames the tray's update item once a version is found, reports download, signature-check and install stages (update-status), then restarts

The main page keeps an available update before the place selector in the yard dock. Installation is started from its dialog. Before handing control to the platform installer, the page records the source and target version in local storage (update-restart). After bootstrap, it consumes that record and shows one timed notice only when the running version matches the target. A failed installation clears the record; an ordinary restart never claims an update succeeded.

Places, artwork and friend identity ​

src/content/worlds.json defines four built-in places: Hill garden (woodland), Stone courtyard (courtyard), Lakeside cottage (lakeside) and Cozy living room (studio). Each describes its activity capabilities, floor and pond geometry, lighting images and window panes. yard/environment.js paints the accepted raster backdrop; easel.js retains the procedural loading/error fallback. weatherPanes clips indoor rain, snow and overcast shading to the authored windows. Outdoor places support fishing and bugs; the living room keeps friends, garden, gifts and games without those outdoor activities. Changing places ends active trips and games rather than transferring their old geometry. Imported background packages use the same capability and geometry contract.

yard/scene.js follows the local clock for time of day. Automatic weather is a simulated choice per 20-minute block, with snow only in winter; it does not fetch local forecasts or geolocation. The selected season follows the local calendar and an approximate hemisphere inferred from the system time zone, or the person's override. The yard and overlay share that choice through local storage. Festivals retain their calendar dates even when the visible season is overridden.

The three bundled identities are Daisy, Clover and Pip. Retired app-owned Aria appearances map to Daisy, Noah appearances to Pip, and the original cats to Clover before restoration. Friend IDs, names, settings and closeness remain unchanged. Imported pets are not matched by this path-based migration. Old unpacked directories stay on disk but are excluded from the built-in catalog. Optional baseline portraitPath, seasonal sheets and the legacy alternate-appearance fields are checked by the catalog and served through the same sprite protocol. A selected appearance changes drawing material rather than relationship or AI connection. The full contract is in Pet file format.

Data flow ​

What each arrow in the diagram actually carries, path by path.

  1. Page commands: When the page calls something like window.playPets.addPet(id), Tauri passes it to commands::call. call checks that the calling window may use that command (the overlay may use only five), and handles it on a worker thread. The handler changes AppData inside the app state lock and saves to state.json. The result comes back as {ok, data} or {ok: false, error}, and the error sentence is already in the current language.
  2. State notifications: When state changes, app::changed builds the full state for the page and puts it into every window as __pp('change', state). The page receives it and redraws. One-off happenings (speech bubbles, reactions, chat arrivals, window hidden, new version) are sent separately as __pp('event', …).
  3. Sprites: The backend takes the page's image address pet-asset://sprite/<pet id> (on Windows, http://pet-asset.localhost/sprite/<pet id>), reads the file on a worker thread and returns it. The page reads pixels to measure the empty rows at the top of each cell, so the response carries a CORS allow header.
  4. Agent status: When a coding agent's hook runs playpets hook claude, the same executable runs in CLI mode with no window. It turns the hook payload into a state and a code (@tool Bash), reads the port and token from bridge.json in the data folder, and sends them to the app's bridge. The app turns the code into a sentence in the current language, shows it above the friend, and remembers it as recent activity to use when answering "what were you doing?".
  5. Chat: When the page calls chat, the backend looks at the friend's AI connection and asks one of the Claude CLI (a long-lived session per friend), the Codex CLI (one run per message) or an HTTP AI. API keys are read from the OS secure storage after the lock is released. Answers stay in the in-memory chat history (the latest 24 per friend) and reach the page as a chat-message event. Chat history is not saved to a file.
  6. Desktop: The overlay thread reads the cursor every 40ms (while moving) or 120ms (while quiet), advances strolls and drags by one step, and makes only the monitor window with the cursor over a friend take clicks. When a friend moves, it sends the layout to that monitor's window, and when the cursor comes onto a friend or leaves it, a hover event tells the windows which friend shows its name button. A friend belongs to the monitor whose work area holds the centre of its box. Resting positions are batched and saved within 2 seconds.
  7. Update: 20 seconds after start (except in dev builds and E2E), or from the button in Settings, the app reads latest.json from the latest GitHub release. If there is a new version it tells the page; when a person presses install, it downloads it, verifies the signature with the public key, installs and restarts.

Interfaces ​

TouchpointSourceSummary
Page → backend commandsThe COMMANDS table in src-tauri/src/commands.rs (each row: the name, which windows may ask, what it does; the bridge, the access check and the dispatch come from it)Friends (addPet, updatePet, removePet, play), pet files (importUrl, importFolder, openFolder), AI and chat (saveProvider, chat, chatWith), yard chatter (chatter, setAutoChatter), yard sounds (setSound), the garden and its gifts (findSeed, plant, pullPlant, receiveGift, moveDecor, putAway), the album (catch, bloomed), closeness (befriend), copying agent settings and choosing which friend an agent reports to (copyConnection, assignAgent), desktop and windows, language, update, quit. The overlay gets only bootstrap, play, openChat, dragStart, dragStop
Backend → pageapp::changed, app::push_eventchange (full state), event (status, say, play, chat-message, select, window, update, update-status, check-update, open-panel, mini, persona-saved, history-reset, import-request, pet-moved)
Spritesapp::sprite_responseThe pet-asset custom protocol
Coding agentsArchitecture: coding agent integrationCLI commands, hooks, MCP, HTTP bridge

Environment and touchpoints ​

The app data folder is named after the app identifier app.playpets. The identifier does not change after release.

OSData folder
macOS~/Library/Application Support/app.playpets/
Windows%APPDATA%\app.playpets\ (the package virtualization path when installed through MSIX)
Linux$XDG_DATA_HOME or ~/.local/share/app.playpets/

The folder holds state.json (friends, settings, AI connections, the garden and its gifts, the album, closeness, friends and connections parked from an earlier start, the yard sounds' setting, where the mini window was left), bridge.json (the agent bridge's port and token, which change on every launch), pets/ (pets imported from an address) and builtin/ (the three illustrated starter entries bundled in the executable, unpacked at launch). API keys are not in this folder but in the OS secure storage.

Environment variableUse
PLAYPETS_DATAChanges the data folder (E2E, measurement)
CODEX_HOMEChanges the Codex pet folder (<value>/pets)
PLAYPETS_LANGPins the language to one of ko, en, ja, zh-hans
PLAYPETS_E2E_EVALThe E2E scenario script. Honored only by an executable built with the e2e feature (cargo build --profile e2e --features e2e, which writes src-tauri/target/e2e/ so it never replaces the app in target/release/): it turns off single-instance, deep-link registration and the start-at-login plugin (the switch only pretends), turns on the test_only rows of COMMANDS (e2eLog, e2eWindow, e2eTray, e2eFreshGifts), and skips the update check. It runs 3 seconds after the page loads
PLAYPETS_BRIDGE, PLAYPETS_PETThe bridge.json path and default friend for CLI mode
PLAYPETS_DEBUGMakes playpets hook … print to standard error why it could not reach the app. Without it, a hook stays silent
WEBKIT_DISABLE_DMABUF_RENDERERLinux: WebKitGTK's renderer switch. The app sets it to 1 at start when it is unset, since the DMA-BUF renderer leaves a blank window without a usable GPU, and the browser or file manager it opens starts without it; a value already set is left as it is and passed on
PLAYPETS_BINARYThe executable E2E, captures and measurement launch (default src-tauri/target/e2e/playpets; the build workflow points it at the target/release build)

The system starts the app at login with --autostart (tauri-plugin-autostart: a LaunchAgent on macOS, the Run key on Windows, an autostart entry on Linux). Started that way with desktop mode on, the yard window stays hidden.

What differs per OS.

OSDifferences
macOSTurns the overlay into an NSPanel so it shows even over other apps' full screen, and restores the original class before closing (without that the app crashes). Closing the yard also hides the Dock icon. The menu bar exists only on macOS
WindowsThe page draws the window title bar. CLI child processes get no console window. A claude.cmd installed through npm is run directly as node plus the script (a .cmd cannot take arguments that contain line breaks). Folders and addresses open through ShellExecuteW. Cursor coordinates are converted with the scale of the monitor under the cursor. When installed through MSIX, the agent settings point at the execution alias playpets.exe
LinuxThe page draws the window title bar. Distributed as deb and AppImage; E2E runs on Xvfb

Change history ​

  • 2026-09-29 Moved from Electron to a single Tauri v2 shell. The web page is in src/, the backend in src-tauri/
  • 2026-09-30 Split the backend into folders by role. Added Korean, English and Japanese. Switched the page loops to timers and set the lock rule
  • 2026-10-01 Added auto-update and distribution channels (winget, Homebrew tap)
  • 2026-10-05 Bundled the three starter pets in the executable and unpacked them into the data folder (pets/builtin.rs), and added the import link scheme
  • 2026-10-06 Added Simplified Chinese as the fourth language, relationships between a friend and the person, and yard chatter (friends talking among themselves, on a button or a timer)
  • 2026-10-09 The painted yard, the garden, gifts, fishing and bugs, and the album; the yard fills its frame and Settings opens as a sheet over it
  • 2026-10-10 The yard's content as data (catalogue, composition, drawings, effects); field.js, world.js, remarks.js and props.js split out
  • 2026-10-10 Closeness, the yard's sounds, one way to send a friend somewhere (sendTo), and one way to save and announce a change in the backend (app::commit)
  • 2026-10-10 One COMMANDS table for every command; app.rs split into app/load.rs and app/sprite.rs; trips.js and hint.js split out of the views; parked friends; closeness levels and relationship voices; one cancel flag per chat; app::hold for every lock but the state; the chat CLIs run in a private working folder and get the system prompt from a file there (Claude Code) or standard input (Codex), not the command line; another friend's words kept to one line

Free and open source under the MIT license. Not affiliated with OpenAI; Codex is a trademark of OpenAI.