Skip to content

Add Lopuuu rainbow unicorn pet - #10

Open
lopugit wants to merge 29 commits into
developfrom
codex/create-custom-pet-based-on-user-profile
Open

Add Lopuuu rainbow unicorn pet#10
lopugit wants to merge 29 commits into
developfrom
codex/create-custom-pet-based-on-user-profile

Conversation

@lopugit

@lopugit lopugit commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Introduce a playful, non-intrusive pet/easter-egg UI so a floating character (Lopuuuuuuuuuu) appears across the app for delight and personality.
  • Render the pet globally from the main layout so it is visible on all pages without requiring per-page changes.

Description

  • Add a new Chakra UI component LopuuuPet at remix/app/components/Pets/LopuuuPet.tsx that renders a unicorn emoji with rainbow, sparkle, brain, and heart accents and CSS keyframe animations.
  • Import and render LopuuuPet from the main layout remix/app/components/Layout/Main.tsx immediately before the Footer so it appears application-wide.
  • The pet is positioned fixed, has pointerEvents: none, zIndex: 20, and an aria-label for accessibility to keep interactions non-blocking and screen-reader friendly.

Testing

  • Ran code formatting with pnpm --prefix remix exec prettier --write app/components/Pets/LopuuuPet.tsx app/components/Layout/Main.tsx, which completed successfully.
  • Built the Remix app with pnpm --prefix remix run build, which produced client and server bundles and completed successfully despite build warnings.
  • Attempted graphify update ., but graphify is not installed in the environment so the update could not run.
  • Attempted a screenshot via a served build, but the screenshot run failed because playwright is not installed and the built server could not load a missing native bcrypt binding.

Codex Task

@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 31, 2026 3:43am
thingtime (develop) Ready Ready Preview Aug 31, 2026 3:43am

Request Review

@lopugit

lopugit commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Wtffff is this 🤣
Screenshot 2026-07-07 at 05 20 41 AM - Google Chrome I2P - Thingtime

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Conflict Resolver: this PR conflicts with main in real source files (beyond graphify-out/), and no AI credential is configured in this repo, so automatic resolution was skipped.

To enable AI conflict resolution, add the ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN Actions secret (easiest: run /install-github-app from the claude CLI). Until then, please resolve the conflicts manually.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Merging main into this branch changes .github/workflows/, which the default token cannot push. Merge the base branch manually (git merge main), or add a CONFLICT_RESOLVER_PAT secret (repo + workflow scope) so this workflow can handle it. Workflow run

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Merging main into this branch changes .github/workflows/, which the default token cannot push. Merge the base branch manually (git merge main), or add a CONFLICT_RESOLVER_PAT secret (repo + workflow scope) so this workflow can handle it. Workflow run

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Merging main into this branch changes .github/workflows/, which the default token cannot push. Merge the base branch manually (git merge main), or add a CONFLICT_RESOLVER_PAT secret (repo + workflow scope) so this workflow can handle it. Workflow run

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Could not auto-resolve conflicts with main — manual resolution needed. See the workflow run.

Conflicted files (as recorded by the merge step):

  • remix/app/components/Layout/Main.tsx

@lopugit
lopugit changed the base branch from main to develop August 6, 2026 06:53
@github-actions github-actions Bot added the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 8, 2026
@lopugit
lopugit force-pushed the codex/create-custom-pet-based-on-user-profile branch from 92b5a73 to 2cace47 Compare August 8, 2026 01:39
@github-actions github-actions Bot removed the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🧬 Rebased codex/create-custom-pet-based-on-user-profile onto develop with the AI PR/stack rebase workflow.

  • Previous head: 92b5a7374436b599bf5c793b6e0005a9e26849e2
  • Rewritten head: 2cace47e95e0965bb796367ef4b572d1ac8c617d
  • graphify: refreshed and committed (semantic mode: none)
  • Direct stacked children dispatched: 0

The branch was published once with an exact force-with-lease. Please review the rewritten commits before relying on them.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🤖 Auto-resolve running — the conflict resolver started working on this PR at 14:27 UTC.

Expected to finish around 14:35 UTC (resolutions typically land in 3-8 minutes; the job times out at 30). On success a merge commit resolving the conflicts is pushed to this branch and a result comment follows — no manual action is needed meanwhile.

Live status

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into codex/create-custom-pet-based-on-user-profile — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • graphify-out/GRAPH_REPORT.md
  • graphify-out/graph.json
  • graphify-out/manifest.json
  • remix/app/components/Layout/Main.tsx

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

@github-actions github-actions Bot added the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🤖 Auto-rebase running — the stack rebase started working on this PR at 12:25 UTC.

Expected to finish around 12:40 UTC (rebases typically land in 5-15 minutes; the job times out at 55). On success this branch is force-pushed onto its new base and a result comment follows — no manual action is needed meanwhile.

Live status

@github-actions github-actions Bot removed the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 9, 2026
@lopugit
lopugit force-pushed the codex/create-custom-pet-based-on-user-profile branch from 56340a0 to 9b78a00 Compare August 9, 2026 08:50
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🧬 Rebased codex/create-custom-pet-based-on-user-profile onto develop with the AI PR/stack rebase workflow.

  • Previous head: 56340a0f745f322a1e6e6b1306eb92c5b4eafd2d
  • Rewritten head: 9b78a0054a5f0473626c246105ddd83bc7192843
  • graphify: refreshed and committed (semantic mode: none)
  • Direct stacked children dispatched: 0

The branch was published once with an exact force-with-lease. Please review the rewritten commits before relying on them.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into codex/create-custom-pet-based-on-user-profile — conflicts auto-resolved by the resolve-pr-conflicts workflow.

No AI resolution was needed by merge time; the branch was updated with a plain merge commit.

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

@github-actions github-actions Bot added the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🧬 Rebased codex/create-custom-pet-based-on-user-profile onto develop with the AI PR/stack rebase workflow.

  • Previous head: 7d81d6c271302d80a646937dcc6a7cf351a2e909
  • Rewritten head: 4ec40d9397a85a6edc2914a1d587de749f0dbc1a
  • graphify: refreshed and committed (semantic mode: none)
  • Direct stacked children dispatched: 0

The branch was published once with an exact force-with-lease. Please review the rewritten commits before relying on them.

@github-actions github-actions Bot removed the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 9, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu — found a real one this pass: the Pet switch doesn't work on first paint. Fixed it.

Re-reviewed a7e282f3 against develop@4387af92. 0 failing checks, no CodeQL alerts.
Eight earlier passes on this PR all landed on "no defects found" — this one didn't, and the
reason is a detail none of us had actually traced: when the stored setting becomes readable.

The defect

The off switch is this feature's headline requirement — an earlier pass added it specifically
to satisfy AI_ALL.mdFeature customization defaults. It was being defeated on every
single page load
.

ThingtimeProvider restores persisted state asynchronously:

ThingtimeProvider.tsx:99   const [loading, setLoading] = React.useState(true)
ThingtimeProvider.tsx:395  await localforage.getItem(storageKey)
ThingtimeProvider.tsx:462  setLoading(false)   // only after that resolves

Until that lands, thingtime.settings.theme is still the default tree, so useTtTheme
resolveTheme hands back pet: true no matter what the user stored. The gate was:

if (!petVisible(theme?.general)) return null;

…which reads exactly that default. So a user who turned the pet off got a full-size
animated unicorn painted into the corner on every load, which then popped out of existence
once hydration finished.

That's worse than just leaving it on. A decorative ornament that flashes in and out is the
"annoying" end of the very eggs.ts rule this component's own doc comment is built around —
and it means the switch you added doesn't actually do the thing it promises.

The fix

The repo already answers this shape the other way round. The drawer is persisted chrome too,
and it stays closed — its absent state — until its stored state restores, with Main.tsx
using this same loading flag to suppress the transition so it never animates into a guessed
position. The pet now does the same:

// petCore.ts — stays a pure core, so it's covered by real assertions
export const petMounted = (loading?: boolean | null, general?: { pet?: boolean } | null): boolean =>
  !loading && petVisible(general);
// LopuuuPet.tsx
const { loading } = useThingtime();
...
if (!petMounted(loading, theme?.general)) return null;
  • petVisible is unchanged and still defaults to on, so the pet-on majority — including a
    stored theme predating the key — only sees it arrive one hydration tick later.
  • A non-boolean loading (no provider: embeds, harnesses) reads as not loading, so a signal
    that never arrives can't permanently suppress the pet.
  • I checked before relying on loading that children never observe the useState seed's
    loading: null — it's Object.assign-ed into Everything synchronously during render
    (ThingtimeProvider.tsx:572-580), so the first child render already sees true.

+2 tests covering the gate in both directions and the missing-flag fallback.

One thing I did not change — the pet lands on the DevKit bubble

DevKit.tsx:614-624 puts a 52×52 trigger at fixed bottom: calc(safe + 20px) /
right: calc(safe + 20px), and it renders whenever deployEnv !== 'production' — so on
every preview deploy and every local dev session. The pet is anchored into that same
corner. Working the geometry, the unicorn's bottom-right corner overlaps by ~22×22px, and the
🌈 accent (right: -12px; bottom: 0 on the unicorn box) sits almost entirely underneath
it. Nothing breaks — pointerEvents: none, DevKit is z-99999 and draggable — but that accent
is invisible to every developer and PR reviewer.

There's a house convention for exactly this: BuilderDrawer.tsx:96 sits at right: 84px
(= 20 + 52 + 12) purely to clear that bubble. Clearing it here means either moving the pet
off the corner or lifting it, e.g.

bottom="calc(var(--thingtime-safe-area-bottom, env(safe-area-inset-bottom, 0px)) + var(--thingtime-devkit-bottom-offset, 20px) + 64px)"

Which of those reads better is an aesthetic call about your own pet, on a surface production
users never see — so it's yours, not mine.

Two smaller things

  • The header comment said the pet sits under "nav 10050". Nav.tsx:218 is 9999; 10050 is
    CommanderV2. Corrected in the file — the number had also propagated into an earlier review
    comment of mine, so it was spreading.
  • On the labels: several of my earlier passes read no-promote + no-ai-rebase as "parked,
    hands off" and declined to touch anything. That over-reads them.
    resolve-pr-conflicts.yml:2061 says no-ai-rebase "only declines history rewriting", and
    promote-features-to-main.yml:39 scopes no-promote to promotion. Neither blocks an ordinary
    review fix on the PR's own head. No rebase, no promotion, no branch deletion here.

Verified rather than assumed

  • Cross-element @keyframes genuinely resolves. The pet declares @keyframes lopuuu-* on
    the parent Box but applies animation on descendants with their own classes. I compiled
    that exact shape through stylis 4 (Emotion's compiler): the at-rule comes out unscoped at
    stylesheet top level
    , not nested under .css-hash. EditorSplit.tsx is a true in-repo
    precedent for the cross-element case (declared :568, used on a descendant :606).
  • The reduced-motion hatch reaches the animated nodes. Same compile:
    @media (prefers-reduced-motion: reduce){.css-hash *{animation:none!important}} — a
    descendant selector, which is where all three animations live.
  • No SSR/hydration-mismatch class exists here — this is a plain Vite SPA (index.html,
    entry.client.tsx, no entry.server).
  • Theme compatibility is saferesolveTheme boolean-guards general.pet, and
    PRISM_THEME/MIDNIGHT_THEME spread THINGTIME_THEME, so they inherit the new required
    field without edits.
  • The graphify-out half of this diff (+1,347,771) is one content-addressed snapshot tree,
    expected per .gitattributes (graphify-out/snapshots/** -merge). graphify-cas.test.mjs:
    15/15, and it doesn't assert snapshot-vs-source, so my edits don't stale it.

Validation: petCore 7/7, customise + tokens 14/14 (including the
source-scanning assertions over LopuuuPet.tsx, which the edit had to keep satisfying),
21/21 together, graphify-cas 15/15, prettier --check clean on all three files,
tsc --noEmit --strict clean on petCore.ts.

Still merge-as-is from my side once you're happy with the DevKit-corner question.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Lopu repository review — batch review of 16 open PRs.

Ornamental, but the three ways app-wide fixed chrome usually goes wrong are all handled deliberately — and the reasoning is recorded where the next person will need it, which is the part I'd have asked for:

  • The containing-block trap. Mounting the pet outside mainShiftContainer because a non-none transform there (the mobile drawer shift) would make that element the containing block for position: fixed — so the pet would slide with the drawer and get clipped by the overflow: hidden ancestors. Subtle CSS failure, precisely named.
  • Hydration flash. Gating on useThingtime().loading so the pet doesn't mount on the default-on value while stored settings resolve — otherwise every load flashes a full-size animated unicorn into the corner for exactly the users who switched it off. Defaults are !== false throughout, so a stored theme predating the key keeps working.
  • Unmount rather than display: none, so a hidden pet doesn't keep paying for three infinite animations including a filter: hue-rotate sweep on every page.

Two smaller things I checked: useTtCustomClasses is called above the early return null, so hook order stays stable even though the component unmounts when switched off — correct, and flagged in the comment. And the '*': { animation: 'none !important' } rule is scoped, not global: Emotion prefixes nested selectors with the generated class, so it compiles to .css-hash * and only reaches the pet's own subtree.

Motion being opt-out twice (the prefers-reduced-motion media query inside sx so it lands on first paint, and the theme's Motion switch) with visibility as a separate third control is the right decomposition — motion off keeps the pet and stops it animating; Pet off removes it.

tokens.test.ts pinning the tt-pet selector against TT_CUSTOM_TARGETS['general.pet'] covers the pairing most likely to drift.

No changes made — no defects found.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu review — the pet is sitting under the DevKit bubble

Reviewed 7888f0d6 against develop @ 4387af92. Checks are all green (no failures, no cancellations), and CodeQL reports zero open alerts on this head — so this is a code review, not a check triage.

Nice work overall: the switch/default/back-compat story is careful, the hydration guard is real (I traced loading through ThingtimeProvider to confirm it isn't dead code), and the containing-block reasoning in Main.tsx — keeping the pet out of mainShiftContainer so the mobile translateX can't become its containing block — is exactly right.

The one real defect

The pet was anchored at right: safe-area-right + 24px (md) / + 12px (base). DevKit.tsx pins a 52 × 52px trigger at right: safe-area-right + 20px, bottom: safe-area-bottom + 20px, and its gate is:

deployEnv = envFromCookie.THINGTIME_VERCEL_ENV || env.NODE_ENV
devKit    = !explicitlyOff && (deployEnv !== 'production' || !!env.devKit)

That's on by default on every preview deployment and every local dev session — precisely the builds this pet gets looked at on. The trigger reaches 72px in from the right edge; the 74px unicorn occupied roughly right 24→105. The dev bubble landed squarely on its face.

It's visual only — pointerEvents: none at zIndex 20 meant the DevKit button was never blocked — but the pet is permanent chrome, so the overlap is permanent too.

Clearing that corner is already the house rule, stated in three places:

  • InspectorReopenPill sits at right: 84px, commented "clear of the DevKit bubble bottom-right"
  • AutoLoginPopup: "clear of the bottom-right floating bubbles (DevKit/notifications)"
  • DevKit itself drops off /messages rather than obscure product UI

What I changed

Added PET_DEVKIT_CLEARANCE = 84 to petCore.ts20 + 52 + a 12px gutter, the same number InspectorReopenPill already uses — and pointed the pet at it. The bottom anchor is untouched, so the home-indicator clearance petInset exists for is preserved. Shifting left rather than lifting keeps the pet bottom-anchored and still clears SiteBlocksHost's ✏️ pill on the other side; at ~110px wide it fits from right: 84px even on a 320px viewport.

Two tests came with it: one pins the clearance to the DevKit geometry (>= 20 + 52), one asserts the component actually reads the constant. I mutation-checked the second — reverting the component to the old inset makes it fail, restoring makes it pass, so it bites rather than asserting a tautology.

Validation: 32/32 tests pass across test:pets (9) and test:theme (23); tsc --noEmit --strict clean on the pure core; prettier --check clean on all three changed files.

Notes, not changes — your call

  1. Keyframe naming. lopuuu-float/-sparkle/-rainbow break the tt- prefix convention (tt-bob, tt-pop, tt-drill-in, …), and lopuuu-float is close in spirit to the documented global tt-bob — it just adds rotation, so not a straight duplicate. Reuse or rename if you'd rather; I didn't want to churn the branch over it.
  2. filter: hue-rotate cost. Of the three infinite animations, the 7s hue-rotate sweep is the expensive one — a filtered-layer repaint every frame, now on every non-full-bleed page by default. Properly gated by both the Motion switch and prefers-reduced-motion, so there's an out; flagging only because "on by default" is a product call.
  3. Comment nit. The unmount rationale says a display:none pet "would still cost the three infinite animations" — browsers don't tick animations on display:none elements. Unmounting is still the right call (it drops the DOM and hooks too); just the stated reason is off.

Everything else I checked came back clean: PRISM/MIDNIGHT inherit pet via their THINGTIME_THEME spread so the required field still typechecks; resolveTheme rejects non-boolean overrides; pet custom CSS is scoped to .tt-pet rather than :root and goes through the existing sanitisers; test:pets is wired into test:unit in the right slot.

— Lopu 🦄

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu — the Pet switch was reading the wrong cache tier. Moved it to the pre-paint path.

Re-reviewed f6ed8bde against develop@4387af92. All checks green — no failures, no cancellations; every skipping entry is an unselected control-plane matrix leg. CodeQL: zero open alerts on this head, so nothing to fix or dispose there. This is a code review, not a check triage.

The last pass found that the pet mounted on the pre-hydration default and so flashed at users who'd switched it off, and gated it on useThingtime().loading. That diagnosis was right, and loading is genuinely live — I traced it again (ThingtimeProvider useState(true) at :99, setLoading(false) at :461, exposed on Everything at :559/:578). But the fix reached for the wrong tier, and the repo is explicit about which one:

Use the synchronous ~/hooks/localCache tier … for anything that gates first paint; the async localforage thingtime blob cannot seed the first render.
AI_ALL.md, Optimistic rendering at all times

Tier 1: ~/hooks/localCacheThe ONLY tier fast enough to gate first paint.
Tier 2: async stores — the localforage “thingtime” blob (ThingtimeProvider) … For large state that does not gate first paint.
— design-system → Practices

The pet's visibility is a first paint, and it was being decided by Tier 2.

What that cost

  1. Every load, for the pet-on default majority. The pet was suppressed until localforage resolved, then popped in a beat late. We'd traded a flash-out for a pop-in rather than closing the gap.
  2. An ornament wired to the global persistence lifecycle. hydrate() returns early and retries (250ms → 5s backoff) without clearing loading when localforage throws — deliberately, so the unread stored value isn't overwritten. Where that keeps failing (IndexedDB blocked/disabled, e.g. private-mode webviews) loading never resolves and the pet never appears at all. A decorative unicorn shouldn't be collateral damage from a storage-tier failure it has no stake in.

The fix — ride the path every other theme token already uses

A synchronous readLocalCache seed would have been the letter of the rule but the wrong shape here: it'd paint differently from SSR and mismatch on hydrate. That's precisely why the repo's pre-paint mechanism is a CSS var rather than a JS read — so I used it.

  • themeToCssVars now emits --tt-pet-display: block | none. It goes here because the docs say so — "New tokens are added HERE, nowhere else" and "Don't add a --tt-* var anywhere except themeToCssVars() — scattered setProperty calls break the snapshot/pre-paint path." It's a ready-to-use CSS value, not a 1/0 flag, matching the --tt-rainbow-anim idiom.
  • petMounted(loading, general)petDisplay() returning var(--tt-pet-display, block); the fallback keeps a first-ever visit (no snapshot yet) on the default.
  • LopuuuPet drops the useThingtime/loading read and applies display={petDisplay()}; the early return is now plain petVisible.

Two tiers, in the order they actually happen: ThemeHost mirrors the var set to localStorage, tt-boot.js reapplies it render-blocking in <head> before React loads — so a pet-off user's very first frame is already correct — and React then unmounts the node once the stored value is readable. petVisible defaulting to on is what makes that ordering safe: React never removes a pet the snapshot had already painted.

One incorrect rationale retired along the way. The old comment justified unmounting with "a display:none pet would still cost the three infinite animations." It wouldn't — per CSS Animations, display: none terminates animations on the element and its descendants. Unmounting is still worth doing (it drops the node and its emoji glyphs), just not for that reason.

Net: pet-off users get no flash — now on frame 1 rather than after hydration — pet-on users get no pop-in, and a storage failure no longer erases the feature.

Also added

  • TESTING.mdDecorative pet. The PR had no TESTING.md coverage, and AI_ALL.md asks for a line whenever a new bug class is fixed. It covers the first-paint regression in both switch positions, the DevKit-clearance regression from the earlier pass, motion/pet independence, reduced-motion, the full-bleed exclusion, and scoped custom CSS — the render-level behaviour a node --test pure-core suite structurally can't reach.
  • Docs correction. The general.pet design-system row claimed it was "Read from JS like iconStyle rather than emitted as a var", which this change makes false. Rewritten to describe the two tiers.
  • Tests. petCore.test.ts gains the var contract, the no-snapshot fallback, unmount-only-on-stored-off, and a source-scan asserting the component applies display={petDisplay()} and no longer reads loading — that last one is the guard against quietly reintroducing the whole problem. tokens.test.ts pins --tt-pet-display to block/none and checks every emitted key matches tt-boot.js's /^--tt-[\w-]+$/ replay filter, so a mis-shaped name can't be silently dropped.

Validation

Check Result
npm run test:pets 11/11 (was 9)
npm run test:theme 25/25 (was 23)
npx tsc --noEmit 108 errors, zero in any file this PR touches
npm run typecheck:ratchet 108 — exactly at baseline, no regression
npm run build exit 0, incl. verify:vercel-output
Bundle var(--tt-pet-display, block) present in the built client assets

Everything else still holds

I re-verified the load-bearing parts rather than trusting the earlier passes: the position: fixed containing-block reasoning for mounting outside mainShiftContainer; resolveTheme's typeof general.pet === 'boolean' guard (shared themes are untrusted input); pet: true in both built-ins so the Switch is controlled from first render; aria-hidden on a purely decorative ornament; the DevKit clearance; and Emotion scoping the sx '*' reduced-motion rule to the pet's own descendants rather than the document.

Also worth stating plainly since it dominates the diff stat: the +1.34M lines are the graphify-out/ CAS snapshot and are not a review concern — 1,014 snapshot entries are already tracked on develop, and .gitignore excludes only the mutable root symlinks and caches. This PR adds one immutable snapshot, which is the documented convention.

With the first-paint tier corrected, this reads as ready to merge. 🦄

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu — the Motion switch had the same tier bug the Pet switch had. Fixed it.

Re-reviewed 4a7f4a93 against develop @ 4387af92. All checks green
(25 pass / 56 skipping / 0 failing), CodeQL snapshot for this head is
empty. One real defect, and it's a familiar one.

The pet animated for a tick at users who'd turned Motion off

An earlier pass on this PR
found the Pet switch couldn't decide a first paint, because
theme.general comes out of the async localforage blob (Tier 2), and moved
it onto the pre-paint --tt-pet-display var. That fix was right — but
petAnimation(spec, motion) reads motion from the same object, and it
didn't get the same treatment.

So on every page load, resolveTheme handed back the default motion: true before hydration and the pet emitted all five shorthands. Anyone with
Settings → Motion off got a bobbing, hue-rotating, sparkling unicorn
for one tick, which then froze. Exactly the flash your own TESTING.md entry
warns about for the Pet switch — one switch over.

It also made the pet the only decorative surface in the app that ignores
the switch pre-paint. The design-system Identity entry is explicit about
this being a house rule — "one toggle stops everything decorative"
banners and avatars ride --tt-rainbow-anim, and eggs.ts /
ConfettiCanvas read --tt-motion off computed style for the same reason.

Worth being precise about the blast radius: this was only the in-app
switch. OS-level prefers-reduced-motion was already correct pre-paint via
the component's CSS media query, so the accessibility-critical path was
never affected. This was a preference being ignored, not a
reduced-motion violation.

The fix — same two tiers you already built, not a second mechanism

- '--tt-pet-display': g.pet ? 'block' : 'none',
+ '--tt-pet-display': g.pet ? 'block' : 'none',
+ '--tt-pet-anim': g.motion ? 'initial' : 'none',
- export const petAnimation = (spec, motion) => (motion ? spec : undefined);
+ export const petAnimation = (spec, motion) =>
+   motion ? `var(--tt-pet-anim, ${spec})` : undefined;

The interesting bit is initial. --tt-rainbow-anim can bake its single
value into the var because there's only one animation behind it; the pet
has three different shorthands with different durations and delays, so
there's no one value to bake. initial is the CSS-wide keyword that makes
a custom property guaranteed-invalid, which makes var() fall through to
each element's own fallback — so one shared var gates all three without
knowing any of them, and collapses them together when motion goes off.

Post-hydration the motion argument still drops the declaration outright.
Both states mean "no animation", so nothing pops when hydration confirms
what the var already painted.

Validation

39/39 across petCore, tokens, customise and themeTryOnCore (your 27

  • 12 pre-existing + 3 I added).

I didn't want to ship a fix resting on --x: initial from spec-reading
alone, so I probed the real cascade in headless Chrome — motion on gives
each element its own spec with delays preserved, motion off gives
animation-name: none on all of them, toggling back on restores them, a
first-ever visit with no snapshot still animates, and the
prefers-reduced-motion !important rule still wins over the var.

One thing you should know that isn't about this feature

app/theme/tokens.test.ts was never running at base. It existed, but
test:theme only pointed at themeTryOnCore.test.ts and no glob covered
app/theme/. This PR wires it in alongside customise.test.ts, so it
quietly closes a pre-existing coverage hole on top of shipping the pet.
Flagging it because that's the kind of thing worth knowing independently of
this branch.

Checked and fine

  • Fixed-positioning. The Main.tsx comment is right —
    mainShiftContainer takes a transform on the mobile drawer shift and
    would capture the pet; it's correctly a sibling outside. mainFlexRoot
    has overflow: hidden but no transform/filter/contain, so it neither
    captures nor clips.
  • DevKit clearance. DEVKIT_TRIGGER_SIZE = 52 at +20px = 72px reach,
    PET_DEVKIT_CLEARANCE = 84 clears it, matching InspectorReopenPill.
    (Trigger's draggable, so it can still be dragged onto the pet — cosmetic
    only at pointerEvents: none / z-index 20. Not worth pinning.)
  • Back-compat & security. resolveTheme guards typeof === 'boolean',
    so themes predating pet and untrusted shared themes default on. Custom
    CSS reuses the existing sanitiser scoped to .tt-pet, and custom is
    per-user and excluded from shared themes.

Merge-ready from my side.

🦄 Lopu · automated review of 4a7f4a93 · fix left as worktree changes for the publisher

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu — the pre-paint path was correct; something else was overwriting it

Re-reviewed 4807b65e against develop @ 4387af92. All checks green, no
CodeQL alerts, so nothing to repair there.

The last few passes on this PR walked the Pet switch, and then the Motion
switch, off the async localforage blob and onto the pre-paint vars. That
reasoning was right. But I went to verify the result actually lands on the first
frame, and it doesn't — the far end of the same path was clobbering it.

What happens on a pet-off reload

  1. tt-boot.js applies the stored snapshot to document.documentElement.style
    — inline, render-blocking. First paint is correctly --tt-pet-display: none. ✅
  2. ThemeHost then re-applies themeToCssVars(theme) inline in a mount effect,
    with no hydration gate.
  3. That theme comes from thingtime.settings.theme, which
    ThingtimeProvider.tsx:387 only fills in after an async localforage.getItem.
    Effects run child-before-parent, so ThemeHost writes before the restore has
    even resolved
    — while theme is still the built-in default.

Net: correct first paint → ThemeHost writes --tt-pet-display: block at inline
priority → the pet appears → localforage resolves → petVisible() unmounts it.
That is exactly the "appear and then disappear" TESTING.md says must not
happen, and the same story for motion-off via --tt-pet-anim.

It was never pet-specific. It clobbered every customised token — a custom
theme painted right, flipped to the defaults for the length of hydration, then
flipped back. And because the snapshot write is debounced 200ms, a hydration
slower than that persisted the defaults over the good snapshot, carrying the
flash into the next load too.

Fix

Two lines in ThemeHost.tsx: bail out while thingtime is hydrating, and add
loading to the deps so the write lands the moment it finishes.

It's safe rather than merely deferred, which is why it stays that small —
GlobalStyles.tsx already declares the same default --tt-* set on :root, so
a first-ever visit with no snapshot still resolves every token, and so do the
getComputedStyle readers in eggs.ts and ConfettiCanvas.tsx. The boot
snapshot is inline, so it outranks that :root rule until the real theme
arrives. On the storage-error path loading stays true while the provider
retries, which now means "keep showing the user's stored snapshot" instead of
"overwrite it with defaults" — the better failure mode.

Important

This is the one thing I'd like your call on. The fix is the root cause of
the behaviour this PR documents and tests, so I made it here rather than just
describing it — but it changes when theme vars first land for every user,
not only pet users. If you'd rather it went in as its own PR against
develop, say so and I'll move it.

Also changed

petCore.test.ts asserted PET_DEVKIT_CLEARANCE >= 20 + 52 with those numbers
hand-copied out of DevKit.tsx. The test exists to stop the pet sliding back
under the dev bubble, but a trigger that grew or moved would do exactly that
while 20 + 52 passed forever — the same argument the file already makes for
scanning LopuuuPet.tsx. It now reads DevKit's real geometry, locating the
trigger by its aria-label so the panel's width="260px" and the status dot's
width="14px" can't be measured instead. Verified by temporarily growing the
trigger to 72px: fails with "a 84px right inset overlaps the DevKit trigger,
which reaches 92px in from the edge"
.

Validation

test:pets 13/13 and test:theme 28/28, run on the exact file lists from
remix/package.json. Both new guards confirmed to fail without their fix, so
neither is a tautology.

Two things I did not change

  • The pet lands on InspectorReopenPill (BuilderDrawer.tsx:96), which is
    fixed at right: 84px; bottom: 14px — the same lane PET_DEVKIT_CLEARANCE
    puts the pet in. In builder edit mode with the inspector closed, they overlap.
    It's cosmetic (pill at z-index 10120 vs the pet's 20, and the pet is
    pointerEvents: none, so nothing is blocked) and every fix I could see
    couples a decorative component to builder state, which seemed worse than the
    overlap. Raising it because it's the same class of bug we just fixed for DevKit.
  • The pet is on by default for logged-out visitors on the public landing and
    marketing pages. Reads as intentional given "app-wide", but it's a default
    worth an explicit yes.

🧹 Housekeeping: comment 5512846054 is a stray Lopu post whose body is the literal string @/tmp/lopu-comment-10.md — a gh call that used --body "@file" instead of --body-file. It carries no Lopu conversation marker, so I've left it alone rather than editing it; it's safe to delete. I checked the control plane and the bug isn't there, so it was a one-off, not something that'll keep recurring.

🦄 Posted by Lopu (github-actions[bot]) — automated review of 4807b65e.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu review — no defects found. This follows the repo's documented rules rather than working around them, which is what makes a decorative feature safe to land app-wide.

The optimistic-first-paint requirement is genuinely satisfied, not just claimed. AI_ALL.md says the async localforage tier "cannot seed the first render", so visibility rides CSS vars instead. I checked the load-bearing link: tt-boot.js filters on /^--tt-[\w-]+$/ — a generic pattern — so --tt-pet-display and --tt-pet-anim are picked up automatically with no allowlist to forget. The no-flash claim holds end-to-end rather than depending on someone remembering to register two new names.

--tt-pet-anim: initial is a genuinely elegant trick. initial on a custom property is the CSS-wide keyword that makes it guaranteed-invalid, so var(--tt-pet-anim, <spec>) falls through to each element's own animation when motion is on, and all three collapse to none together when it's off. That's what lets one shared var gate three different animation shorthands without knowing any of them — the right answer to a problem --tt-rainbow-anim solves by baking one spec in.

The ThemeHost loading guard is a separate, real bug fix and arguably the most valuable line in the PR. Before it, the effect wrote the built-in defaults inline (highest priority) over the pre-paint snapshot tt-boot.js had just applied — a custom theme painted correctly, flipped to defaults for the length of hydration, then flipped back. Worse, the 200 ms debounced snapshot write would persist those defaults over the good snapshot and carry the flash into the next load. That affects every theme user, not just pet users.

Other things that check out:

  • Backwards compatibleresolveTheme only overrides pet for an actual boolean, and both built-in themes default it true, so a stored theme predating the key keeps the pet rather than having it vanish.
  • Mounted outside mainShiftContainer with the right reason — a non-none transform there would become the containing block for a position: fixed element, so the pet would slide with the mobile drawer and be clipped by overflow: hidden ancestors.
  • aria-hidden="true" + pointerEvents="none"; prefers-reduced-motion as a CSS media query so it lands on the first paint with no client-only state that could diverge from the server render.
  • PET_DEVKIT_CLEARANCE = 84 reuses the established 20 + 52 + 12 figure already used by InspectorReopenPill/AutoLoginPopup rather than inventing a new one, and petInset uses --thingtime-safe-area-* with the raw env() fallback like NavDrawer/Footer.
  • Separating pet from motion is the right call and matches the "Feature customization defaults" rule — motion off keeps the pet still, pet off unmounts it.

Validation: petCore.test.ts 13/13 · tokens.test.ts 13/13 · customise.test.ts 6/6 — all pass. New test:pets correctly wired into test:unit, and test:theme extended with the two theme suites, so these actually run in CI.

Run with a minimal ESM resolver standing in for tsx (no node_modules in the review worktree); the modules under test are pure and dependency-free, so this exercises the real code.

Posted by Lopu, Thingtime's PR manager.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu — reviewed 56df7ab6; merge as-is. Two housekeeping items before you do.

All checks green (25 pass / 56 skipping / 0 failing), CodeQL snapshot empty. No changes made — nothing here justified an edit, and I'd rather not churn the branch.

I re-derived the design claims from the source instead of trusting the comments asserting them. They hold:

  • --tt-pet-anim: initial makes the property guaranteed-invalid, so each element's var(--tt-pet-anim, <own spec>) falls back to its own animation — which is what lets one var gate three different shorthands and collapse them together to none. Correct through the stylesheet, the inline setProperty, and the JSON round-trip alike.
  • The new if (loading) return in ThemeHost is the riskiest line in the PR, because it now gates all 64 --tt-* tokens rather than just the pet. It's safe for a structural reason: GlobalStyles.tsx:7 derives its :root block from themeToCssVars(THINGTIME_THEME) itself, so every published token has a :root default automatically and can't drift out of step. The only other thing gated is thingtime:theme-change, which has no in-app listeners.
  • The fixed-position placement is right. The pet is outside mainShiftContainer (whose mobile transform would capture it) but still inside mainFlexRoot, which is overflow: hidden — that does not clip it, because mainFlexRoot has no transform/filter/perspective/contain/will-change and so isn't a containing block for position: fixed.
  • .tt-pet opens no injection vector: applyThemeDoc re-attaches the local custom map, so a shared theme can't carry CSS.

32/32 tests pass locally (petCore 13, tokens + customise 19). The DevKit-clearance test earns its keep — it reads the trigger geometry out of DevKit.tsx rather than duplicating 20 + 52, so a moved bubble fails the test instead of quietly sliding the pet back underneath it.

Two things to sort before merge

1. The committed Graphify snapshot is stale. graph.json carries built_at_commit: 18f19474, but 517 lines across 14 source files changed in the 8 commits after it — the whole Pet switch, the two-tier var design, the ThemeHost fix, and all 32 tests. So the snapshot's source_fingerprint no longer describes this head.

I deliberately did not patch this. The path encodes source_fingerprint/artifact_hash, and .gitattributes fails closed if a content-addressed path ever holds different bytes — hand-writing those bytes would defeat exactly the integrity model #436 exists to provide. It needs a real graphify run (not installed here). Nothing is broken meanwhile; test:graphify-cas passes.

2. The PR description no longer matches the code. It says the pet carries an aria-label — it now correctly uses aria-hidden="true", which is the right call for an ornament that would otherwise announce itself on every route change. The body also describes two files, while the PR now spans the Pet setting across three surfaces, two theme tokens, the ThemeHost hydration fix, and 32 tests.

Separately, not this PR

graphify-out/snapshots is now 1.5 GB across 35 immutable snapshot roots. Coexistence is the documented design and I'm not arguing with it — but neither #436 nor docs/graphify-content-addressed-snapshots.md defines a retention policy, so it grows without bound and every clone and CI checkout pays for it. That deserves its own issue rather than a footnote here.

One nit worth your eye and nothing more: lopuuu-rainbow animates filter: hue-rotate() forever on every non-full-bleed page, and it's the one animation of the three that can't be composited. It's properly gated by the Motion switch and prefers-reduced-motion, so it's a note, not a defect.


Housekeeping on my own side: comment 5512846054 above has the literal body @/tmp/lopu-comment-10.md — an earlier run of mine passed a path where the file contents were meant (--body-file, not --body "@file"). I've left it alone rather than editing it, because it lacks the thingtime-lopu-conversation:v1 marker my editing rule requires. And this PR is now at 94 comments, a lot of them near-identical "re-reviewed, still green" passes; the ones that changed something were worth it, the rest weren't. Worth tightening the re-review trigger so I speak up when a finding or the head actually moves.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu — re-reviewed 56df7ab6 against the new develop; still merge-as-is

The head hasn't moved since the last pass, but develop has
(4387af92 → b8e93ece), so this is a re-review against the current base.
No change made, no defect found.

Live merge into the current develop: clean. (GitHub's cached merge SHA
6dcd565c is a stale test-merge against 4387af92, so it doesn't represent the
merge that would actually happen.)

The two subtle parts hold up

  • ThemeHost's if (loading) return guard is the sharpest change in the PR
    and it is correct. Writing vars before hydration would overwrite the
    pre-paint snapshot inline — highest priority — and the 200 ms-debounced
    snapshot write would then persist those defaults over the good snapshot and
    carry the flash into the next load. loading is in the effect deps so the
    write lands as soon as hydration resolves, and GlobalStyles already declares
    the default --tt-* set on :root, so nothing is missing meanwhile.
  • The Main.tsx placement comment is load-bearing, not decorative. The pet
    is position: fixed, so it genuinely must stay outside mainShiftContainer:
    a non-none transform there makes that element the containing block, and the
    pet would ride the mobile drawer and clip against the overflow: hidden
    ancestors.

Defaulting both petVisible and petMotionEnabled to on is what makes the
two-tier visibility ordering safe — React never rips out a pet the pre-paint
snapshot already painted. That's the right reading of the Tier-1/Tier-2 rule.

The tests are unusually good for decorative code: petCore.test.ts reads the
DevKit trigger geometry out of DevKit.tsx instead of copying 20 + 52, so a
trigger that grows or moves fails the test rather than silently sliding the pet
back under the bubble — and it source-scans LopuuuPet.tsx to assert the
component actually spends PET_DEVKIT_CLEARANCE and petDisplay(), which is
what keeps those constants from becoming ornamental.

Fleet note

This PR conflicts with #578, #295, #291 and #554 once any of them lands —
every collision is remix/package.json. It appends test:pets to the same
~2,000-character test:unit one-liner three siblings also edit, and its
test:theme edit sits adjacent to #554's test:things edit. Full measurement
in the comment on #578.

Also restating, since this is the oldest PR in the batch (opened 2026-06-21):
it carries no-promote and no-ai-rebase, so it is deliberately held out of
promotion.

Checks 22 pass / 18 skipping, no failure to diagnose. CodeQL: 0 open alerts,
nothing dismissed.

— Lopu 🌸

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Branch status still computing. The base branch moved, and GitHub had not finished recomputing whether this PR conflicts or is behind after the detector waited 500s — so no branch update was started this round. The next push or the twice-hourly scheduled sweep (minutes :02/:32) re-checks automatically.

Posted by the conflict detector at 10:18 UTC, 2026-09-03; this notice is edited in place on re-checks.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — PR #10 (head 56df7ab6 vs develop 81bce89d)

No changes needed — I made none. Validated green:

node --import tsx --test app/components/Pets/petCore.test.ts \
  app/theme/tokens.test.ts app/theme/customise.test.ts
# pass 32 | fail 0

The ThemeHost change is the substantial part here, and it isn't pet-specific

Worth calling out because the PR title makes it sound purely decorative. Previously the effect wrote --tt-* inline on <html> while loading was still true — i.e. while theme was still the built-in default. Inline styles outrank the pre-paint snapshot tt-boot.js had just applied, so a custom theme painted correctly, flipped to Thingtime defaults for the length of hydration, then flipped back. And because the snapshot write is debounced 200 ms, a slow hydration persisted those defaults over the good snapshot and carried the flash into the next load.

Gating on loading fixes a real, user-visible bug affecting every themed user, not just pet users. I checked the "nothing is missing meanwhile" claim — GlobalStyles declares the same default --tt-* set on :root, and there's a test asserting every published var has a :root default.

Things I checked rather than assumed

  • Two-tier visibility matches the house rule: theme.general is the async localforage tier, which can't seed a first render, so visibility rides pre-paint --tt-pet-display and the component only unmounts once the stored value is readable. Both petVisible and petMotionEnabled default to on, which is what makes the ordering safe — React never rips out a pet the snapshot already painted.
  • The mount point outside mainShiftContainer is load-bearing, not stylistic. A non-none transform there (the mobile drawer shift) would make it the containing block for position: fixed, and the pet would slide with the drawer and be clipped by the overflow: hidden ancestors.
  • PET_DEVKIT_CLEARANCE = 84 isn't a hand-copied number. The test reads the DevKit trigger's geometry out of DevKit.tsx — locating it by aria-label first so the panel's 260px and the status dot's 14px can't be measured instead — and asserts the clearance still covers it. A DevKit bubble that grew or moved fails the test instead of silently sliding the pet back underneath.
  • aria-hidden + pointerEvents: none is right for an ornament on every non-full-bleed route; an accessible name would announce it once per route change forever.

One thing worth a live confirmation

--tt-pet-anim is set to the string initial when motion is on, relying on initial being the CSS-wide keyword that makes a custom property guaranteed-invalid, so each element's var(--tt-pet-anim, <its own spec>) falls back to its own animation. That's correct per the custom-properties spec and it's a neat way to gate three different shorthands from one var — but it reaches the DOM via setProperty('--tt-pet-anim', 'initial') and tt-boot.js's replay, and the unit tests can only assert the emitted string, not what a browser computes.

Worth one glance in a real browser that the pet actually animates with Motion on. If a browser stored the literal token instead, animation: initial resolves to animation-name: none and the pet would just sit still. Cosmetic either way, which is why I haven't touched it.

CodeQL: 0 open alerts on this head.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected an out-of-date PR branch

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 10:48 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 10:28 UTC (UTC+00:00) 2026-09-03 03:28 PDT (UTC-07:00) 2026-09-03 20:28 AEST (UTC+10:00)
Estimated finish 2026-09-03 10:48 UTC (UTC+00:00) 2026-09-03 03:48 PDT (UTC-07:00) 2026-09-03 20:48 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 16
Repository Conflicting 4
Repository Out-of-date with target 4
Repository GitHub state unknown 2
Repository Part of an open stack 0
Repository Touch files changed by another open PR 8
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 10
This resolver batch Currently resolving 0
This resolver batch Waiting 10
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #291, #295, #554, #564, #578, #595.

Exact branch pair: developcodex/create-custom-pet-based-on-user-profile.

Timeline

  • 10:28 UTC — Detected that develop needs to be merged into codex/create-custom-pet-based-on-user-profile; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected an out-of-date PR branch

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 11:07 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 10:47 UTC (UTC+00:00) 2026-09-03 03:47 PDT (UTC-07:00) 2026-09-03 20:47 AEST (UTC+10:00)
Estimated finish 2026-09-03 11:07 UTC (UTC+00:00) 2026-09-03 04:07 PDT (UTC-07:00) 2026-09-03 21:07 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 17
Repository Conflicting 5
Repository Out-of-date with target 5
Repository GitHub state unknown 2
Repository Part of an open stack 0
Repository Touch files changed by another open PR 9
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 10
This resolver batch Currently resolving 0
This resolver batch Waiting 10
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #291, #295, #554, #564, #578, #595, #601.

Exact branch pair: developcodex/create-custom-pet-based-on-user-profile.

Timeline

  • 10:47 UTC — Detected that develop needs to be merged into codex/create-custom-pet-based-on-user-profile; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🦄 Lopu review — the riskiest change here isn't the unicorn

No changes needed; 32/32 tests pass. But it's worth naming what actually carries risk in this PR, because it isn't the pet.

ThemeHost.tsx's if (loading) return;

That gate sits on the effect that writes every --tt-* var, dispatches thingtime:theme-change, and persists the pre-paint snapshot — so its blast radius is all theming, not this feature.

The reasoning is right. Before it, a slow hydration wrote the built-in defaults inline (highest priority) over the snapshot tt-boot.js had just applied: a custom theme painted correctly, flipped to defaults for the duration of hydration, then flipped back. And since the snapshot write is debounced 200 ms, a slow enough hydration would persist those defaults over the good snapshot and carry the flash into the next load.

I checked the failure mode the gate introduces. loading is only cleared after hydration succeeds, and a persistently failing localforage retries forever with capped backoff — so in that state the vars are never written inline and the theme-change event never fires. That's not a regression: previously the same state wrote built-in defaults, so the user got the default theme either way; now they keep their own last-known snapshot instead, which is better. And the PR pins the invariant that makes it safe with its own test — "every published var has a :root default, so skipping the write loses nothing". Good instinct to guard exactly the thing that could have made the gate unsafe.

On the pet itself

The --tt-pet-anim trick is the neat bit and it's correct CSS: the pet has three different animation shorthands, so it can't bake one value in the way --tt-rainbow-anim does. Setting the custom property to the CSS-wide keyword initial makes it guaranteed-invalid, so each element's var(--tt-pet-anim, <its own spec>) falls back to its own animation when motion is on, and all three collapse to none together when it's off. One variable gating three animations without knowing any of them.

Other things I checked and liked:

  • Main.tsx renders the pet outside mainShiftContainer — and the comment says why: a non-none transform there (the mobile drawer shift) would become the containing block for a position: fixed child, so the pet would slide with the drawer and get clipped by overflow: hidden ancestors. That's a real trap, correctly avoided.
  • PET_DEVKIT_CLEARANCE = 84 reuses the existing convention (InspectorReopenPill, AutoLoginPopup) rather than reinventing it — 20 + 52 + 12.
  • prefers-reduced-motion as a CSS media query rather than client state, so it lands on the first paint and can't diverge from the server render. The '*': { animation: 'none !important' } is Emotion-scoped to the pet's subtree, not global.
  • useTtCustomClasses is called above the early return, so hook order stays stable across the unmount.
  • resolveTheme accepts pet only when boolean and both builtins default it to true, so a stored theme predating the key keeps today's behaviour rather than flashing the pet away.
  • customise.test.ts parses ThemeStudio.tsx for every customKey and asserts it's registered in TT_CUSTOM_TARGETS — that structural guard is why the new .tt-pet target can't silently drift from the class the component sets.

Additive boolean on the theme document, no endpoint, no data access, no new dependency. Nothing to flag on correctness, security or compatibility.

Validated

node --import tsx --test app/components/Pets/petCore.test.ts \
                        app/theme/tokens.test.ts app/theme/customise.test.ts
# tests 32  # pass 32  # fail 0

Test wiring is complete too — test:pets added and included in test:unit, test:theme widened to cover tokens.test.ts and customise.test.ts.

Checks: 25 pass, 56 skipping, 0 failing. no-promote / no-ai-rebase noted — nothing in this review touched the branch history.

— Lopu, automated repository review. Full report accompanies this PR.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected an out-of-date PR branch

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 12:15 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 11:55 UTC (UTC+00:00) 2026-09-03 04:55 PDT (UTC-07:00) 2026-09-03 21:55 AEST (UTC+10:00)
Estimated finish 2026-09-03 12:15 UTC (UTC+00:00) 2026-09-03 05:15 PDT (UTC-07:00) 2026-09-03 22:15 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 16
Repository Conflicting 4
Repository Out-of-date with target 5
Repository GitHub state unknown 1
Repository Part of an open stack 0
Repository Touch files changed by another open PR 8
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 9
This resolver batch Currently resolving 0
This resolver batch Waiting 9
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #291, #295, #554, #564, #578, #595.

Exact branch pair: developcodex/create-custom-pet-based-on-user-profile.

Timeline

  • 11:55 UTC — Detected that develop needs to be merged into codex/create-custom-pet-based-on-user-profile; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected an out-of-date PR branch

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 13:07 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 12:47 UTC (UTC+00:00) 2026-09-03 05:47 PDT (UTC-07:00) 2026-09-03 22:47 AEST (UTC+10:00)
Estimated finish 2026-09-03 13:07 UTC (UTC+00:00) 2026-09-03 06:07 PDT (UTC-07:00) 2026-09-03 23:07 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 17
Repository Conflicting 3
Repository Out-of-date with target 5
Repository GitHub state unknown 1
Repository Part of an open stack 0
Repository Touch files changed by another open PR 9
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 10
This resolver batch Currently resolving 0
This resolver batch Waiting 10
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #291, #295, #554, #564, #578, #595.

Exact branch pair: developcodex/create-custom-pet-based-on-user-profile.

Timeline

  • 12:47 UTC — Detected that develop needs to be merged into codex/create-custom-pet-based-on-user-profile; assigning the exact snapshot to the resolver queue.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu detected an out-of-date PR branch

Status: Work detected — Lopu is taking ownership.

Current phase: Entering Lopu's serialized PR-resolution queue.

Estimated completion: around 13:17 UTC (~20 minutes; this adjusts as the queue moves).

Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run.

Time conversion (UTC source)

Moment UTC Los Angeles Melbourne
Updated 2026-09-03 12:57 UTC (UTC+00:00) 2026-09-03 05:57 PDT (UTC-07:00) 2026-09-03 22:57 AEST (UTC+10:00)
Estimated finish 2026-09-03 13:17 UTC (UTC+00:00) 2026-09-03 06:17 PDT (UTC-07:00) 2026-09-03 23:17 AEST (UTC+10:00)

Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically.

Lopu queue and PR pulse

Scope Metric Count
Repository Open PRs 16
Repository Conflicting 4
Repository Out-of-date with target 5
Repository GitHub state unknown 1
Repository Part of an open stack 0
Repository Touch files changed by another open PR 8
Repository Target a non-root branch without an open parent PR 0
This resolver batch Admitted snapshots 9
This resolver batch Currently resolving 0
This resolver batch Waiting 9
This resolver batch Finished 0

Related PR context

  • Stack: No open parent or child PR currently links to this branch.
  • Target: develop is a repository root/integration branch.
  • Changed-file overlap: 2 changed files are also touched by #291, #295, #554, #564, #578, #595.

Exact branch pair: developcodex/create-custom-pet-based-on-user-profile.

Timeline

  • 12:57 UTC — Detected that develop needs to be merged into codex/create-custom-pet-based-on-user-profile; assigning the exact snapshot to the resolver queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex lopu: mergeable The PR branches can currently be merged without conflicts lopu: overlapping files This PR changes files also changed by another open PR lopu: queued The current PR snapshot is waiting in Lopu's PR-management queue no-ai-rebase Opt this PR's head branch out of AI history rewriting; the merge resolver owns its conflicts no-promote Never promote this develop PR to main

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant