Skip to content

fix(pet): match Codex's per-frame pacing and pointer interactions for imported pets - #8730

Merged
nwparker merged 8 commits into
stablyai:mainfrom
nasagong:fix/pet-codex-sprite-pacing
Jul 17, 2026
Merged

nwparker merged 8 commits into
stablyai:mainfrom
nasagong:fix/pet-codex-sprite-pacing

Conversation

@nasagong

@nasagong nasagong commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #8729

Imported .codex-pet bundles 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.

applyCodexPetDefaults mirrors Codex's animation rows (idle=0 … review=8) but flattens the pacing into a sheet-wide fps: 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): SpriteAnimation gains optional frameDurationsMs; the Codex default tables (now with durations) move to src/shared/codex-pet-sprite-defaults.ts so main bakes them at import and the renderer reuses the same fingerprint. SpriteFrame renders uneven holds via one step-end keyframe stop per frame; uniform-fps sprites keep the existing steps() path. Pets imported before durations existed persist the exact legacy default fingerprint, so usePetUrl upgrades them in place — no re-import needed.
  • feat(pet): pointer interactions align with the Codex App mascot. Hover plays jumping, grabbing snaps the pet to frame 0 of its live agent state and holds there, and dragging horizontally plays running-right/running-left with a 4px deadzone that keeps the last direction on vertical moves. Live agent states resume on release/leave.

Screenshots

Before (uniform 8 fps) After (Codex per-frame pacing)
issue issue3

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test — no failures introduced by this branch (the 28 locally-failing tests fail identically on main in this environment; one flaky pty-connection test passes on rerun)
  • pnpm build
  • Added regression tests: duration-table invariants (lengths match frame counts, idle totals 6600 ms), legacy-fingerprint upgrade (hand-authored layouts/fps/durations pass through untouched), per-frame step-end keyframe emission, bundle-import length validation, and pointer interaction selection (hover, grab-and-hold, drag direction hysteresis)
  • Live verification: launched the built app against a profile persisted by the previous build (legacy sprite without durations) and confirmed via CDP that the overlay renders 6.6s step-end infinite with 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 rows

AI Review Report

  • Ran a multi-angle review (line-by-line, removed-behavior, cross-file tracing, reuse/simplification/efficiency, conventions) with verification passes on the correctness candidates.
  • Removed-behavior + cross-file: the CODEX_PET_* move keeps all consumers resolving via re-exports; frameDurationsMs is optional and round-trips the permissive persisted-UI/RPC schemas; blob-cache skip-detection stays consistent (the upgrade never changes dims/fps).
  • A "hover state can latch and mask agent states" candidate was refuted against the Pointer Events spec: pointercancel/pointerup/capture-release all mandate pointerleave at the capture target, which is the same element carrying the leave handler.
  • Confirmed-but-accepted edge: 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.
  • Cross-platform: explicitly checked for macOS/Linux/Windows — no shortcuts, labels, paths, or shell behavior touched; renderer CSS and standard Pointer Events are platform-neutral, and pet assets load from local userData over 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:importPetBundle are unchanged, and no new IPC channels or dependencies are added. Generated keyframe CSS interpolates only numbers that passed Number.isFinite && > 0 render 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

  • Codex plays app-state animations 3× and then settles into the slow idle even while the state persists; Orca keeps looping the current state's row (its overlay is a live status indicator). Happy to align in a follow-up if desired.
  • v2 sheets (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.
  • The legacy upgrade is a render-time shim by design: persisted data is never rewritten, so the change is fully reversible on downgrade.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Pet 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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements Codex per-frame timing, legacy upgrades, and the requested hover/grab/drag interactions for #8729.
Out of Scope Changes check ✅ Passed The added tests, defaults, renderer updates, and shared types all support the stated pet timing and interaction goals.
Title check ✅ Passed The title clearly summarizes the main change: Codex pet pacing and pointer interactions for imported pets.
Description check ✅ Passed The description includes all required sections and adequately covers summary, screenshots, testing, review, security, and notes.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nasagong

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

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.

@nasagong, I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@brennanb2025

Copy link
Copy Markdown
Contributor

Taking a look!

nasagong and others added 8 commits July 16, 2026 20:26
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>
@nwparker
nwparker force-pushed the fix/pet-codex-sprite-pacing branch from 5701ba2 to 26a2ff9 Compare July 17, 2026 04:41
@nwparker

Copy link
Copy Markdown
Contributor

Took this over and rebased it onto current main, then ran it through several rounds of adversarial review (GPT‑5.6‑Sol at max reasoning). The original approach is solid — mirroring Codex's exact model.rs duration tables (verified against upstream: idle [1680,660,660,840,840,1920], app‑state rows 120–150ms + longer final hold) and the step-end keyframe rendering are spot on. Layered these fixes on top (all with tests, typecheck, lint, and the full pet suite green):

Correctness

  • Grab‑and‑hold now actually holds. spriteAnimate stayed true through a drag, so a stationary grab restarted to frame 0 then kept playing. It now freezes on frame 0 of the live state for a still/vertical grab and only animates once a horizontal drag crosses the deadzone.
  • Each row starts at frame 0. The @keyframes name is keyed on the animation (and SpriteFrame is remounted per pet via key={url}), so hover/agent‑state/direction changes and pet switches no longer inherit the previous animation's currentTime and enter mid‑cycle (confirmed in Chromium).
  • Drag direction tracks horizontal travel only, with the baseline advanced solely on an accepted direction — a slow diagonal drag now accumulates to the threshold instead of resetting every vertical component and never firing.
  • Pointer hook scoped to its pointer (activePointerRef) so a second touch can't hijack the anchors and only the owning pointer's up/cancel — plus lost capture — ends the drag.
  • Explicit fps honored for every value (incl. 8) by baking it as uniform per‑frame durations, which also keeps the sprite out of the legacy‑retiming path.
  • Keyframe fallback guards: degrade to uniform steps() if two stops collapse or the final stop rounds to 100%, so no frame is silently dropped.
  • Hardened against untrusted persisted data: null animation entries and non‑array frameDurationsMs fall back cleanly instead of throwing during render.

Structure

  • Extracted the keyframe generation into a pure, unit‑testable buildSpriteAnimationCss, and the legacy detection into isLegacyCodexSprite (full‑geometry fingerprint so a hand‑authored sheet that merely reuses the row map isn't retimed).

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 frameDurationsMs); and Orca keeps looping the current agent‑state row rather than Codex's 3×‑then‑idle, since the overlay is a live status indicator.

Credit to @nasagong for the original fix and the faithful Codex mirroring. Co‑authored on the follow‑up commits.

@nwparker
nwparker merged commit 13c690b into stablyai:main Jul 17, 2026
1 check passed
nwparker added a commit that referenced this pull request Jul 17, 2026
…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.
@2YH02 2YH02 mentioned this pull request Jul 17, 2026
5 tasks done
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
… 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>
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Imported .codex-pet bundles ignore Codex's per-frame durations and play every row at 8 fps

3 participants