diff --git a/ui/src/styles/dreams.css b/ui/src/styles/dreams.css index 9db89e7f3ef..b3908e0eebe 100644 --- a/ui/src/styles/dreams.css +++ b/ui/src/styles/dreams.css @@ -282,153 +282,56 @@ } } -/* ---- Stats bar ---- */ +/* ---- Sleep phase cards ---- */ -.dreams__stats { - position: relative; +.dreams__phases { display: flex; align-items: center; - gap: 48px; - margin-top: 36px; + gap: 24px; + margin-top: 32px; z-index: 1; } -.dreams__stat { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -.dreams__stat-value { - font-size: 28px; - font-weight: 700; - font-variant-numeric: tabular-nums; -} - -.dreams__stat-label { - font-size: 11px; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.dreams__stat-divider { - width: 1px; - height: 32px; - background: var(--border); -} - -.dreams__trace { - position: relative; - width: min(900px, calc(100% - 40px)); - margin-top: 28px; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - align-items: start; - gap: 12px; - z-index: 1; - user-select: text; - max-height: min(500px, calc(100vh - 240px)); - overflow-y: auto; - overflow-x: hidden; -} - -.dreams__trace-section { - position: relative; - background: color-mix(in oklab, var(--panel) 82%, transparent); - border: 1px solid color-mix(in oklab, var(--border) 78%, transparent); - border-radius: 16px; - padding: 12px; - min-height: 180px; - backdrop-filter: blur(14px); - overflow: hidden; - min-width: 0; - z-index: 1; -} - -.dreams__trace-header { +.dreams__phase { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 10px; -} - -.dreams__trace-title { - font-size: 11px; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.08em; - font-weight: 600; -} - -.dreams__trace-count { - min-width: 24px; - height: 24px; - border-radius: 999px; - display: inline-flex; - align-items: center; - justify-content: center; - background: color-mix(in oklab, var(--accent-subtle) 85%, transparent); - color: var(--accent); - font-size: 12px; - font-weight: 700; - font-variant-numeric: tabular-nums; -} - -.dreams__trace-list { - display: flex; - flex-direction: column; gap: 8px; - min-width: 0; -} - -.dreams__trace-item { - padding: 10px; + padding: 8px 16px; border-radius: 12px; - background: color-mix(in oklab, var(--panel-raised) 88%, transparent); - border: 1px solid color-mix(in oklab, var(--border) 72%, transparent); - overflow: hidden; - overflow-wrap: anywhere; - word-break: break-word; + background: color-mix(in oklab, var(--panel) 70%, transparent); + border: 1px solid color-mix(in oklab, var(--border) 60%, transparent); } -.dreams__trace-snippet { - font-size: 13px; - line-height: 1.35; +.dreams__phase--off { + opacity: 0.4; +} + +.dreams__phase-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--muted); + opacity: 0.4; +} + +.dreams__phase-dot--on { + background: var(--ok); + opacity: 1; + box-shadow: 0 0 6px rgba(34, 197, 94, 0.3); +} + +.dreams__phase-name { + font-size: 12px; + font-weight: 600; color: var(--text); - overflow-wrap: anywhere; - word-break: break-word; - white-space: normal; + text-transform: uppercase; + letter-spacing: 0.06em; } -.dreams__trace-source, -.dreams__trace-meta, -.dreams__trace-empty { - margin-top: 6px; +.dreams__phase-next { font-size: 11px; - line-height: 1.35; color: var(--muted); -} - -.dreams__trace-source { - font-family: var(--mono); - overflow-wrap: anywhere; - word-break: break-word; -} - -.dreams__trace-meta, -.dreams__trace-empty { - overflow-wrap: anywhere; - word-break: break-word; -} - -@media (max-width: 980px) { - .dreams__trace { - grid-template-columns: 1fr; - width: min(680px, calc(100% - 32px)); - } + font-variant-numeric: tabular-nums; } /* ---- Dreaming on/off toggle (header bar) ---- */ @@ -561,48 +464,13 @@ =========================================== */ .dreams-diary { - position: relative; display: flex; flex-direction: column; - align-items: center; - padding: 48px 24px 64px; + padding: 24px 32px 64px; flex: 1; min-height: 320px; min-width: 0; overflow: auto; - background: linear-gradient( - 180deg, - var(--bg) 0%, - color-mix(in oklab, var(--bg) 94%, #0d0818) 40%, - color-mix(in oklab, var(--bg) 88%, #0d0818) 100% - ); -} - -/* Ambient shimmer across the diary surface */ -.dreams-diary::before { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient( - 135deg, - transparent 30%, - rgba(251, 191, 36, 0.012) 45%, - rgba(255, 77, 77, 0.015) 55%, - transparent 70% - ); - background-size: 400% 400%; - animation: diary-shimmer 20s ease-in-out infinite; - pointer-events: none; -} - -@keyframes diary-shimmer { - 0%, - 100% { - background-position: 0% 0%; - } - 50% { - background-position: 100% 100%; - } } /* ---- Diary header ---- */ @@ -611,11 +479,7 @@ display: flex; align-items: center; gap: 16px; - margin-bottom: 32px; - width: 100%; - max-width: 520px; - position: relative; - z-index: 1; + margin-bottom: 20px; flex-shrink: 0; } @@ -677,18 +541,12 @@ .dreams-diary__entry { position: relative; - max-width: 520px; - width: 100%; - padding: 0 0 0 20px; - z-index: 1; + max-width: 680px; + padding: 0 0 0 16px; flex-shrink: 0; animation: diary-entry-reveal 1.4s cubic-bezier(0.22, 1, 0.36, 1) both; } -.dreams-diary__entry--structured { - max-width: 1180px; -} - @keyframes diary-entry-reveal { 0% { opacity: 0; @@ -732,13 +590,25 @@ .dreams-diary__date { display: block; - font-size: 10px; - color: var(--accent-muted); - letter-spacing: 0.08em; - text-transform: uppercase; - font-weight: 400; + font-size: 12px; + color: var(--text); + font-weight: 600; margin-bottom: 16px; - opacity: 0.8; +} + +.dreams-diary__daychips { + display: flex; + gap: 6px; + margin: 0 0 24px; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + -ms-overflow-style: none; + padding-bottom: 2px; +} + +.dreams-diary__daychips::-webkit-scrollbar { + display: none; } .dreams-diary__navigator { @@ -804,9 +674,7 @@ } .dreams-diary__day-chip { - width: 100%; - min-width: 0; - padding: 6px 0; + padding: 4px 12px; border-radius: 999px; border: 1px solid color-mix(in oklab, var(--border) 70%, transparent); background: color-mix(in oklab, var(--panel) 84%, transparent); @@ -815,6 +683,13 @@ font-variant-numeric: tabular-nums; cursor: pointer; text-align: center; + white-space: nowrap; + transition: border-color 140ms ease, color 140ms ease, background 140ms ease; +} + +.dreams-diary__day-chip:hover { + color: var(--text); + border-color: color-mix(in oklab, var(--accent) 30%, var(--border)); } .dreams-diary__day-chip--active { @@ -965,10 +840,9 @@ .dreams-diary__para { margin: 0 0 12px; - font-size: 14px; - line-height: 1.8; - color: color-mix(in oklab, var(--text) 85%, var(--muted)); - font-style: italic; + font-size: 13px; + line-height: 1.7; + color: var(--text); animation: diary-text-stream 2.4s cubic-bezier(0.22, 1, 0.36, 1) both; overflow-wrap: anywhere; word-break: break-word; @@ -1039,29 +913,13 @@ min-height: calc(100vh - 96px); } - .dreams__stats { - gap: 22px; - } - - .dreams__phase-bar { + .dreams__phases { + gap: 12px; flex-wrap: wrap; - gap: 8px; - padding: 10px 16px; - } - - .dreams__phase-bar-phases { - gap: 10px; + justify-content: center; } .dreams-diary { - padding: 32px 16px 48px; - } - - .dreams-diary__entry { - padding-left: 16px; - } - - .dreams-diary__grid { - grid-template-columns: 1fr; + padding: 20px 16px 48px; } } diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 5e3f3ab7d24..2c27257b07b 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -1936,6 +1936,7 @@ export function renderApp(state: AppViewState) { totalSignalCount: state.dreamingStatus?.totalSignalCount ?? 0, promotedCount: state.dreamingStatus?.promotedToday ?? 0, phaseSignalCount: state.dreamingStatus?.phaseSignalCount ?? 0, + phases: state.dreamingStatus?.phases ?? undefined, shortTermEntries: state.dreamingStatus?.shortTermEntries ?? [], signalEntries: state.dreamingStatus?.signalEntries ?? [], promotedEntries: state.dreamingStatus?.promotedEntries ?? [], diff --git a/ui/src/ui/views/dreaming.ts b/ui/src/ui/views/dreaming.ts index 2b01990a152..e113123d7f7 100644 --- a/ui/src/ui/views/dreaming.ts +++ b/ui/src/ui/views/dreaming.ts @@ -255,6 +255,12 @@ function renderDiaryNavigator( `; } +type DreamingPhaseInfo = { + enabled: boolean; + cron: string; + nextRunAtMs?: number; +}; + export type DreamingProps = { active: boolean; shortTermCount: number; @@ -262,6 +268,11 @@ export type DreamingProps = { totalSignalCount: number; promotedCount: number; phaseSignalCount: number; + phases?: { + light: DreamingPhaseInfo; + deep: DreamingPhaseInfo; + rem: DreamingPhaseInfo; + }; shortTermEntries: { key: string; path: string; @@ -474,8 +485,40 @@ export function renderDreaming(props: DreamingProps) { // ── Scene renderer ──────────────────────────────────────────────────── +// Strip source citations like [memory/2026-04-09.md:9] and section headings, +// flatten structured diary entries into plain paragraphs. +function flattenDiaryBody(body: string): string[] { + return body + .split("\n") + .map((line) => line.trim()) + // Remove section headings that leak implementation + .filter( + (line) => + line.length > 0 && + line !== "What Happened" && + line !== "Reflections" && + line !== "Candidates" && + line !== "Possible Lasting Updates", + ) + // Strip source citations [memory/...] + .map((line) => line.replace(/\s*\[memory\/[^\]]+\]/g, "")) + // Strip leading list markers and labels + .map((line) => + line + .replace(/^(?:\d+\.\s+|-\s+(?:\[[^\]]+\]\s+)?(?:[a-z_]+:\s+)?)/i, "") + .replace(/^(?:likely_durable|likely_situational|unclear):\s+/i, "") + .trim(), + ) + .filter((line) => line.length > 0); +} + +function formatPhaseNextRun(nextRunAtMs?: number): string { + if (!nextRunAtMs) {return "—";} + const d = new Date(nextRunAtMs); + return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); +} + function renderScene(props: DreamingProps, idle: boolean, dreamText: string) { - const groundedEntries = props.shortTermEntries.filter((entry) => entry.groundedCount > 0); return html`
${STARS.map( @@ -534,6 +577,24 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) { + +
+ ${["light", "deep", "rem"].map((phaseId) => { + const phase = props.phases?.[phaseId as keyof NonNullable]; + const enabled = phase?.enabled ?? false; + const nextRun = formatPhaseNextRun(phase?.nextRunAtMs); + const label = phaseId.charAt(0).toUpperCase() + phaseId.slice(1); + return html` +
+
+ ${label} + ${enabled ? nextRun : "off"} +
+ `; + })} +
+ +
- -
- -
-
- ${props.shortTermCount} - ${t("dreaming.stats.shortTerm")} -
-
-
- ${props.groundedSignalCount} - ${t("dreaming.stats.grounded")} -
-
-
- ${props.totalSignalCount} - ${t("dreaming.stats.signals")} -
-
-
- ${props.promotedCount} - ${t("dreaming.stats.promoted")} -
-
- -
- ${renderTraceSection("shortTerm", props.shortTermEntries, { - count: props.shortTermCount, - emptyKey: "dreaming.trace.emptyShortTerm", - meta: (entry) => - [ - entry.recallCount > 0 - ? `${entry.recallCount} recall${entry.recallCount === 1 ? "" : "s"}` - : null, - entry.dailyCount > 0 ? `${entry.dailyCount} daily` : null, - entry.groundedCount > 0 ? `${entry.groundedCount} grounded` : null, - entry.phaseHitCount > 0 - ? `${entry.phaseHitCount} phase hit${entry.phaseHitCount === 1 ? "" : "s"}` - : null, - ] - .filter(Boolean) - .join(" · "), - })} - ${renderTraceSection("grounded", groundedEntries, { - count: groundedEntries.length, - emptyKey: "dreaming.trace.emptyGrounded", - meta: (entry) => - [ - `${entry.groundedCount} grounded`, - entry.recallCount > 0 - ? `${entry.recallCount} recall${entry.recallCount === 1 ? "" : "s"}` - : null, - entry.dailyCount > 0 ? `${entry.dailyCount} daily` : null, - isGroundedLed(entry) ? t("dreaming.trace.groundedLed") : null, - ] - .filter(Boolean) - .join(" · "), - })} - ${renderTraceSection("signals", props.signalEntries, { - count: props.totalSignalCount, - emptyKey: "dreaming.trace.emptySignals", - meta: (entry) => - [ - `${entry.totalSignalCount} signal${entry.totalSignalCount === 1 ? "" : "s"}`, - entry.phaseHitCount > 0 - ? `${entry.phaseHitCount} phase hit${entry.phaseHitCount === 1 ? "" : "s"}` - : null, - ] - .filter(Boolean) - .join(" · "), - })} - ${renderTraceSection("promoted", props.promotedEntries, { - count: props.promotedCount, - emptyKey: "dreaming.trace.emptyPromoted", - meta: (entry) => - [ - entry.promotedAt ? formatCompactDateTime(entry.promotedAt) : null, - entry.groundedCount > 0 ? `${entry.groundedCount} grounded` : null, - isGroundedLed(entry) ? t("dreaming.trace.groundedLed") : null, - entry.totalSignalCount > 0 - ? `${entry.totalSignalCount} signal${entry.totalSignalCount === 1 ? "" : "s"} before promote` - : null, - ] - .filter(Boolean) - .join(" · "), - })}
${props.statusError @@ -791,31 +752,6 @@ function renderDiarySection(props: DreamingProps) {
${t("dreaming.diary.title")} -
- - ${page + 1} / ${reversed.length} - -
-
-
- ${renderDiaryNavigator(reversed, page, props.onRequestUpdate)} -
+ +
+ ${reversed.map( + (e, i) => html` + + `, + )}
-
+
${entry.date ? html`` : nothing} - ${structured - ? html` -
-
-

What Happened

-
- ${structured.whatHappened.map( - (item, i) => html` -
- -

${item}

-
- `, - )} -
-
-
-

Reflections

-
- ${structured.reflections.map( - (item, i) => html` -
- -

${item}

-
- `, - )} -
-
-
-

Candidates + Possible Lasting Updates

- ${structured.candidates.length > 0 - ? html` -
Candidates
-
- ${structured.candidates.map( - (item, i) => html` -
- -

${item}

-
- `, - )} -
- ` - : nothing} - ${structured.lastingUpdates.length > 0 - ? html` -
Possible Lasting Updates
-
- ${structured.lastingUpdates.map( - (item, i) => html` -
- -

${item}

-
- `, - )} -
- ` - : nothing} -
-
- ` - : html` -
- ${entry.body - .split("\n") - .map( - (para, i) => - html`

- ${para} -

`, - )} -
- `} +
+ ${flattenDiaryBody(entry.body).map( + (para, i) => + html`

+ ${para} +

`, + )} +
`;