Repository navigation
fix(pet): match Codex's per-frame pacing and pointer interactions for imported pets - #8730
Conversation
📝 WalkthroughWalkthroughPet sprite animation manifests now support per-frame durations with import validation. Shared Codex defaults upgrade eligible legacy sprites while preserving custom timing layouts. Renderer animation selection now handles drag direction and hover state through a unified pointer interaction hook. PetOverlay generates duration-based keyframes when available, restarts sprite animations across drag generations, and separates sprite motion from bobbing. Tests cover shared defaults, bundle validation, animation selection, pointer behavior, and rendered CSS. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
|
The PR description image "issue" shows a dark-themed app titled "Clawd" with a small pixel-art pet character (an orange creature wearing a brown hat) displayed in the corner near a status bar showing icons and "2 hosts" with a green indicator. The PR description image "issue2" shows the Orca app's welcome screen with the Orca logo and a pixel-art octopus/pet character wearing blue headphones, alongside Korean text inviting the user to add a project or create a workspace.
✅ Action performedReview finished.
|
|
Taking a look! |
Imported .codex-pet bundles played every animation at a uniform 8 fps, so the ambient idle loop (6.6s in Codex, with ~1.7-1.9s holds on its bookend frames) cycled in 0.75s and pets flipped through poses roughly 9x faster than intended. Mirror the per-frame duration tables from Codex (codex-rs tui/src/pets/model.rs) into the codex pet defaults, extend SpriteAnimation with optional frameDurationsMs, and render uneven holds as step-end keyframes with one stop per frame. Pets imported before durations existed carry the exact legacy default fingerprint, so the renderer upgrades them in place without forcing a re-import.
The overlay's only pointer reaction was playing the jumping row while dragged, so most of the interaction rows that codex pet sheets ship never showed. Align with the Codex App mascot: hovering plays the jumping row, grabbing snaps the pet to frame 0 of its live agent state and holds there, and dragging horizontally runs toward the pointer (running-right/left) with a 4px deadzone that keeps the last direction on vertical moves. The sprite keeps animating through a drag so the running rows show, and live agent states resume on release. The drag and hover handlers move into usePetPointerInteraction to keep PetOverlay focused on rendering.
…rame CSS Addresses adversarial review of the Codex pet pacing work: - Grab-and-hold now actually holds: a stationary (or vertical-only) grab freezes the sprite on frame 0 of the live state; a horizontal drag past the 4px deadzone resumes animating so the running rows show. Previously spriteAnimate stayed true through a drag, so the sprite restarted then kept playing, contradicting the intended Codex interaction. - applyCodexSpriteTimingDefaults only upgrades the full legacy Codex geometry (192x208 frames, 8-wide sheet, idle default) rather than any 8 fps sheet that merely reuses the nine Codex rows, so a hand-authored sheet is never silently retimed. v2 (11-row) sheets still upgrade. - Keyframe generation moves to a pure, unit-testable buildSpriteAnimationCss; it degrades to uniform steps() if two per-frame stops would collapse to the same rounded percentage rather than dropping a frame to the later duplicate selector. Co-authored-by: nasagong <zinho2000@gachon.ac.kr> Co-authored-by: Orca <help@stably.ai>
… stop Second adversarial review round: - usePetPointerInteraction tracks the owning pointerId in a ref: extra pointers can't hijack the anchors, only the owning pointer's up/cancel ends the drag, and a lost pointer capture also ends it so it can't wedge. - The drag direction moves into a ref read/written synchronously, so two coalesced pointermoves in one commit can't resurrect a stale direction from the state closure. - stepEndStops also falls back to uniform when the final stop rounds to 100% (a trailing sub-precision frame that would otherwise have no interval before the loop), mirroring the leading-collapse guard. - Legacy upgrade documents its explicit-durations opt-out. - Tests: hook-level coverage for direction hysteresis, batched-move regression, multi-touch scoping and lost-capture; the grab-hold test now asserts the restart id change and the running-right row selection; a trailing-short-frame fallback case. Co-authored-by: Orca <help@stably.ai>
Third adversarial review round: - applyCodexPetDefaults bakes the untimed layout when a Codex-layout bundle pins an explicit fps, so the requested uniform pacing is honored instead of being overridden by the injected per-frame durations. The timed table is used only when fps is omitted. - SpriteFrame keys the @Keyframes name on the selected animation, not just useId + restartKey. Reusing one name across rows let the browser preserve currentTime, so a hover/agent-state/direction change entered the new row mid-cycle; a distinct name per animation starts each cleanly (restartKey still restarts a same-row grab). - Tests: explicit-fps untimed baking; the grab-hold test now asserts the keyframes name is keyed per row (idle -> running-right -> idle). Co-authored-by: Orca <help@stably.ai>
…tions Fourth adversarial review round: - An explicit fps is baked as uniform per-frame durations (codexAnimationsAtUniformFps) rather than a durationless layout. The fps is honored for every value (SpriteFrame prefers durations), and because the sprite now carries durations it never matches the no-durations legacy fingerprint — so fps=8, which is indistinguishable from a legacy import otherwise, is no longer silently retimed. New imports always carry durations, leaving the render-time upgrade to migrate only pre-durations persisted pets. - isLegacyCodexSprite guards each animation entry (untrusted persisted data) so a corrupted null value is a non-match, not a render-time throw. Co-authored-by: Orca <help@stably.ai>
…er pet Fifth adversarial review round: - nextPetDragAnimation keys direction purely on horizontal travel and the hook advances its baseline only when a direction is accepted. A slow diagonal drag (each move under 4px horizontally but drifting rightward) now accumulates toward the threshold and triggers running-right, instead of resetting the baseline on every vertical component and never firing. - SpriteFrame is keyed on the pet url so switching between cached pets replaces the element rather than updating it in place; a fresh element starts its CSS animation at frame 0 instead of inheriting the previous pet's currentTime under a reused @Keyframes name. - Trimmed two over-long why-comments to the one-to-two-line convention. - Tests: diagonal-drag accumulation, per-pet remount (node identity), and the updated single-axis nextPetDragAnimation contract. Co-authored-by: Orca <help@stably.ai>
validFrameDurations used a truthiness check, so a corrupt persisted or
RPC-synced value that is a non-array with a matching `length` (e.g.
{ length: 6 } or a 6-char string) passed the length check and then threw
on `.every` during render. Require Array.isArray first so such values fall
back to uniform pacing instead of crashing the overlay. The import-time
zod schema still validates fresh local imports; this covers stale or
hand-edited persisted state.
Co-authored-by: Orca <help@stably.ai>
5701ba2 to
26a2ff9
Compare
|
Took this over and rebased it onto current Correctness
Structure
Deliberate trade‑offs (documented in code): the legacy upgrade is a render‑time shim keyed on geometry, not a persisted provenance bit (a byte‑identical pre‑existing hand‑authored Codex‑layout sheet wanting uniform pacing would be retimed; opt‑out = declare any Credit to @nasagong for the original fix and the faithful Codex mirroring. Co‑authored on the follow‑up commits. |
…tions (#9140) Follow-up polish to #8730: key @Keyframes on the resolved row/frames (so a same-row fallback like hover on a jumping-less pet doesn't restart idle), mirror the importer's 60s per-frame cap at render, and trim comments to the 1-2 line convention.
… imported pets (stablyai#8730) Imported .codex-pet bundles played every animation at a flat 8 fps (~9x too fast). Mirror Codex's exact per-frame duration tables, render uneven holds as step-end keyframes, upgrade legacy-persisted pets at render, and add the Codex mascot's pointer interactions (hover, grab-and-hold on frame 0, horizontal-drag running). Hardened over several adversarial review rounds: honor explicit fps, start each row/pet from frame 0, scope drag to its pointer, and guard untrusted persisted data. Fixes stablyai#8729 Co-authored-by: nasagong <zinho2000@gachon.ac.kr>
…tions (stablyai#9140) Follow-up polish to stablyai#8730: key @Keyframes on the resolved row/frames (so a same-row fallback like hover on a jumping-less pet doesn't restart idle), mirror the importer's 60s per-frame cap at render, and trim comments to the 1-2 line convention.
Summary
Fixes #8729
Imported
.codex-petbundles play every sprite animation at a uniform 8 fps, roughly 9x faster than Codex intends, and the pet overlay lacks the Codex mascot's pointer interactions.applyCodexPetDefaultsmirrors Codex's animation rows (idle=0 … review=8) but flattens the pacing into a sheet-widefps: 8. Codex paces frames unevenly: idle holds[1680, 660, 660, 840, 840, 1920] ms(a calm 6.6 s ambient cycle), and app-state rows run 120–150 ms/frame with a longer final hold (codex-rs/tui/src/pets/model.rs; the Codex App renderer plays the same values). At uniform 8 fps the idle poses cycle in 0.75 s, so pets frantically flip through poses regardless of state.fix(pet):SpriteAnimationgains optionalframeDurationsMs; the Codex default tables (now with durations) move tosrc/shared/codex-pet-sprite-defaults.tsso main bakes them at import and the renderer reuses the same fingerprint.SpriteFramerenders uneven holds via onestep-endkeyframe stop per frame; uniform-fps sprites keep the existingsteps()path. Pets imported before durations existed persist the exact legacy default fingerprint, sousePetUrlupgrades them in place — no re-import needed.feat(pet): pointer interactions align with the Codex App mascot. Hover playsjumping, grabbing snaps the pet to frame 0 of its live agent state and holds there, and dragging horizontally playsrunning-right/running-leftwith a 4px deadzone that keeps the last direction on vertical moves. Live agent states resume on release/leave.Screenshots
Testing
pnpm lintpnpm typecheckpnpm test— no failures introduced by this branch (the 28 locally-failing tests fail identically onmainin this environment; one flaky pty-connection test passes on rerun)pnpm buildstep-endkeyframe emission, bundle-import length validation, and pointer interaction selection (hover, grab-and-hold, drag direction hysteresis)6.6s step-end infinitewith measured ~1.7 s / ~1.9 s holds on the bookend frames, that hover switches to the jumping row, and that grab/drag play the held state and running rowsAI Review Report
CODEX_PET_*move keeps all consumers resolving via re-exports;frameDurationsMsis optional and round-trips the permissive persisted-UI/RPC schemas; blob-cache skip-detection stays consistent (the upgrade never changes dims/fps).pointercancel/pointerup/capture-release all mandatepointerleaveat the capture target, which is the same element carrying the leave handler.toFixed(4)stop quantization can collapse two keyframe stops for adversarial manifests (a sub-30 ms frame inside a ~30 s total, hand-crafted input only); the dropped frame is ≤1e-6 of the cycle, so impact is nil. Noted for the record.userDataover IPC, so SSH-hosted workspaces are unaffected.Security Audit
The new manifest field is bounded by the existing zod schema (≤512 entries, each positive ≤60 s) plus an import-time length===frames check with a clear error; the manifest size cap and existing path-traversal/symlink/TOCTOU defenses in
pet:importPetBundleare unchanged, and no new IPC channels or dependencies are added. Generated keyframe CSS interpolates only numbers that passedNumber.isFinite && > 0render guards (hand-edited persisted state is re-guarded independently, falling back to the uniform path), and the keyframes identifier keeps the pre-existing sanitizer.Notes
spriteVersionNumber: 2, 11 rows) carry 16 look-direction poses in rows 9–10 (pointer-gaze in the Codex App); intentionally unused here, as in the current defaults.