dreaming: add an advanced review tab

This commit is contained in:
Dave Morin
2026-04-09 18:50:19 -10:00
committed by Vignesh
parent 0202af9b38
commit 564b46b39e
5 changed files with 478 additions and 31 deletions

View File

@@ -277,6 +277,7 @@ export const en: TranslationMap = {
tabs: {
scene: "Scene",
diary: "Diary",
advanced: "Advanced",
},
header: {
refresh: "Refresh",
@@ -302,6 +303,21 @@ export const en: TranslationMap = {
rem: "Rem",
off: "off",
},
advanced: {
eyebrow: "Operator Review",
title: "Grounded Replay + Promotion",
description:
"Inspect grounded replay output and use maintenance actions without cluttering the main Dreaming scene.",
stagedTitle: "Grounded Replay",
shortTermTitle: "Short-term Queue",
signalsTitle: "Signal Hotspots",
promotedTitle: "Recent Promotions",
emptyGrounded: "No staged grounded replay entries right now.",
emptyShortTerm: "No short-term entries to inspect.",
emptySignals: "No signal-rich entries to inspect.",
emptyPromoted: "No recent promotions to inspect.",
updatedPrefix: "updated",
},
stats: {
shortTerm: "Short-term",
grounded: "Grounded",

View File

@@ -415,13 +415,6 @@
color: var(--ok-muted);
}
.dreams__actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 14px;
}
.dreams__status-dot {
width: 6px;
height: 6px;
@@ -457,6 +450,181 @@
color: var(--muted);
}
/* ===========================================
Dreaming Advanced Operator Review
=========================================== */
.dreams-advanced {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px 32px 40px;
flex: 1;
min-height: 320px;
min-width: 0;
overflow: auto;
}
.dreams-advanced__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.dreams-advanced__intro {
max-width: 720px;
}
.dreams-advanced__eyebrow {
display: block;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-muted);
margin-bottom: 10px;
}
.dreams-advanced__title {
margin: 0;
font-size: 24px;
line-height: 1.1;
color: var(--text);
}
.dreams-advanced__description {
margin: 10px 0 0;
max-width: 60ch;
font-size: 13px;
line-height: 1.6;
color: var(--muted);
}
.dreams-advanced__actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.dreams-advanced__summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.dreams-advanced__summary-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px 16px;
border-radius: 14px;
border: 1px solid color-mix(in oklab, var(--border) 70%, transparent);
background: color-mix(in oklab, var(--panel) 88%, transparent);
}
.dreams-advanced__summary-label {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.dreams-advanced__summary-value {
font-size: 24px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text);
}
.dreams-advanced__sections {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.dreams-advanced__section {
display: flex;
flex-direction: column;
min-width: 0;
border-radius: 16px;
border: 1px solid color-mix(in oklab, var(--border) 70%, transparent);
background: color-mix(in oklab, var(--panel) 86%, transparent);
overflow: hidden;
}
.dreams-advanced__section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid color-mix(in oklab, var(--border) 62%, transparent);
}
.dreams-advanced__section-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.dreams-advanced__section-count {
min-width: 26px;
height: 26px;
padding: 0 8px;
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-advanced__list {
display: flex;
flex-direction: column;
}
.dreams-advanced__item {
padding: 14px 16px;
border-top: 1px solid color-mix(in oklab, var(--border) 50%, transparent);
}
.dreams-advanced__item:first-child {
border-top: none;
}
.dreams-advanced__snippet {
font-size: 13px;
line-height: 1.45;
color: var(--text);
}
.dreams-advanced__source,
.dreams-advanced__meta,
.dreams-advanced__empty {
font-size: 11px;
line-height: 1.45;
color: var(--muted);
margin-top: 8px;
}
.dreams-advanced__source {
font-family: var(--mono);
}
.dreams-advanced__empty {
padding: 16px;
}
/* ===========================================
Dream Diary Scroll section below hero
=========================================== */
@@ -667,6 +835,15 @@
min-height: calc(100vh - 96px);
}
.dreams-advanced {
padding: 20px 16px 32px;
}
.dreams-advanced__summary,
.dreams-advanced__sections {
grid-template-columns: 1fr;
}
.dreams__phases {
gap: 12px;
flex-wrap: wrap;

View File

@@ -69,6 +69,7 @@ import {
backfillDreamDiary,
loadDreamDiary,
loadDreamingStatus,
resetGroundedShortTerm,
resetDreamDiary,
resolveConfiguredDreaming,
updateDreamingEnabled,
@@ -1930,8 +1931,14 @@ export function renderApp(state: AppViewState) {
${state.tab === "dreams"
? renderDreaming({
active: dreamingOn,
shortTermCount: state.dreamingStatus?.shortTermCount ?? 0,
groundedSignalCount: state.dreamingStatus?.groundedSignalCount ?? 0,
totalSignalCount: state.dreamingStatus?.totalSignalCount ?? 0,
promotedCount: state.dreamingStatus?.promotedToday ?? 0,
phases: state.dreamingStatus?.phases ?? undefined,
shortTermEntries: state.dreamingStatus?.shortTermEntries ?? [],
signalEntries: state.dreamingStatus?.signalEntries ?? [],
promotedEntries: state.dreamingStatus?.promotedEntries ?? [],
dreamingOf: null,
nextCycle: dreamingNextCycle,
timezone: state.dreamingStatus?.timezone ?? null,
@@ -1947,6 +1954,7 @@ export function renderApp(state: AppViewState) {
onRefreshDiary: () => loadDreamDiary(state),
onBackfillDiary: () => backfillDreamDiary(state),
onResetDiary: () => resetDreamDiary(state),
onResetGroundedShortTerm: () => resetGroundedShortTerm(state),
onRequestUpdate: requestHostUpdate,
})
: nothing}

View File

@@ -7,12 +7,64 @@ import { renderDreaming, setDreamSubTab, type DreamingProps } from "./dreaming.t
function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
return {
active: true,
shortTermCount: 47,
groundedSignalCount: 9,
totalSignalCount: 182,
promotedCount: 12,
phases: {
light: { enabled: true, cron: "0 * * * *", nextRunAtMs: Date.parse("2026-04-05T11:30:00Z") },
deep: { enabled: true, cron: "30 * * * *", nextRunAtMs: Date.parse("2026-04-05T12:00:00Z") },
rem: { enabled: false, cron: "0 4 * * *" },
},
shortTermEntries: [
{
key: "memory:memory/2026-04-05.md:1:2",
path: "memory/2026-04-05.md",
startLine: 1,
endLine: 2,
snippet: "Emma prefers shorter, lower-pressure check-ins.",
recallCount: 2,
dailyCount: 1,
groundedCount: 1,
totalSignalCount: 3,
lightHits: 1,
remHits: 1,
phaseHitCount: 2,
},
],
signalEntries: [
{
key: "memory:memory/2026-04-05.md:1:2",
path: "memory/2026-04-05.md",
startLine: 1,
endLine: 2,
snippet: "Emma prefers shorter, lower-pressure check-ins.",
recallCount: 2,
dailyCount: 1,
groundedCount: 1,
totalSignalCount: 3,
lightHits: 1,
remHits: 1,
phaseHitCount: 2,
},
],
promotedEntries: [
{
key: "memory:memory/2026-04-04.md:4:5",
path: "memory/2026-04-04.md",
startLine: 4,
endLine: 5,
snippet: "Use the Happy Together calendar for flights.",
recallCount: 3,
dailyCount: 2,
groundedCount: 4,
totalSignalCount: 9,
lightHits: 0,
remHits: 0,
phaseHitCount: 0,
promotedAt: "2026-04-05T04:00:00.000Z",
},
],
dreamingOf: null,
nextCycle: "4:00 AM",
timezone: "America/Los_Angeles",
@@ -29,6 +81,7 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
onRefreshDiary: () => {},
onBackfillDiary: () => {},
onResetDiary: () => {},
onResetGroundedShortTerm: () => {},
...overrides,
};
}
@@ -73,13 +126,14 @@ describe("dreaming view", () => {
expect(container.querySelector(".dreams__phase--off")?.textContent).toContain("off");
});
it("renders scene backfill and reset controls", () => {
it("keeps maintenance controls out of the scene tab", () => {
const container = renderInto(buildProps());
const buttons = [...container.querySelectorAll("button")].map((node) =>
node.textContent?.trim(),
);
expect(buttons).toContain("Backfill");
expect(buttons).toContain("Reset");
expect(buttons).not.toContain("Backfill");
expect(buttons).not.toContain("Reset");
expect(buttons).not.toContain("Clear Grounded");
});
it("shows dream bubble when active", () => {
@@ -131,9 +185,10 @@ describe("dreaming view", () => {
it("renders sub-tab navigation", () => {
const container = renderInto(buildProps());
const tabs = container.querySelectorAll(".dreams__tab");
expect(tabs.length).toBe(2);
expect(tabs.length).toBe(3);
expect(tabs[0]?.textContent).toContain("Scene");
expect(tabs[1]?.textContent).toContain("Diary");
expect(tabs[2]?.textContent).toContain("Advanced");
});
it("renders dream diary with parsed entry on diary tab", () => {
@@ -258,5 +313,24 @@ describe("dreaming view", () => {
setDreamSubTab("scene");
});
it("renders operator actions and evidence lists on the advanced tab", () => {
setDreamSubTab("advanced");
const container = renderInto(buildProps());
expect(container.querySelector(".dreams-advanced__title")?.textContent).toContain(
"Grounded Replay",
);
const buttons = [...container.querySelectorAll("button")].map((node) =>
node.textContent?.trim(),
);
expect(buttons).toContain("Backfill");
expect(buttons).toContain("Reset");
expect(buttons).toContain("Clear Grounded");
expect(container.querySelector(".dreams-advanced__summary-value")?.textContent).toBe("47");
expect(container.querySelector(".dreams-advanced__item")?.textContent).toContain(
"Emma prefers shorter",
);
setDreamSubTab("scene");
});
// Toggle lives in the page header (app-render.ts), not inside the dreaming view.
});

View File

@@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n/index.ts";
import type { DreamingEntry } from "../controllers/dreaming.ts";
// ── Diary entry parser ─────────────────────────────────────────────────
@@ -89,12 +90,18 @@ type DreamingPhaseInfo = {
export type DreamingProps = {
active: boolean;
shortTermCount: number;
groundedSignalCount: number;
totalSignalCount: number;
promotedCount: number;
phases?: {
light: DreamingPhaseInfo;
deep: DreamingPhaseInfo;
rem: DreamingPhaseInfo;
};
shortTermEntries: DreamingEntry[];
signalEntries: DreamingEntry[];
promotedEntries: DreamingEntry[];
dreamingOf: string | null;
nextCycle: string | null;
timezone: string | null;
@@ -110,6 +117,7 @@ export type DreamingProps = {
onRefreshDiary: () => void;
onBackfillDiary: () => void;
onResetDiary: () => void;
onResetGroundedShortTerm: () => void;
onRequestUpdate?: () => void;
};
@@ -145,7 +153,7 @@ const DREAM_SWAP_MS = 6_000;
// ── Sub-tab state ─────────────────────────────────────────────────────
type DreamSubTab = "scene" | "diary";
type DreamSubTab = "scene" | "diary" | "advanced";
let _subTab: DreamSubTab = "scene";
export function setDreamSubTab(tab: DreamSubTab): void {
@@ -254,9 +262,22 @@ export function renderDreaming(props: DreamingProps) {
>
${t("dreaming.tabs.diary")}
</button>
<button
class="dreams__tab ${_subTab === "advanced" ? "dreams__tab--active" : ""}"
@click=${() => {
_subTab = "advanced";
props.onRequestUpdate?.();
}}
>
${t("dreaming.tabs.advanced")}
</button>
</nav>
${_subTab === "scene" ? renderScene(props, idle, dreamText) : renderDiarySection(props)}
${_subTab === "scene"
? renderScene(props, idle, dreamText)
: _subTab === "diary"
? renderDiarySection(props)
: renderAdvancedSection(props)}
</div>
`;
}
@@ -380,24 +401,175 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
)}
</div>
<!-- Actions -->
<div class="dreams__actions">
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onBackfillDiary()}
>
${props.dreamDiaryActionLoading
? t("dreaming.scene.working")
: t("dreaming.scene.backfill")}
</button>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onResetDiary()}
>
${t("dreaming.scene.reset")}
</button>
${props.statusError
? html`<div class="dreams__controls-error">${props.statusError}</div>`
: nothing}
</section>
`;
}
function formatRange(path: string, startLine: number, endLine: number): string {
return startLine === endLine ? `${path}:${startLine}` : `${path}:${startLine}-${endLine}`;
}
function formatCompactDateTime(value: string): string {
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) {
return value;
}
return new Date(parsed).toLocaleString([], {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
function renderAdvancedEntryList(
titleKey: string,
emptyKey: string,
entries: DreamingEntry[],
meta: (entry: DreamingEntry) => string[],
) {
return html`
<section class="dreams-advanced__section">
<div class="dreams-advanced__section-header">
<span class="dreams-advanced__section-title">${t(titleKey)}</span>
<span class="dreams-advanced__section-count">${entries.length}</span>
</div>
${entries.length === 0
? html`<div class="dreams-advanced__empty">${t(emptyKey)}</div>`
: html`
<div class="dreams-advanced__list">
${entries.map(
(entry) => html`
<article class="dreams-advanced__item" data-entry-key=${entry.key}>
<div class="dreams-advanced__snippet">${entry.snippet}</div>
<div class="dreams-advanced__source">
${formatRange(entry.path, entry.startLine, entry.endLine)}
</div>
<div class="dreams-advanced__meta">
${meta(entry)
.filter((part) => part.length > 0)
.join(" · ")}
</div>
</article>
`,
)}
</div>
`}
</section>
`;
}
function renderAdvancedSection(props: DreamingProps) {
const groundedEntries = props.shortTermEntries.filter((entry) => entry.groundedCount > 0);
return html`
<section class="dreams-advanced">
<div class="dreams-advanced__header">
<div class="dreams-advanced__intro">
<span class="dreams-advanced__eyebrow">${t("dreaming.advanced.eyebrow")}</span>
<h2 class="dreams-advanced__title">${t("dreaming.advanced.title")}</h2>
<p class="dreams-advanced__description">${t("dreaming.advanced.description")}</p>
</div>
<div class="dreams-advanced__actions">
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onBackfillDiary()}
>
${props.dreamDiaryActionLoading
? t("dreaming.scene.working")
: t("dreaming.scene.backfill")}
</button>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onResetDiary()}
>
${t("dreaming.scene.reset")}
</button>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onResetGroundedShortTerm()}
>
${t("dreaming.scene.clearGrounded")}
</button>
</div>
</div>
<div class="dreams-advanced__summary">
<div class="dreams-advanced__summary-card">
<span class="dreams-advanced__summary-label">${t("dreaming.stats.shortTerm")}</span>
<span class="dreams-advanced__summary-value">${props.shortTermCount}</span>
</div>
<div class="dreams-advanced__summary-card">
<span class="dreams-advanced__summary-label">${t("dreaming.stats.grounded")}</span>
<span class="dreams-advanced__summary-value">${props.groundedSignalCount}</span>
</div>
<div class="dreams-advanced__summary-card">
<span class="dreams-advanced__summary-label">${t("dreaming.stats.signals")}</span>
<span class="dreams-advanced__summary-value">${props.totalSignalCount}</span>
</div>
<div class="dreams-advanced__summary-card">
<span class="dreams-advanced__summary-label">${t("dreaming.stats.promoted")}</span>
<span class="dreams-advanced__summary-value">${props.promotedCount}</span>
</div>
</div>
<div class="dreams-advanced__sections">
${renderAdvancedEntryList(
"dreaming.advanced.stagedTitle",
"dreaming.advanced.emptyGrounded",
groundedEntries,
(entry) => [
entry.groundedCount > 0
? `${entry.groundedCount} ${t("dreaming.stats.grounded").toLowerCase()}`
: "",
entry.recallCount > 0 ? `${entry.recallCount} recall` : "",
entry.dailyCount > 0 ? `${entry.dailyCount} daily` : "",
],
)}
${renderAdvancedEntryList(
"dreaming.advanced.shortTermTitle",
"dreaming.advanced.emptyShortTerm",
props.shortTermEntries,
(entry) => [
entry.recallCount > 0 ? `${entry.recallCount} recall` : "",
entry.dailyCount > 0 ? `${entry.dailyCount} daily` : "",
entry.groundedCount > 0
? `${entry.groundedCount} ${t("dreaming.stats.grounded").toLowerCase()}`
: "",
entry.phaseHitCount > 0 ? `${entry.phaseHitCount} phase hit` : "",
],
)}
${renderAdvancedEntryList(
"dreaming.advanced.signalsTitle",
"dreaming.advanced.emptySignals",
props.signalEntries,
(entry) => [
`${entry.totalSignalCount} ${t("dreaming.stats.signals").toLowerCase()}`,
entry.phaseHitCount > 0 ? `${entry.phaseHitCount} phase hit` : "",
],
)}
${renderAdvancedEntryList(
"dreaming.advanced.promotedTitle",
"dreaming.advanced.emptyPromoted",
props.promotedEntries,
(entry) => [
entry.promotedAt
? `${t("dreaming.advanced.updatedPrefix")} ${formatCompactDateTime(entry.promotedAt)}`
: "",
entry.groundedCount > 0
? `${entry.groundedCount} ${t("dreaming.stats.grounded").toLowerCase()}`
: "",
entry.totalSignalCount > 0
? `${entry.totalSignalCount} ${t("dreaming.stats.signals").toLowerCase()}`
: "",
],
)}
</div>
${props.statusError