refactor(memory-core): rename sleep surface back to dreaming

This commit is contained in:
Vincent Koc
2026-04-05 18:13:49 +01:00
parent 848cc5e0ce
commit 8ff41a6bc4
27 changed files with 258 additions and 2251 deletions

View File

@@ -66,9 +66,9 @@ import {
} from "./controllers/devices.ts";
import {
loadDreamingStatus,
updateSleepEnabled,
updateSleepPhaseEnabled,
type SleepPhaseId,
updateDreamingEnabled,
updateDreamingPhaseEnabled,
type DreamingPhaseId,
} from "./controllers/dreaming.ts";
import {
loadExecApprovals,
@@ -149,16 +149,16 @@ const lazyLogs = createLazy(() => import("./views/logs.ts"));
const lazyNodes = createLazy(() => import("./views/nodes.ts"));
const lazySessions = createLazy(() => import("./views/sessions.ts"));
const lazySkills = createLazy(() => import("./views/skills.ts"));
const lazyDreams = createLazy(() => import("./views/dreams.ts"));
const SLEEP_PHASE_OPTIONS: Array<{ id: SleepPhaseId; label: string; detail: string }> = [
const lazyDreamingView = createLazy(() => import("./views/dreaming.ts"));
const DREAMING_PHASE_OPTIONS: Array<{ id: DreamingPhaseId; label: string; detail: string }> = [
{ id: "light", label: "Light", detail: "sort and stage the day" },
{ id: "deep", label: "Deep", detail: "promote durable memory" },
{ id: "rem", label: "REM", detail: "surface themes and reflections" },
];
function resolveConfiguredSleep(configValue: Record<string, unknown> | null): {
function resolveConfiguredDreaming(configValue: Record<string, unknown> | null): {
enabled: boolean;
phases: Record<SleepPhaseId, boolean>;
phases: Record<DreamingPhaseId, boolean>;
} {
if (!configValue) {
return {
@@ -174,13 +174,13 @@ function resolveConfiguredSleep(configValue: Record<string, unknown> | null): {
const entries = plugins?.entries as Record<string, unknown> | undefined;
const memoryCore = entries?.["memory-core"] as Record<string, unknown> | undefined;
const config = memoryCore?.config as Record<string, unknown> | undefined;
const sleep = config?.sleep as Record<string, unknown> | undefined;
const phases = sleep?.phases as Record<string, unknown> | undefined;
const dreaming = config?.dreaming as Record<string, unknown> | undefined;
const phases = dreaming?.phases as Record<string, unknown> | undefined;
const light = phases?.light as Record<string, unknown> | undefined;
const deep = phases?.deep as Record<string, unknown> | undefined;
const rem = phases?.rem as Record<string, unknown> | undefined;
return {
enabled: typeof sleep?.enabled === "boolean" ? sleep.enabled : true,
enabled: typeof dreaming?.enabled === "boolean" ? dreaming.enabled : true,
phases: {
light: typeof light?.enabled === "boolean" ? light.enabled : true,
deep: typeof deep?.enabled === "boolean" ? deep.enabled : true,
@@ -199,8 +199,8 @@ function formatDreamNextCycle(nextRunAtMs: number | undefined): string | null {
});
}
function resolveSleepNextCycle(
status: { phases: Record<SleepPhaseId, { enabled: boolean; nextRunAtMs?: number }> } | null,
function resolveDreamingNextCycle(
status: { phases: Record<DreamingPhaseId, { enabled: boolean; nextRunAtMs?: number }> } | null,
): string | null {
if (!status) {
return null;
@@ -397,17 +397,17 @@ export function renderApp(state: AppViewState) {
const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null;
const configValue =
state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
const configuredSleep = resolveConfiguredSleep(configValue);
const dreamingOn = state.dreamingStatus?.enabled ?? configuredSleep.enabled;
const dreamingNextCycle = resolveSleepNextCycle(state.dreamingStatus);
const configuredDreaming = resolveConfiguredDreaming(configValue);
const dreamingOn = state.dreamingStatus?.enabled ?? configuredDreaming.enabled;
const dreamingNextCycle = resolveDreamingNextCycle(state.dreamingStatus);
const dreamingLoading = state.dreamingStatusLoading || state.dreamingModeSaving;
const refreshDreamingStatus = () => loadDreamingStatus(state);
const applySleepEnabled = (enabled: boolean) => {
const applyDreamingEnabled = (enabled: boolean) => {
if (state.dreamingModeSaving || dreamingOn === enabled) {
return;
}
void (async () => {
const updated = await updateSleepEnabled(state, enabled);
const updated = await updateDreamingEnabled(state, enabled);
if (!updated) {
return;
}
@@ -415,17 +415,17 @@ export function renderApp(state: AppViewState) {
await loadDreamingStatus(state);
})();
};
const applySleepPhaseEnabled = (phase: SleepPhaseId, enabled: boolean) => {
const applyDreamingPhaseEnabled = (phase: DreamingPhaseId, enabled: boolean) => {
if (state.dreamingModeSaving) {
return;
}
const currentEnabled =
state.dreamingStatus?.phases[phase].enabled ?? configuredSleep.phases[phase];
state.dreamingStatus?.phases[phase].enabled ?? configuredDreaming.phases[phase];
if (currentEnabled === enabled) {
return;
}
void (async () => {
const updated = await updateSleepPhaseEnabled(state, phase, enabled);
const updated = await updateDreamingPhaseEnabled(state, phase, enabled);
if (!updated) {
return;
}
@@ -740,23 +740,19 @@ export function renderApp(state: AppViewState) {
<div
class="dreaming-header-controls__modes"
role="group"
aria-label="Sleep controls"
aria-label="Dreaming controls"
>
<button
class="dreaming-header-controls__mode ${dreamingOn
? "dreaming-header-controls__mode--active"
: ""}"
?disabled=${dreamingLoading}
title=${dreamingOn
? "Sleep maintenance is enabled."
: "Sleep maintenance is disabled."}
aria-label=${dreamingOn
? "Disable sleep maintenance"
: "Enable sleep maintenance"}
@click=${() => applySleepEnabled(!dreamingOn)}
title=${dreamingOn ? "Dreaming is enabled." : "Dreaming is disabled."}
aria-label=${dreamingOn ? "Disable dreaming" : "Enable dreaming"}
@click=${() => applyDreamingEnabled(!dreamingOn)}
>
<span class="dreaming-header-controls__mode-label"
>${dreamingOn ? "Sleep On" : "Sleep Off"}</span
>${dreamingOn ? "Dreaming On" : "Dreaming Off"}</span
>
<span class="dreaming-header-controls__mode-detail"
>${dreamingOn ? "all phases may run" : "no phases will run"}</span
@@ -2149,8 +2145,8 @@ export function renderApp(state: AppViewState) {
)
: nothing}
${state.tab === "dreams"
? lazyRender(lazyDreams, (m) =>
m.renderDreams({
? lazyRender(lazyDreamingView, (m) =>
m.renderDreaming({
active: dreamingOn,
shortTermCount: state.dreamingStatus?.shortTermCount ?? 0,
longTermCount: state.dreamingStatus?.promotedTotal ?? 0,
@@ -2158,11 +2154,11 @@ export function renderApp(state: AppViewState) {
dreamingOf: null,
nextCycle: dreamingNextCycle,
timezone: state.dreamingStatus?.timezone ?? null,
phases: SLEEP_PHASE_OPTIONS.map((phase) => ({
phases: DREAMING_PHASE_OPTIONS.map((phase) => ({
...phase,
enabled:
state.dreamingStatus?.phases[phase.id].enabled ??
configuredSleep.phases[phase.id],
configuredDreaming.phases[phase.id],
nextCycle: formatDreamNextCycle(
state.dreamingStatus?.phases[phase.id].nextRunAtMs,
),
@@ -2173,8 +2169,8 @@ export function renderApp(state: AppViewState) {
statusError: state.dreamingStatusError,
modeSaving: state.dreamingModeSaving,
onRefresh: refreshDreamingStatus,
onToggleEnabled: applySleepEnabled,
onTogglePhase: applySleepPhaseEnabled,
onToggleEnabled: applyDreamingEnabled,
onTogglePhase: applyDreamingPhaseEnabled,
}),
)
: nothing}

View File

@@ -1,8 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import {
loadDreamingStatus,
updateSleepEnabled,
updateSleepPhaseEnabled,
updateDreamingEnabled,
updateDreamingPhaseEnabled,
type DreamingState,
} from "./dreaming.ts";
@@ -24,11 +24,11 @@ function createState(): { state: DreamingState; request: ReturnType<typeof vi.fn
return { state, request };
}
describe("sleep controller", () => {
it("loads and normalizes sleep status from doctor.memory.status", async () => {
describe("dreaming controller", () => {
it("loads and normalizes dreaming status from doctor.memory.status", async () => {
const { state, request } = createState();
request.mockResolvedValue({
sleep: {
dreaming: {
enabled: true,
timezone: "America/Los_Angeles",
verboseLogging: false,
@@ -91,11 +91,11 @@ describe("sleep controller", () => {
expect(state.dreamingStatusError).toBeNull();
});
it("patches config to update global sleep enablement", async () => {
it("patches config to update global dreaming enablement", async () => {
const { state, request } = createState();
request.mockResolvedValue({ ok: true });
const ok = await updateSleepEnabled(state, false);
const ok = await updateDreamingEnabled(state, false);
expect(ok).toBe(true);
expect(request).toHaveBeenCalledWith(
@@ -113,7 +113,7 @@ describe("sleep controller", () => {
const { state, request } = createState();
request.mockResolvedValue({ ok: true });
const ok = await updateSleepPhaseEnabled(state, "rem", false);
const ok = await updateDreamingPhaseEnabled(state, "rem", false);
expect(ok).toBe(true);
expect(request).toHaveBeenCalledWith(
@@ -128,7 +128,7 @@ describe("sleep controller", () => {
const { state, request } = createState();
state.configSnapshot = {};
const ok = await updateSleepEnabled(state, true);
const ok = await updateDreamingEnabled(state, true);
expect(ok).toBe(false);
expect(request).not.toHaveBeenCalled();

View File

@@ -1,21 +1,21 @@
import type { GatewayBrowserClient } from "../gateway.ts";
import type { ConfigSnapshot } from "../types.ts";
export type SleepPhaseId = "light" | "deep" | "rem";
export type DreamingPhaseId = "light" | "deep" | "rem";
type SleepPhaseStatusBase = {
type DreamingPhaseStatusBase = {
enabled: boolean;
cron: string;
managedCronPresent: boolean;
nextRunAtMs?: number;
};
type LightSleepStatus = SleepPhaseStatusBase & {
type LightDreamingStatus = DreamingPhaseStatusBase & {
lookbackDays: number;
limit: number;
};
type DeepSleepStatus = SleepPhaseStatusBase & {
type DeepDreamingStatus = DreamingPhaseStatusBase & {
limit: number;
minScore: number;
minRecallCount: number;
@@ -24,13 +24,13 @@ type DeepSleepStatus = SleepPhaseStatusBase & {
maxAgeDays?: number;
};
type RemSleepStatus = SleepPhaseStatusBase & {
type RemDreamingStatus = DreamingPhaseStatusBase & {
lookbackDays: number;
limit: number;
minPatternStrength: number;
};
export type SleepStatus = {
export type DreamingStatus = {
enabled: boolean;
timezone?: string;
verboseLogging: boolean;
@@ -42,14 +42,14 @@ export type SleepStatus = {
storePath?: string;
storeError?: string;
phases: {
light: LightSleepStatus;
deep: DeepSleepStatus;
rem: RemSleepStatus;
light: LightDreamingStatus;
deep: DeepDreamingStatus;
rem: RemDreamingStatus;
};
};
type DoctorMemoryStatusPayload = {
sleep?: unknown;
dreaming?: unknown;
};
export type DreamingState = {
@@ -59,7 +59,7 @@ export type DreamingState = {
applySessionKey: string;
dreamingStatusLoading: boolean;
dreamingStatusError: string | null;
dreamingStatus: SleepStatus | null;
dreamingStatus: DreamingStatus | null;
dreamingModeSaving: boolean;
lastError: string | null;
};
@@ -97,7 +97,7 @@ function normalizeFiniteScore(value: unknown, fallback = 0): number {
return Math.max(0, Math.min(1, value));
}
function normalizeStorageMode(value: unknown): SleepStatus["storageMode"] {
function normalizeStorageMode(value: unknown): DreamingStatus["storageMode"] {
const normalized = normalizeTrimmedString(value)?.toLowerCase();
if (normalized === "inline" || normalized === "separate" || normalized === "both") {
return normalized;
@@ -109,7 +109,7 @@ function normalizeNextRun(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function normalizePhaseStatusBase(record: Record<string, unknown> | null): SleepPhaseStatusBase {
function normalizePhaseStatusBase(record: Record<string, unknown> | null): DreamingPhaseStatusBase {
return {
enabled: normalizeBoolean(record?.enabled, false),
cron: normalizeTrimmedString(record?.cron) ?? "",
@@ -120,7 +120,7 @@ function normalizePhaseStatusBase(record: Record<string, unknown> | null): Sleep
};
}
function normalizeSleepStatus(raw: unknown): SleepStatus | null {
function normalizeDreamingStatus(raw: unknown): DreamingStatus | null {
const record = asRecord(raw);
if (!record) {
return null;
@@ -182,7 +182,7 @@ export async function loadDreamingStatus(state: DreamingState): Promise<void> {
"doctor.memory.status",
{},
);
state.dreamingStatus = normalizeSleepStatus(payload?.sleep);
state.dreamingStatus = normalizeDreamingStatus(payload?.dreaming);
} catch (err) {
state.dreamingStatusError = String(err);
} finally {
@@ -190,7 +190,7 @@ export async function loadDreamingStatus(state: DreamingState): Promise<void> {
}
}
async function writeSleepPatch(
async function writeDreamingPatch(
state: DreamingState,
patch: Record<string, unknown>,
): Promise<boolean> {
@@ -213,7 +213,7 @@ async function writeSleepPatch(
baseHash,
raw: JSON.stringify(patch),
sessionKey: state.applySessionKey,
note: "Sleep settings updated from Dreams tab.",
note: "Dreaming settings updated from the Dreaming tab.",
});
return true;
} catch (err) {
@@ -226,13 +226,16 @@ async function writeSleepPatch(
}
}
export async function updateSleepEnabled(state: DreamingState, enabled: boolean): Promise<boolean> {
const ok = await writeSleepPatch(state, {
export async function updateDreamingEnabled(
state: DreamingState,
enabled: boolean,
): Promise<boolean> {
const ok = await writeDreamingPatch(state, {
plugins: {
entries: {
"memory-core": {
config: {
sleep: {
dreaming: {
enabled,
},
},
@@ -249,17 +252,17 @@ export async function updateSleepEnabled(state: DreamingState, enabled: boolean)
return ok;
}
export async function updateSleepPhaseEnabled(
export async function updateDreamingPhaseEnabled(
state: DreamingState,
phase: SleepPhaseId,
phase: DreamingPhaseId,
enabled: boolean,
): Promise<boolean> {
const ok = await writeSleepPatch(state, {
const ok = await writeDreamingPatch(state, {
plugins: {
entries: {
"memory-core": {
config: {
sleep: {
dreaming: {
phases: {
[phase]: {
enabled,
@@ -285,5 +288,3 @@ export async function updateSleepPhaseEnabled(
}
return ok;
}
export type DreamingStatus = SleepStatus;

View File

@@ -88,7 +88,7 @@ describe("control UI routing", () => {
const app = mountApp("/chat");
await app.updateComplete;
const dreamsLink = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/dreams"]');
const dreamsLink = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/dreaming"]');
expect(dreamsLink).not.toBeNull();
});

View File

@@ -63,7 +63,7 @@ const TAB_PATHS: Record<Tab, string> = {
aiAgents: "/ai-agents",
debug: "/debug",
logs: "/logs",
dreams: "/dreams",
dreams: "/dreaming",
};
const PATH_TO_TAB = new Map(Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]));

View File

@@ -1,151 +0,0 @@
/* @vitest-environment jsdom */
import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
import { renderDreams, type DreamsProps } from "./dreams.ts";
function buildProps(overrides?: Partial<DreamsProps>): DreamsProps {
return {
active: true,
shortTermCount: 47,
longTermCount: 182,
promotedCount: 12,
dreamingOf: null,
nextCycle: "4:00 AM",
timezone: "America/Los_Angeles",
phases: [
{
id: "light",
label: "Light",
detail: "sort and stage the day",
enabled: true,
nextCycle: "1:00 AM",
managedCronPresent: true,
},
{
id: "deep",
label: "Deep",
detail: "promote durable memory",
enabled: true,
nextCycle: "3:00 AM",
managedCronPresent: true,
},
{
id: "rem",
label: "REM",
detail: "surface themes and reflections",
enabled: false,
nextCycle: null,
managedCronPresent: false,
},
],
statusLoading: false,
statusError: null,
modeSaving: false,
onRefresh: () => {},
onToggleEnabled: () => {},
onTogglePhase: () => {},
...overrides,
};
}
function renderInto(props: DreamsProps): HTMLDivElement {
const container = document.createElement("div");
render(renderDreams(props), container);
return container;
}
describe("dreams view", () => {
it("renders the sleeping lobster SVG", () => {
const container = renderInto(buildProps());
const svg = container.querySelector(".dreams__lobster svg");
expect(svg).not.toBeNull();
});
it("shows three floating Z elements", () => {
const container = renderInto(buildProps());
const zs = container.querySelectorAll(".dreams__z");
expect(zs.length).toBe(3);
});
it("renders stars", () => {
const container = renderInto(buildProps());
const stars = container.querySelectorAll(".dreams__star");
expect(stars.length).toBe(12);
});
it("renders moon", () => {
const container = renderInto(buildProps());
expect(container.querySelector(".dreams__moon")).not.toBeNull();
});
it("displays memory stats", () => {
const container = renderInto(buildProps());
const values = container.querySelectorAll(".dreams__stat-value");
expect(values.length).toBe(3);
expect(values[0]?.textContent).toBe("47");
expect(values[1]?.textContent).toBe("182");
expect(values[2]?.textContent).toBe("12");
});
it("shows dream bubble when active", () => {
const container = renderInto(buildProps({ active: true }));
expect(container.querySelector(".dreams__bubble")).not.toBeNull();
});
it("hides dream bubble when idle", () => {
const container = renderInto(buildProps({ active: false }));
expect(container.querySelector(".dreams__bubble")).toBeNull();
});
it("shows custom dreamingOf text when provided", () => {
const container = renderInto(buildProps({ dreamingOf: "reindexing old chats…" }));
const text = container.querySelector(".dreams__bubble-text");
expect(text?.textContent).toBe("reindexing old chats…");
});
it("shows active status label when active", () => {
const container = renderInto(buildProps({ active: true }));
const label = container.querySelector(".dreams__status-label");
expect(label?.textContent).toBe("Sleep Maintenance Active");
});
it("shows idle status label when inactive", () => {
const container = renderInto(buildProps({ active: false }));
const label = container.querySelector(".dreams__status-label");
expect(label?.textContent).toBe("Sleep Idle");
});
it("applies idle class when not active", () => {
const container = renderInto(buildProps({ active: false }));
expect(container.querySelector(".dreams--idle")).not.toBeNull();
});
it("shows next cycle info when provided", () => {
const container = renderInto(buildProps({ nextCycle: "4:00 AM" }));
const detail = container.querySelector(".dreams__status-detail span");
expect(detail?.textContent).toContain("4:00 AM");
});
it("renders phase controls", () => {
const container = renderInto(buildProps());
expect(container.querySelector(".dreams__controls")).not.toBeNull();
expect(container.querySelectorAll(".dreams__phase").length).toBe(3);
});
it("renders control error when present", () => {
const container = renderInto(buildProps({ statusError: "patch failed" }));
expect(container.querySelector(".dreams__controls-error")?.textContent).toContain(
"patch failed",
);
});
it("wires phase toggle callbacks", () => {
const onTogglePhase = vi.fn();
const container = renderInto(buildProps({ onTogglePhase }));
container.querySelector<HTMLButtonElement>(".dreams__phase .btn")?.click();
expect(onTogglePhase).toHaveBeenCalled();
});
});

View File

@@ -1,256 +0,0 @@
import { html, nothing } from "lit";
import type { SleepPhaseId } from "../controllers/dreaming.ts";
export type DreamsProps = {
active: boolean;
shortTermCount: number;
longTermCount: number;
promotedCount: number;
dreamingOf: string | null;
nextCycle: string | null;
timezone: string | null;
phases: Array<{
id: SleepPhaseId;
label: string;
detail: string;
enabled: boolean;
nextCycle: string | null;
managedCronPresent: boolean;
}>;
statusLoading: boolean;
statusError: string | null;
modeSaving: boolean;
onRefresh: () => void;
onToggleEnabled: (enabled: boolean) => void;
onTogglePhase: (phase: SleepPhaseId, enabled: boolean) => void;
};
const DREAM_PHRASES = [
"consolidating memories…",
"tidying the knowledge graph…",
"replaying today's conversations…",
"weaving short-term into long-term…",
"defragmenting the mind palace…",
"filing away loose thoughts…",
"connecting distant dots…",
"composting old context windows…",
"alphabetizing the subconscious…",
"promoting promising hunches…",
"forgetting what doesn't matter…",
"dreaming in embeddings…",
"reorganizing the memory attic…",
"softly indexing the day…",
"nurturing fledgling insights…",
"simmering half-formed ideas…",
"whispering to the vector store…",
];
let _dreamIndex = Math.floor(Math.random() * DREAM_PHRASES.length);
let _dreamLastSwap = 0;
const DREAM_SWAP_MS = 6_000;
function currentDreamPhrase(): string {
const now = Date.now();
if (now - _dreamLastSwap > DREAM_SWAP_MS) {
_dreamLastSwap = now;
_dreamIndex = (_dreamIndex + 1) % DREAM_PHRASES.length;
}
return DREAM_PHRASES[_dreamIndex];
}
const STARS: {
top: number;
left: number;
size: number;
delay: number;
hue: "neutral" | "accent";
}[] = [
{ top: 8, left: 15, size: 3, delay: 0, hue: "neutral" },
{ top: 12, left: 72, size: 2, delay: 1.4, hue: "neutral" },
{ top: 22, left: 35, size: 3, delay: 0.6, hue: "accent" },
{ top: 18, left: 88, size: 2, delay: 2.1, hue: "neutral" },
{ top: 35, left: 8, size: 2, delay: 0.9, hue: "neutral" },
{ top: 45, left: 92, size: 2, delay: 1.7, hue: "neutral" },
{ top: 55, left: 25, size: 3, delay: 2.5, hue: "accent" },
{ top: 65, left: 78, size: 2, delay: 0.3, hue: "neutral" },
{ top: 75, left: 45, size: 2, delay: 1.1, hue: "neutral" },
{ top: 82, left: 60, size: 3, delay: 1.8, hue: "accent" },
{ top: 30, left: 55, size: 2, delay: 0.4, hue: "neutral" },
{ top: 88, left: 18, size: 2, delay: 2.3, hue: "neutral" },
];
const sleepingLobster = html`
<svg viewBox="0 0 120 120" fill="none">
<defs>
<linearGradient id="dream-lob-g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff4d4d" />
<stop offset="100%" stop-color="#991b1b" />
</linearGradient>
</defs>
<path
d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z"
fill="url(#dream-lob-g)"
/>
<path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#dream-lob-g)" />
<path
d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z"
fill="url(#dream-lob-g)"
/>
<path d="M45 15Q38 8 35 14" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round" />
<path d="M75 15Q82 8 85 14" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round" />
<path
d="M39 36Q45 32 51 36"
stroke="#050810"
stroke-width="2.5"
stroke-linecap="round"
fill="none"
/>
<path
d="M69 36Q75 32 81 36"
stroke="#050810"
stroke-width="2.5"
stroke-linecap="round"
fill="none"
/>
</svg>
`;
export function renderDreams(props: DreamsProps) {
const idle = !props.active;
const dreamText = props.dreamingOf ?? currentDreamPhrase();
return html`
<section class="dreams ${idle ? "dreams--idle" : ""}">
${STARS.map(
(s) => html`
<div
class="dreams__star"
style="
top: ${s.top}%;
left: ${s.left}%;
width: ${s.size}px;
height: ${s.size}px;
background: ${s.hue === "accent" ? "var(--accent-muted)" : "var(--text)"};
animation-delay: ${s.delay}s;
"
></div>
`,
)}
<div class="dreams__moon"></div>
${props.active
? html`
<div class="dreams__bubble">
<span class="dreams__bubble-text">${dreamText}</span>
</div>
<div
class="dreams__bubble-dot"
style="top: calc(50% - 100px); left: calc(50% - 80px); width: 12px; height: 12px; animation-delay: 0.2s;"
></div>
<div
class="dreams__bubble-dot"
style="top: calc(50% - 70px); left: calc(50% - 50px); width: 8px; height: 8px; animation-delay: 0.4s;"
></div>
`
: nothing}
<div class="dreams__glow"></div>
<div class="dreams__lobster">${sleepingLobster}</div>
<span class="dreams__z">z</span>
<span class="dreams__z">z</span>
<span class="dreams__z">Z</span>
<div class="dreams__status">
<span class="dreams__status-label"
>${props.active ? "Sleep Maintenance Active" : "Sleep Idle"}</span
>
<div class="dreams__status-detail">
<div class="dreams__status-dot"></div>
<span>
${props.promotedCount} promoted
${props.nextCycle ? html`· next phase ${props.nextCycle}` : nothing}
${props.timezone ? html`· ${props.timezone}` : nothing}
</span>
</div>
</div>
<div class="dreams__stats">
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--text-strong);"
>${props.shortTermCount}</span
>
<span class="dreams__stat-label">Short-term</span>
</div>
<div class="dreams__stat-divider"></div>
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--accent);"
>${props.longTermCount}</span
>
<span class="dreams__stat-label">Long-term</span>
</div>
<div class="dreams__stat-divider"></div>
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--accent-2);"
>${props.promotedCount}</span
>
<span class="dreams__stat-label">Promoted Today</span>
</div>
</div>
<div class="dreams__controls">
<div class="dreams__controls-head">
<div>
<div class="dreams__controls-title">Sleep phases</div>
<div class="dreams__controls-subtitle">Light sorts, deep keeps, REM reflects.</div>
</div>
<div class="dreams__controls-actions">
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving}
@click=${props.onRefresh}
>
${props.statusLoading ? "Refreshing…" : "Refresh"}
</button>
<button
class="btn btn--sm ${props.active ? "btn--subtle" : ""}"
?disabled=${props.modeSaving}
@click=${() => props.onToggleEnabled(!props.active)}
>
${props.active ? "Disable Sleep" : "Enable Sleep"}
</button>
</div>
</div>
<div class="dreams__phase-grid">
${props.phases.map(
(phase) => html`
<article class="dreams__phase ${phase.enabled ? "dreams__phase--active" : ""}">
<div class="dreams__phase-top">
<div>
<div class="dreams__phase-label">${phase.label}</div>
<div class="dreams__phase-detail">${phase.detail}</div>
</div>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || !props.active}
@click=${() => props.onTogglePhase(phase.id, !phase.enabled)}
>
${phase.enabled ? "Pause" : "Enable"}
</button>
</div>
<div class="dreams__phase-meta">
<span>${phase.enabled ? "scheduled" : "off"}</span>
<span>${phase.nextCycle ? `next ${phase.nextCycle}` : "no next run"}</span>
<span>${phase.managedCronPresent ? "managed cron" : "cron missing"}</span>
</div>
</article>
`,
)}
</div>
${props.statusError
? html`<div class="dreams__controls-error">${props.statusError}</div>`
: nothing}
</div>
</section>
`;
}