Extract default familiar display support seam
This commit is contained in:
parent
655a7b2da1
commit
6a372c55e8
6 changed files with 144 additions and 57 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
Loading…
Add table
Reference in a new issue