Extract default familiar display support seam

This commit is contained in:
OpenPets Dev 2026-06-19 08:03:15 +00:00
parent 655a7b2da1
commit 6a372c55e8
6 changed files with 144 additions and 57 deletions

View file

@ -44,6 +44,7 @@ const behaviorTests = [
".test-dist/tests/default-familiar-external-show.test.js",
".test-dist/tests/default-familiar-controller-bubbles.test.js",
".test-dist/tests/default-familiar-controller-motion.test.js",
".test-dist/tests/default-familiar-controller-display-support.test.js",
".test-dist/tests/default-familiar-controller-window.test.js",
".test-dist/tests/app-state-preferences.test.js",
".test-dist/tests/app-state-analytics.test.js",

View file

@ -222,6 +222,7 @@ const leaseManagerSource = readFileSync(join(appDir, "src", "lease-manager.ts"),
const defaultPetControllerSource = readFileSync(join(appDir, "src", "default-familiar-controller.ts"), "utf8");
const defaultPetControllerBubbleSource = readFileSync(join(appDir, "src", "default-familiar-controller-bubbles.ts"), "utf8");
const defaultPetControllerDisplaySource = readFileSync(join(appDir, "src", "default-familiar-controller-display.ts"), "utf8");
const defaultPetControllerDisplaySupportSource = readFileSync(join(appDir, "src", "default-familiar-controller-display-support.ts"), "utf8");
const defaultPetControllerMotionSource = readFileSync(join(appDir, "src", "default-familiar-controller-motion.ts"), "utf8");
const defaultPetControllerWindowSource = readFileSync(join(appDir, "src", "default-familiar-controller-window.ts"), "utf8");
const agentPetControllerSourceForLogging = readFileSync(join(appDir, "src", "agent-familiar-controller.ts"), "utf8");
@ -384,6 +385,7 @@ assert.match(petPreloadSource, /familiaros:familiar-ready/, "familiar preload mu
assert.match(petPreloadSource, /familiaros:familiar-drag-start/, "familiar preload must start controlled familiar dragging from the sprite.");
assert.match(petPreloadSource, /familiaros:familiar-open-prompt/, "familiar preload must request the floating prompt window on double-click.");
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-display(?:\.js)?"/, "default familiar controller must compose the extracted display seam.");
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-display-support(?:\.js)?"/, "default familiar controller must compose the extracted display-support seam.");
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-motion(?:\.js)?"/, "default familiar controller must compose the extracted motion seam.");
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-bubbles(?:\.js)?"/, "default familiar controller must compose the extracted bubble seam.");
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-window(?:\.js)?"/, "default familiar controller must compose the extracted window seam.");
@ -393,6 +395,7 @@ assert.match(defaultPetControllerBubbleSource, /refreshDefaultPetContent\(\)/, "
assert.match(defaultPetControllerDisplaySource, /export function setDefaultPetTransientDisplay/, "default familiar display seam must export transient display handling.");
assert.match(defaultPetControllerDisplaySource, /export function setDefaultPetStatusBadge/, "default familiar display seam must export status badge handling.");
assert.match(defaultPetControllerDisplaySource, /export function getDefaultPetDismissToken/, "default familiar display seam must export dismiss token shaping.");
assert.match(defaultPetControllerDisplaySupportSource, /export function createDefaultPetDisplaySupport/, "default familiar display-support seam must export the controller helper factory.");
assert.match(defaultPetControllerMotionSource, /export async function moveDefaultPetWindowBy/, "default familiar motion seam must export bounded plugin window movement.");
assert.match(defaultPetControllerMotionSource, /export function getPetMovementBlockedReason/, "default familiar motion seam must export movement block-state resolution.");
assert.match(defaultPetControllerWindowSource, /export function loadDefaultPetWindowContent/, "default familiar window seam must export content reload handling.");

View file

@ -253,6 +253,7 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `default-familiar-controller.ts`: Default familiar visibility, position persistence, transient reactions, status badges, logging, and display/motion/bubble/window-shell orchestration
- `default-familiar-controller-bubbles.ts`: Extracted default familiar plugin-bubble slot state, arbiter routing, and familiar-surface refresh/show callbacks
- `default-familiar-controller-display.ts`: Extracted transient display timers, status badge lifetimes, and dismiss-token shaping
- `default-familiar-controller-display-support.ts`: Extracted controller-local display helper factory for dismiss handling, badge wiring, and scale persistence
- `default-familiar-controller-motion.ts`: Extracted default familiar movement blocking and animated reposition helpers
- `default-familiar-controller-window.ts`: Extracted default familiar content reload, hide/destroy lifecycle, display handler wiring, bounds recovery, and resume/topology mouse-interop helpers
- `agent-familiar-controller.ts`: Lease-triggered familiar windows, dismissal tracking, transient displays, status badges, logging

View file

@ -0,0 +1,98 @@
import type { BrowserWindow } from "electron";
import {
clearDefaultPetDisplayState,
clearDefaultPetStatusBadge,
getDefaultPetDismissToken,
setDefaultPetStatusBadge,
setDefaultPetTransientDisplay,
type DefaultPetDisplayState,
} from "./default-familiar-controller-display.js";
import type { FamiliarOSReaction } from "./local-ipc-protocol.js";
import type { PetPluginBubbles, PetStatusBadgeReaction, PetTransientDisplay } from "./familiar-window.js";
interface DefaultPetBubbleDismissHandler {
handleDismissed: (dismissToken: string) => boolean;
}
interface DefaultPetDisplaySupportOptions {
readonly displayState: DefaultPetDisplayState;
readonly bubbleArbiter: DefaultPetBubbleDismissHandler;
readonly debug: (message: string, fields?: Record<string, unknown>) => void;
readonly refreshContent: () => void;
readonly getWindow: () => BrowserWindow | null;
readonly getPaused: () => boolean;
readonly getPluginBubbles: () => PetPluginBubbles | null;
readonly loadContent: (
window: BrowserWindow,
paused: boolean,
display: PetTransientDisplay | null,
badge: PetStatusBadgeReaction | null,
dismissToken?: string,
pluginBubbles?: PetPluginBubbles | null,
) => Promise<void>;
readonly updateScalePreference: (scale: number) => void | Promise<void>;
}
export function createDefaultPetDisplaySupport(options: DefaultPetDisplaySupportOptions) {
function getDefaultPetDisplayDeps() {
return {
debug: options.debug,
refreshContent: options.refreshContent,
getWindowForReaction: () => options.getWindow(),
};
}
function setTransientDisplay(display: PetTransientDisplay): void {
setDefaultPetTransientDisplay(options.displayState, display, getDefaultPetDisplayDeps());
}
function setStatusBadge(reaction: FamiliarOSReaction): void {
setDefaultPetStatusBadge(options.displayState, reaction, getDefaultPetDisplayDeps());
}
function clearStatusBadge(): void {
clearDefaultPetStatusBadge(options.displayState, getDefaultPetDisplayDeps());
}
function clearDefaultPetDisplayTimers(): void {
clearDefaultPetDisplayState(options.displayState);
}
function getCurrentDismissToken(): string | undefined {
return getDefaultPetDismissToken(options.displayState);
}
function handleBubbleDismissed(dismissToken: string): void {
options.debug("bubble dismissed callback", { windowId: options.getWindow()?.id, dismissToken, currentGeneration: options.displayState.displayGeneration });
if (options.bubbleArbiter.handleDismissed(dismissToken)) {
return;
}
if (dismissToken !== String(options.displayState.displayGeneration)) {
options.debug("bubble dismissed stale token", { dismissToken, currentGeneration: options.displayState.displayGeneration });
return;
}
clearDefaultPetDisplayTimers();
const window = options.getWindow();
if (window) {
void options.loadContent(window, options.getPaused(), null, null, undefined, options.getPluginBubbles());
}
}
function handleScaleChanged(scale: number): void {
options.debug("scale changed", { scale });
void Promise.resolve(options.updateScalePreference(scale)).catch((error: unknown) => {
console.error("Failed to update familiar scale preference.", error);
});
}
return {
clearDefaultPetDisplayTimers,
clearStatusBadge,
getCurrentDismissToken,
handleBubbleDismissed,
handleScaleChanged,
setStatusBadge,
setTransientDisplay,
};
}

View file

@ -3,13 +3,9 @@ import { BrowserWindow } from "electron";
import { getAppStateSnapshot, getDefaultPetPosition, resetDefaultPetPosition, setDefaultPetPosition, updatePreferences } from "./app-state.js";
import { shouldShowDefaultPetForExternalEvent } from "./app-state-core.js";
import {
clearDefaultPetDisplayState,
clearDefaultPetStatusBadge,
createDefaultPetDisplayState,
getDefaultPetDismissToken,
setDefaultPetStatusBadge,
setDefaultPetTransientDisplay,
} from "./default-familiar-controller-display.js";
import { createDefaultPetDisplaySupport } from "./default-familiar-controller-display-support.js";
import { getPetMovementBlockedReason, moveDefaultPetWindowBy } from "./default-familiar-controller-motion.js";
import { createDefaultPetBubbleRuntime } from "./default-familiar-controller-bubbles.js";
import {
@ -52,6 +48,28 @@ const defaultPetBubbleRuntime = createDefaultPetBubbleRuntime({
showDefaultPetForExternalEvent: () => showDefaultPetForExternalEvent(),
refreshDefaultPetContent: () => refreshDefaultPetContent(),
});
const defaultPetDisplaySupport = createDefaultPetDisplaySupport({
displayState,
bubbleArbiter: defaultPetBubbleRuntime,
debug: (message, fields) => debug("familiar.default", message, fields),
refreshContent: () => refreshDefaultPetContent(),
getWindow: () => (defaultPetWindow && !defaultPetWindow.isDestroyed() ? defaultPetWindow : null),
getPaused: () => paused,
getPluginBubbles: () => getDefaultPetPluginBubbles(),
loadContent: (window, pausedState, display, badge, dismissToken, pluginBubbles) => loadDefaultPetContent(window, pausedState, display, badge, dismissToken, pluginBubbles),
updateScalePreference: (scale) => import("./app-state.js").then(({ updatePreferences }) => {
updatePreferences({ petScale: scale });
}),
});
const {
clearDefaultPetDisplayTimers,
clearStatusBadge,
getCurrentDismissToken,
handleBubbleDismissed,
handleScaleChanged,
setStatusBadge,
setTransientDisplay,
} = defaultPetDisplaySupport;
/** The default familiar's bubble arbiter — the Electron bubbles capability targets this. */
export const defaultPetBubbleArbiter = defaultPetBubbleRuntime.arbiter;
@ -218,30 +236,6 @@ export function getDefaultPetWindowBounds(): Electron.Rectangle | null {
return getManagedDefaultPetWindowBounds(defaultPetWindow);
}
function handleBubbleDismissed(dismissToken: string): void {
debug("familiar.default", "bubble dismissed callback", { windowId: defaultPetWindow?.id, dismissToken, currentGeneration: displayState.displayGeneration });
if (defaultPetBubbleRuntime.handleDismissed(dismissToken)) {
return;
}
if (dismissToken !== String(displayState.displayGeneration)) {
debug("familiar.default", "bubble dismissed stale token", { dismissToken, currentGeneration: displayState.displayGeneration });
return;
}
clearDefaultPetDisplayTimers();
if (defaultPetWindow && !defaultPetWindow.isDestroyed()) {
void loadDefaultPetContent(defaultPetWindow, paused, null, null, undefined, getDefaultPetPluginBubbles());
}
}
function handleScaleChanged(scale: number): void {
debug("familiar.default", "scale changed", { scale });
import("./app-state.js").then(({ updatePreferences }) => {
updatePreferences({ petScale: scale });
}).catch((error) => {
console.error("Failed to update familiar scale preference.", error);
});
}
function getOrCreateDefaultPetWindow(): BrowserWindow {
if (defaultPetWindow && !defaultPetWindow.isDestroyed()) {
return defaultPetWindow;
@ -275,10 +269,6 @@ function getOrCreateDefaultPetWindow(): BrowserWindow {
return defaultPetWindow;
}
function setTransientDisplay(display: PetTransientDisplay): void {
setDefaultPetTransientDisplay(displayState, display, getDefaultPetDisplayDeps());
}
function showDefaultPetForExternalEvent(): void {
const state = getAppStateSnapshot();
const visible = isDefaultPetVisible();
@ -327,22 +317,6 @@ function getMovementBlockedReason(window: BrowserWindow, allowMoving = false): s
});
}
function setStatusBadge(reaction: FamiliarOSReaction): void {
setDefaultPetStatusBadge(displayState, reaction, getDefaultPetDisplayDeps());
}
function clearStatusBadge(): void {
clearDefaultPetStatusBadge(displayState, getDefaultPetDisplayDeps());
}
function clearDefaultPetDisplayTimers(): void {
clearDefaultPetDisplayState(displayState);
}
function getCurrentDismissToken(): string | undefined {
return getDefaultPetDismissToken(displayState);
}
function reclampDefaultPetWindow(): void {
reclampManagedDefaultPetWindow(defaultPetWindow, recoverDefaultPetMouseInterop);
}
@ -358,11 +332,3 @@ export function shouldOpenDefaultPetOnLaunch(): boolean {
export function resetDefaultPetToInitialPosition(): void {
resetManagedDefaultPetWindowToInitialPosition(defaultPetWindow);
}
function getDefaultPetDisplayDeps() {
return {
debug: (message: string, fields?: Record<string, unknown>) => debug("familiar.default", message, fields),
refreshContent: refreshDefaultPetContent,
getWindowForReaction: () => (defaultPetWindow && !defaultPetWindow.isDestroyed() ? defaultPetWindow : null),
};
}

View file

@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
const controllerSource = readFileSync(resolve(desktopRoot, "src/default-familiar-controller.ts"), "utf8");
const controllerDisplaySupportSource = readFileSync(resolve(desktopRoot, "src/default-familiar-controller-display-support.ts"), "utf8");
assert.match(controllerSource, /from "\.\/default-familiar-controller-display-support(?:\.js)?"/, "default familiar controller must compose the extracted display-support seam.");
assert.match(controllerDisplaySupportSource, /export function createDefaultPetDisplaySupport/, "default familiar display-support seam must export the display helper factory.");
assert.match(controllerDisplaySupportSource, /setDefaultPetTransientDisplay\(options\.displayState, display, getDefaultPetDisplayDeps\(\)\)/, "default familiar display-support seam must drive transient display updates through the shared display seam.");
assert.match(controllerDisplaySupportSource, /setDefaultPetStatusBadge\(options\.displayState, reaction, getDefaultPetDisplayDeps\(\)\)/, "default familiar display-support seam must drive status badges through the shared display seam.");
assert.match(controllerDisplaySupportSource, /if \(options\.bubbleArbiter\.handleDismissed\(dismissToken\)\) \{\s*return;/, "default familiar display-support seam must short-circuit plugin-managed bubble dismissals.");
assert.match(controllerDisplaySupportSource, /void options\.loadContent\(window, options\.getPaused\(\), null, null, undefined, options\.getPluginBubbles\(\)\);/, "default familiar display-support seam must reload cleared familiar content after dismissing a live transient bubble.");
assert.match(controllerDisplaySupportSource, /Promise\.resolve\(options\.updateScalePreference\(scale\)\)/, "default familiar display-support seam must persist scale changes through the injected preference updater.");
console.error("Default familiar display-support seam validation passed.");