From a1e699ae55085416924df00b9105cc1764f1fa06 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 29 Jun 2026 14:57:26 -0500 Subject: [PATCH] feat(desktop): roaming pet patrols the base of an open overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a full-screen route overlay (settings/profiles/cron/agents/command-center) is up, the pet's walkable surface swaps to a single ledge at the overlay card's bottom edge — derived from OverlayView's shared inset, not measured — so it patrols there; closing the overlay restores the normal surfaces and it drops back down. --- .../src/components/pet/floating-pet.tsx | 3 ++ .../src/components/pet/use-pet-roam.ts | 41 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/components/pet/floating-pet.tsx b/apps/desktop/src/components/pet/floating-pet.tsx index 87c9026ed4e..2bc9512ecee 100644 --- a/apps/desktop/src/components/pet/floating-pet.tsx +++ b/apps/desktop/src/components/pet/floating-pet.tsx @@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react' import { useCallback, useEffect, useRef, useState } from 'react' import { useGatewayRequest } from '@/app/gateway/hooks/use-gateway-request' +import { useRouteOverlayActive } from '@/app/hooks/use-route-overlay-active' import { persistString, storedString } from '@/lib/storage' import { $petAtRest, $petInfo, $petRoam, $petRoamDir, clearPetUnread, type PetInfo, petProfile, setPetInfo } from '@/store/pet' import { resetPetGallery, setPetScale } from '@/store/pet-gallery' @@ -108,6 +109,7 @@ export function FloatingPet() { const roamEnabled = useStore($petRoam) const atRest = useStore($petAtRest) const roamDir = useStore($petRoamDir) + const routeOverlayOpen = useRouteOverlayActive() const [position, setPosition] = useState(loadPosition) const containerRef = useRef(null) @@ -391,6 +393,7 @@ export function FloatingPet() { enabled: roamEnabled && active && !overlayActive && atRest, isInteracting: isDragging, loopMs: info.loopMs ?? 1100, + overlayOpen: routeOverlayOpen, petH, petW }) diff --git a/apps/desktop/src/components/pet/use-pet-roam.ts b/apps/desktop/src/components/pet/use-pet-roam.ts index aff24b5b7bf..bd4f416180b 100644 --- a/apps/desktop/src/components/pet/use-pet-roam.ts +++ b/apps/desktop/src/components/pet/use-pet-roam.ts @@ -1,5 +1,6 @@ import { type RefObject, useEffect } from 'react' +import { TITLEBAR_HEIGHT } from '@/app/shell/titlebar' import { $petMotion, $petRoamDir, type PetState } from '@/store/pet' interface Point { @@ -126,6 +127,8 @@ interface PetRoamOptions { petH: number /** Sprite animation loop duration (ms) — paces the walk to the leg cadence. */ loopMs: number + /** A full-screen route overlay (settings/profiles/…) is up: patrol its base. */ + overlayOpen: boolean /** Persist the resting position back to React state when the loop settles. */ commit: (point: Point) => void } @@ -151,7 +154,16 @@ interface PetRoamOptions { * the shared `$petState`, and `$petRoamDir` (-1/0/1) lets the floating pet pick * the directional run row + mirror for the travel direction. */ -export function usePetRoam({ enabled, containerRef, isInteracting, petW, petH, loopMs, commit }: PetRoamOptions): void { +export function usePetRoam({ + enabled, + containerRef, + isInteracting, + petW, + petH, + loopMs, + overlayOpen, + commit +}: PetRoamOptions): void { useEffect(() => { if (!enabled) { $petMotion.set(null) @@ -281,13 +293,26 @@ export function usePetRoam({ enabled, containerRef, isInteracting, petW, petH, l return best ?? ledges[0]! } - const planNext = () => { - const ledges = snapshotLedges(petW, petH) - curLedge = resolveLedge(ledges) - const grounded = Math.abs(cur.y - groundTop(curLedge)) <= GROUND_EPS + // While an overlay is up, it's the only walkable surface: a single ledge at + // the overlay card's bottom inner edge. The card uses `OverlayView`'s equal + // inset on every side — `titlebar-height + padding` — so derive it from that + // (never measured). + const overlayLedge = (): Ledge => { + const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 + const inset = TITLEBAR_HEIGHT + (vw() >= 640 ? 0.875 : 0.625) * rem - if (!grounded) { - // Dragged into the air (or a perch vanished): fall to the surface below. + return { left: inset, right: Math.max(0, vw() - inset - petW), y: vh() - inset } + } + + const planNext = () => { + // An open overlay swaps the surface set to just its bottom edge, so the pet + // patrols along it; closing it restores the normal surfaces (and the pet + // drops to whatever's below). + const ledges = overlayOpen ? [overlayLedge()] : snapshotLedges(petW, petH) + curLedge = resolveLedge(ledges) + + if (Math.abs(cur.y - groundTop(curLedge)) > GROUND_EPS) { + // Dragged into the air, or the surface moved out from under it: fall. beginVertical(curLedge) return @@ -414,5 +439,5 @@ export function usePetRoam({ enabled, containerRef, isInteracting, petW, petH, l // the loop stops re-asserting it. commit({ ...cur }) } - }, [enabled, petW, petH, loopMs, containerRef, isInteracting, commit]) + }, [enabled, petW, petH, loopMs, overlayOpen, containerRef, isInteracting, commit]) }