From 5ae609429c960c74beffa5754342dc8b23aee028 Mon Sep 17 00:00:00 2001 From: Alvin Unreal Date: Sat, 16 May 2026 16:13:57 +0200 Subject: [PATCH] Fix agent pet scale rendering --- apps/desktop/src/agent-pet-controller.ts | 22 +++++++++++++------- apps/desktop/src/check-packaging-contract.ts | 5 +++++ apps/desktop/src/pet-window.ts | 8 ++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/agent-pet-controller.ts b/apps/desktop/src/agent-pet-controller.ts index c5bd965e..d75d7d9a 100644 --- a/apps/desktop/src/agent-pet-controller.ts +++ b/apps/desktop/src/agent-pet-controller.ts @@ -1,6 +1,6 @@ import { BrowserWindow } from "electron"; -import { getAppStateSnapshot } from "./app-state.js"; +import { getAppStateSnapshot, type PetScaleValue } from "./app-state.js"; import { defaultPetWindowSize, getDefaultPetInitialPosition } from "./display.js"; import { debug, info } from "./logger.js"; import { transientDisplayMs, type OpenPetsReaction } from "./local-ipc-protocol.js"; @@ -84,11 +84,12 @@ export function closeAllAgentPets(): void { export function refreshAgentPetContent(): void { debug("pet.agent", "refresh all content", { activeWindows: agentPetWindows.size, petIds: [...agentPetWindows.keys()] }); + const scale = getPreferredPetScale(); for (const [petId, window] of agentPetWindows.entries()) { if (!window.isDestroyed()) { const display = transientDisplays.get(petId) ?? null; const badge = statusBadges.get(petId) ?? null; - void loadExplicitPetContent(window, petId, display, badge, getCurrentDismissToken(petId, display, badge)); + void loadExplicitPetContent(window, petId, display, badge, getCurrentDismissToken(petId, display, badge), scale); } } } @@ -103,7 +104,7 @@ function handleBubbleDismissed(petId: string, dismissToken: string): void { clearAgentDisplay(petId); const window = agentPetWindows.get(petId); if (window && !window.isDestroyed()) { - void loadExplicitPetContent(window, petId, null, null); + void loadExplicitPetContent(window, petId, null, null, undefined, getPreferredPetScale()); } } @@ -114,7 +115,9 @@ function getOrCreateAgentPetWindow(petId: string): BrowserWindow { return existing; } - const pet = getAppStateSnapshot().pets.installed.find((candidate) => candidate.id === petId); + const state = getAppStateSnapshot(); + const scale = state.preferences.petScale as PetScaleValue; + const pet = state.pets.installed.find((candidate) => candidate.id === petId); if (!pet) throw new Error(`Installed pet is unavailable: ${petId}`); const offset = agentPetWindows.size + 1; const initial = getDefaultPetInitialPosition(defaultPetWindowSize); @@ -123,6 +126,7 @@ function getOrCreateAgentPetWindow(petId: string): BrowserWindow { const window = createAgentPetWindow({ petId, displayName: pet.displayName, + scale, position: { x: initial.x - offset * 36, y: initial.y - offset * 24 }, display, badge, @@ -177,12 +181,12 @@ function setAgentDisplay(petId: string, display: PetTransientDisplay): void { const window = agentPetWindows.get(petId); if (window && !window.isDestroyed()) { const badge = statusBadges.get(petId) ?? null; - void loadExplicitPetContent(window, petId, null, badge, getCurrentDismissToken(petId, null, badge)); + void loadExplicitPetContent(window, petId, null, badge, getCurrentDismissToken(petId, null, badge), getPreferredPetScale()); } }, displayDurationMs); transientTimers.set(petId, timer); const window = agentPetWindows.get(petId); - if (window && !window.isDestroyed()) void loadExplicitPetContent(window, petId, preparedDisplay, statusBadges.get(petId) ?? null, preparedDisplay.dismissToken); + if (window && !window.isDestroyed()) void loadExplicitPetContent(window, petId, preparedDisplay, statusBadges.get(petId) ?? null, preparedDisplay.dismissToken, getPreferredPetScale()); } function clearAgentDisplay(petId: string): void { @@ -226,7 +230,7 @@ function setStatusBadge(petId: string, reaction: OpenPetsReaction): void { const window = agentPetWindows.get(petId); if (window && !window.isDestroyed()) { const display = transientDisplays.get(petId) ?? null; - void loadExplicitPetContent(window, petId, display, null, getCurrentDismissToken(petId, display, null)); + void loadExplicitPetContent(window, petId, display, null, getCurrentDismissToken(petId, display, null), getPreferredPetScale()); } }, isBusyStatusBadgeReaction(reaction) ? busyStatusBadgeMs : transientDisplayMs); statusBadgeTimers.set(petId, timer); @@ -247,3 +251,7 @@ function isBusyStatusBadgeReaction(reaction: OpenPetsReaction): boolean { function getCurrentDismissToken(petId: string, display: PetTransientDisplay | null, badge: PetStatusBadgeReaction | null): string | undefined { return display?.dismissToken ?? (badge ? String(displayGenerations.get(petId) ?? 0) : undefined); } + +function getPreferredPetScale(): PetScaleValue { + return getAppStateSnapshot().preferences.petScale as PetScaleValue; +} diff --git a/apps/desktop/src/check-packaging-contract.ts b/apps/desktop/src/check-packaging-contract.ts index 6ab8e034..029ca2b1 100644 --- a/apps/desktop/src/check-packaging-contract.ts +++ b/apps/desktop/src/check-packaging-contract.ts @@ -213,6 +213,11 @@ assert.match(petWindowSource, /html\[data-motion-state=\"\$\{motion\}\"\] \$\{se assert.match(petWindowSource, /\.sprite, \.installed-sprite, \.bubble/, "reduced-motion CSS must include built-in and installed sprites."); assert.match(petWindowSource, /function createAgentPetWindow[\s\S]*?installMotionStatePublisher\(window\)/, "agent pet windows must publish motion state so dragged non-default pets run."); assert.match(petWindowSource, /loadExplicitPetContent[\s\S]*?state\.preferences\.petScale/, "explicit agent pet windows must use the saved pet scale preference."); +assert.match(petWindowSource, /interface AgentPetWindowOptions[\s\S]*?readonly scale: PetScaleValue/, "new agent pet windows must receive the current pet scale explicitly for their first render."); +assert.match(petWindowSource, /loadExplicitPetContent\(window, options\.petId, options\.display, options\.badge, dismissToken, options\.scale\)/, "agent pet first render must not fall back to the medium default scale."); +assert.match(agentPetControllerSourceForLogging, /function getPreferredPetScale\(\): PetScaleValue/, "agent pet reloads must share one explicit saved scale helper."); +assert.match(agentPetControllerSourceForLogging, /loadExplicitPetContent\(window, petId, display, badge, getCurrentDismissToken\(petId, display, badge\), scale\)/, "agent pet refreshes must pass the saved pet scale explicitly."); +assert.match(agentPetControllerSourceForLogging, /loadExplicitPetContent\(window, petId, preparedDisplay, statusBadges\.get\(petId\) \?\? null, preparedDisplay\.dismissToken, getPreferredPetScale\(\)\)/, "agent pet transient updates must pass the saved pet scale explicitly."); assert.match(mappingDoc, /\| 3 \| `waving` \| `waving`, Claude `Notification`\. \|/, "mapping docs must describe waving animation row and notification mapping."); assert.match(mappingDoc, /reaction-animation-mapping\.ts/, "mapping docs must reference the shared reaction animation mapping source of truth."); assert.match(mappingDoc, /overrid/i, "mapping docs must mention that reaction animation defaults can be overridden in Settings."); diff --git a/apps/desktop/src/pet-window.ts b/apps/desktop/src/pet-window.ts index 89475c34..23cf50a7 100644 --- a/apps/desktop/src/pet-window.ts +++ b/apps/desktop/src/pet-window.ts @@ -25,6 +25,7 @@ export interface DefaultPetWindowOptions { export interface AgentPetWindowOptions { readonly petId: string; readonly displayName: string; + readonly scale: PetScaleValue; readonly position: Point; readonly display: PetTransientDisplay | null; readonly badge: PetStatusBadgeReaction | null; @@ -86,7 +87,7 @@ export function createAgentPetWindow(options: AgentPetWindowOptions, dismissToke installMousePassthroughAndDrag(window, options.onBubbleDismissed); installMotionStatePublisher(window); installPetContextMenu(window, { label: "Close pet", click: options.onCloseRequested }); - void loadExplicitPetContent(window, options.petId, options.display, options.badge, dismissToken); + void loadExplicitPetContent(window, options.petId, options.display, options.badge, dismissToken, options.scale); return window; } @@ -378,7 +379,7 @@ export async function loadDefaultPetContent(window: BrowserWindow, paused: boole }); } -export async function loadExplicitPetContent(window: BrowserWindow, petId: string, display: PetTransientDisplay | null = null, badge: PetStatusBadgeReaction | null = null, dismissToken?: string): Promise { +export async function loadExplicitPetContent(window: BrowserWindow, petId: string, display: PetTransientDisplay | null = null, badge: PetStatusBadgeReaction | null = null, dismissToken?: string, scaleOverride?: PetScaleValue): Promise { const sequence = allocateWindowLoadSequence(window); try { const state = getAppStateSnapshot(); @@ -387,7 +388,8 @@ export async function loadExplicitPetContent(window: BrowserWindow, petId: strin throw new Error(`Cannot render explicit pet: ${petId}`); } debug("pet.window", "explicit content render begin", { windowId: window.id, sequence, petId, displayName: pet.displayName, hasDisplay: Boolean(display), reaction: display?.reaction, hasMessage: Boolean(display?.message), badge }); - const render = await createInstalledPetRender(pet.id, pet.displayName, false, display, state.preferences.petScale as PetScaleValue, badge, `explicit:${pet.id}`, dismissToken); + const scale = scaleOverride ?? state.preferences.petScale as PetScaleValue; + const render = await createInstalledPetRender(pet.id, pet.displayName, false, display, scale, badge, `explicit:${pet.id}`, dismissToken); if (tryUpdateLoadedPetContent(window, render, `explicit-${pet.id}`, sequence)) return; await loadPetHtmlFile(window, render.html, `explicit-${pet.id}`, sequence); petWindowRenderCache.set(window, render.cacheKey);