Files
orca/src/shared/codex-pet-sprite-defaults.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00

101 lines
4.7 KiB
TypeScript

import type { CustomPet, SpriteAnimation } from './pet-types'
/** Codex pet sprite layout and pacing, mirroring the tables in Codex CLI's
* `tui/src/pets`. Shared because main bakes these into imported bundles and
* the renderer needs the same fingerprint to upgrade legacy persisted pets. */
export const CODEX_PET_SPRITESHEET_PATH = 'spritesheet.webp'
export const CODEX_PET_FRAME = { width: 192, height: 208 } as const
export const CODEX_PET_DEFAULT_ANIMATION = 'idle'
export const CODEX_PET_DEFAULT_FPS = 8
// Codex sheets are 8 columns wide (its widest rows run 8 frames).
export const CODEX_PET_DEFAULT_COLUMNS = 8
// Codex's `app_state_animation`: every non-final frame holds `frameMs`, the last
// holds `finalMs`. Values below mirror `codex-rs/tui/src/pets/model.rs` exactly.
function appStateDurations(frames: number, frameMs: number, finalMs: number): number[] {
return Array.from({ length: frames }, (_, i) => (i === frames - 1 ? finalMs : frameMs))
}
export const CODEX_PET_ANIMATIONS: Record<string, SpriteAnimation> = {
// Idle is the ambient loop: long holds on the bookend frames, 6.6s cycle.
idle: { row: 0, frames: 6, frameDurationsMs: [1680, 660, 660, 840, 840, 1920] },
'running-right': { row: 1, frames: 8, frameDurationsMs: appStateDurations(8, 120, 220) },
'running-left': { row: 2, frames: 8, frameDurationsMs: appStateDurations(8, 120, 220) },
waving: { row: 3, frames: 4, frameDurationsMs: appStateDurations(4, 140, 280) },
jumping: { row: 4, frames: 5, frameDurationsMs: appStateDurations(5, 140, 280) },
failed: { row: 5, frames: 8, frameDurationsMs: appStateDurations(8, 140, 240) },
waiting: { row: 6, frames: 6, frameDurationsMs: appStateDurations(6, 150, 260) },
running: { row: 7, frames: 6, frameDurationsMs: appStateDurations(6, 120, 220) },
review: { row: 8, frames: 6, frameDurationsMs: appStateDurations(6, 150, 280) }
}
/** The Codex row layout paced uniformly at `fps` (each frame held 1000/fps ms).
* A bundle that pins an explicit fps gets its pacing baked as durations, so the
* fps is honored (SpriteFrame prefers durations over the sheet fps) and the
* sprite carries durations — which keeps it distinct from the no-durations
* legacy fingerprint below, so the render-time upgrade never retimes it. */
export function codexAnimationsAtUniformFps(fps: number): Record<string, SpriteAnimation> {
const frameMs = 1000 / fps
return Object.fromEntries(
Object.entries(CODEX_PET_ANIMATIONS).map(([name, { row, frames }]) => [
name,
{ row, frames, frameDurationsMs: Array.from({ length: frames }, () => frameMs) }
])
)
}
export type CustomPetSprite = NonNullable<CustomPet['sprite']>
/** The exact sprite an old Orca build baked for an imported Codex bundle:
* 192x208 frames on an 8-wide sheet at the flat 8 fps rate, idle default, and
* the nine Codex rows carrying no per-frame durations. Matching the full
* geometry (not just the row map) keeps a hand-authored 8 fps sheet that merely
* reuses those rows from being silently retimed. A pet deliberately built on
* the Codex layout that wants to keep uniform pacing opts out by declaring any
* frameDurationsMs of its own.
*
* Rows/sheet height are intentionally excluded so v2 (11-row) Codex sheets,
* which still bake these nine animations, upgrade too. */
function isLegacyCodexSprite(sprite: CustomPetSprite): boolean {
const animations = sprite.animations
if (
!animations ||
sprite.fps !== CODEX_PET_DEFAULT_FPS ||
sprite.frameWidth !== CODEX_PET_FRAME.width ||
sprite.frameHeight !== CODEX_PET_FRAME.height ||
sprite.columns !== CODEX_PET_DEFAULT_COLUMNS ||
sprite.defaultAnimation !== CODEX_PET_DEFAULT_ANIMATION
) {
return false
}
const names = Object.keys(animations)
if (names.length !== Object.keys(CODEX_PET_ANIMATIONS).length) {
return false
}
return names.every((name) => {
const anim = animations[name]
const preset = CODEX_PET_ANIMATIONS[name]
// Why: `anim` is untrusted persisted data — guard it so a corrupted entry
// (e.g. a null value) is a non-match rather than a render-time throw.
return (
!!preset &&
!!anim &&
anim.row === preset.row &&
anim.frames === preset.frames &&
anim.frameDurationsMs === undefined
)
})
}
/** Pets imported before per-frame durations existed persist the legacy Codex
* fingerprint at the flat 8 fps sheet rate, so swap in the current defaults for
* those. Anything else passes through untouched. Render-time only — persisted
* data is never rewritten, so the upgrade is reversible on downgrade. */
export function applyCodexSpriteTimingDefaults(sprite: CustomPetSprite): CustomPetSprite {
if (!isLegacyCodexSprite(sprite)) {
return sprite
}
return { ...sprite, animations: { ...CODEX_PET_ANIMATIONS } }
}