chore(pet): compress pet webp images and remove unused assets

Reduce resources/claude.webp, gremlin.webp, opencode.webp sizes; drop
the obsolete pet-overlay design doc and compress-pet-glb script now
that the GLB pipeline has been replaced by 2D webp images.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-05-01 16:16:15 -07:00
co-authored by Orca
parent 152a1ac86e
commit e3ee5ce776
5 changed files with 0 additions and 182 deletions
-98
View File
@@ -1,98 +0,0 @@
# Design: Pet Overlay (v1)
**Branch:** `add-a-pet-for-orca`
**Status:** Draft — scoped for fast ship
## Problem
Orca has no idle-time personality surface. A small animated pet in the bottom-right corner gives the app a recognizable mascot and is the kind of detail users screenshot and share. It needs to be opt-in (some users find mascots distracting) and cheap when disabled.
## Goals (v1)
- Floating pet overlay pinned to bottom-right, above terminal panes and below modals.
- Toggled by an experimental flag in Settings → Experimental.
- "Hide pet" menu from a status-bar segment for quick dismissal without disabling the feature.
- Visually 3D / animated, not a flat SVG.
- Zero cost when disabled — no renderer weight, no dangling listeners.
## Non-goals (v1)
- Tint, accessories, full Pet Studio — follow-up.
- Drag-to-reposition — follow-up. Position is fixed bottom-right.
- Reactions to app state beyond a single idle loop — follow-up.
- Pet-to-pet multiplayer, voice/audio, reacting to code or AI output.
**In v1:** a model picker across three bundled GLBs (gremlin, blue demon, green blob) **plus user-uploaded custom GLBs**. Selection lives in `petModelId` on `PersistedUIState`, flipped from a Model submenu in the status-bar pet dropdown. Custom models live in `userData/pets/custom/<uuid>.glb` on disk and are listed alongside bundled models in the submenu, each with an inline Remove button. Upload goes through a `pet:import` IPC that opens a native file picker, validates size/extension, and copies the file into userData; the renderer fetches bytes via `pet:read` and renders them via a `blob:` URL so `webSecurity=true` and `sandbox=true` can stay enabled. Kept tiny on purpose — no separate pane, no per-model settings; this is the customization surface that fits on the existing dropdown without introducing a Pet Studio.
## Format decision: glTF (GLB) via three.js
Considered options:
| Format | 3D? | Size | Notes |
|---|---|---|---|
| SVG / Lottie | No | Tiny | Can't do realistic shading |
| Transparent WebM (VP9 alpha) | Pre-rendered | Medium | Baked pixels, no runtime variation |
| Sprite sheet (PNG frames) | No | Large | Rigid, no angle changes |
| **GLB + three.js** | Yes | ~500KB model + ~150KB gz (~600KB raw) for three + fiber subset | Real 3D, room to grow into customization |
**Decision: GLB + three.js.** Even without v1 customization, GLB is the only option that leaves the door open for the follow-up features without a format migration.
Stack: `three` + `@react-three/fiber`. All imports live behind a dynamic `import()` gated on the experimental flag so disabled users pay zero bytes.
## UI integration
### Experimental flag
Add to `GlobalSettings` in `src/shared/types.ts`:
```ts
/** Experimental: floating animated pet overlay in the bottom-right corner.
* Opt-in because it loads three.js (~150KB gz) and a GLB model; users who
* don't want it should pay zero cost. When false, the overlay component
* is never mounted and three.js is never imported. */
experimentalPet: boolean
```
Default `false`. Same pattern as `experimentalAgentDashboard` at `src/shared/types.ts:1047`, with one deliberate difference: `experimentalAgentDashboard` requires a relaunch because it gates hook installation in the main process; `experimentalPet` is purely renderer-side, so toggling it mounts/unmounts `PetOverlay` immediately and triggers the dynamic `import()` on first enable within a session.
### Status bar segment
`src/renderer/src/components/status-bar/StatusBar.tsx` gets a new `PetStatusSegment` (only rendered when `experimentalPet` is on). It does **not** participate in the user-configurable `statusBarItems` array (`src/shared/types.ts:1090`) in v1 — that list is for always-available provider/system segments, and gating pet visibility via two independent toggles (experimental flag + status-bar checkbox + in-menu "Hide pet") would be confusing. Revisit if/when pet promotes out of experimental.
Clicking the segment opens a menu with:
- **Hide pet** — flips a `petVisible` boolean stored on the ui slice. Kept separate from `experimentalPet` so a user can re-show without re-enabling the feature.
- **Pet settings…** — opens Settings → Experimental (just scrolls to the flag in v1; Pet Studio is follow-up).
### Persistence
`petVisible` must survive reload. Follow the existing pattern used by every other persisted UI field:
1. Add `petVisible: boolean` to `PersistedUIState` in `src/shared/types.ts:1098`.
2. Hydrate it in `hydratePersistedUI` in `src/renderer/src/store/slices/ui.ts` (default `true` when absent, so existing users see the pet the first time they enable the flag).
3. Include it in whatever the ui slice emits to the main-process persistence writer. No new persistence channel is needed — this is the same path `statusBarVisible`, `sidebarWidth`, etc. take.
`experimentalPet` itself lives on `GlobalSettings` and is persisted through the settings pipeline, not `PersistedUIState`.
### Overlay component
`src/renderer/src/components/pet/PetOverlay.tsx`:
- Mounted once at the app root, conditional on `experimentalPet && petVisible`.
- Fixed bottom-right canvas, `pointer-events: none` on the whole canvas. All interaction goes through the status-bar segment: a pet that eats clicks in the bottom-right would silently block terminal-pane interaction near the corner, and hit-testing a transparent GLB against actual mesh bounds (not the canvas rect) is non-trivial. Revisit once drag-to-reposition lands.
- Transparent background; `z-index` just under the modal layer.
- Single idle animation clip on loop.
- Pause the animation loop when `document.visibilityState !== 'visible'` to avoid burning battery in the background.
- Respect `prefers-reduced-motion` via `window.matchMedia('(prefers-reduced-motion: reduce)')`: when reduced, render a static first frame and skip the animation loop.
- If WebGL context creation fails (some Linux/VM setups), unmount silently — do not crash the app root. Log once.
### Architectural boundary
No pet-specific IPC and no main-process pet logic. Rendering, animation loop, visibility-state handling, and the `petVisible` toggle all live in the renderer. The only thing that crosses IPC is the generic `PersistedUIState` write-back that `petVisible` rides on, alongside every other persisted ui-slice field — this is existing infrastructure, not new plumbing. This matches the boundary documented in `CLAUDE.md`: cosmetic per-window state stays in the renderer.
## Rollout
1. Land experimental flag + overlay + status-bar segment behind `experimentalPet=false`, with one bundled gremlin GLB in `resources/pets/`.
2. Dogfood internally; watch for WebGL init failures across macOS/Windows/Linux.
3. Follow-up PRs: drag-to-reposition (expands position to all four corners), tint + accessories, additional models, custom GLB loading, state-reactive animation clips.
4. Promote out of experimental once no WebGL-init crash reports surface from dogfood and external bug channels across macOS/Windows/Linux. There is no in-app telemetry pipeline (grep confirms), so the bar is "no reported failures" rather than a measured success rate — log WebGL-init failures to the renderer console with a stable prefix (`[pet-overlay]`) so users who hit it can surface the string when reporting, and so dogfooders can grep their own logs.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 MiB

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 MiB

After

Width:  |  Height:  |  Size: 740 KiB

-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env bash
# Compress a GLB for use as a bundled pet model.
#
# Why two passes: the first `optimize` run with `--compress draco` must decode
# any existing Draco compression before it can downscale textures; textures
# dominate file size for most source models, so the real win comes from the
# second pass after textures are shrunk + re-encoded as WebP at 512px.
#
# Typical result: 20–30 MB source → <1 MB bundled. Output lands at
# resources/pets/<slug>.glb unless --out is given.
#
# Usage:
# scripts/compress-pet-glb.sh <input.glb> [--out <path>] [--size 512]
#
# Example:
# scripts/compress-pet-glb.sh ~/Downloads/some_model.glb --out resources/pets/cat.glb
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "usage: $0 <input.glb> [--out <path>] [--size <px>]" >&2
exit 1
fi
INPUT="$1"
shift
SIZE=512
OUT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUT="$2"
shift 2
;;
--size)
SIZE="$2"
shift 2
;;
*)
echo "unknown flag: $1" >&2
exit 1
;;
esac
done
if [[ ! -f "$INPUT" ]]; then
echo "input not found: $INPUT" >&2
exit 1
fi
if [[ -z "$OUT" ]]; then
BASENAME="$(basename "$INPUT" .glb)"
# Why: produce a kebab-case slug so the filename matches the PetModelId
# convention (lowercase, hyphen-separated) without manual renaming.
SLUG="$(echo "$BASENAME" | tr '[:upper:] _' '[:lower:]--' | tr -cd 'a-z0-9-' | sed 's/--*/-/g;s/^-//;s/-$//')"
OUT="resources/pets/${SLUG}.glb"
fi
echo "==> compressing: $INPUT"
echo " -> $OUT (textures: ${SIZE}px, webp + draco)"
# Why: write to a temp file first so a failed second pass doesn't leave a
# half-optimized GLB at the destination.
TMP="$(mktemp -t pet-glb.XXXXXX).glb"
trap 'rm -f "$TMP"' EXIT
npx --yes @gltf-transform/cli optimize "$INPUT" "$TMP" \
--compress draco \
--texture-compress webp \
--texture-size "$SIZE"
# Why: second optimize pass. After the first pass decoded Draco and shrank
# textures, re-running draco + webp compresses the now-smaller assets far
# more aggressively (observed: 8.8 MB → 0.8 MB on tiny_planet dinosaur).
npx --yes @gltf-transform/cli optimize "$TMP" "$OUT" \
--compress draco \
--texture-compress webp \
--texture-size "$SIZE"
SIZE_BYTES=$(wc -c <"$OUT" | tr -d ' ')
SIZE_KB=$(( SIZE_BYTES / 1024 ))
echo "==> done: $OUT (${SIZE_KB} KB)"