diff --git a/docs/screenshots/dreams-dark-detail.png b/docs/screenshots/dreams-dark-detail.png
new file mode 100644
index 0000000..e7b6664
Binary files /dev/null and b/docs/screenshots/dreams-dark-detail.png differ
diff --git a/docs/screenshots/dreams-dark-list.png b/docs/screenshots/dreams-dark-list.png
new file mode 100644
index 0000000..337c980
Binary files /dev/null and b/docs/screenshots/dreams-dark-list.png differ
diff --git a/docs/screenshots/dreams-light-detail.png b/docs/screenshots/dreams-light-detail.png
new file mode 100644
index 0000000..bcb2e0b
Binary files /dev/null and b/docs/screenshots/dreams-light-detail.png differ
diff --git a/docs/screenshots/dreams-light-list.png b/docs/screenshots/dreams-light-list.png
new file mode 100644
index 0000000..d599858
Binary files /dev/null and b/docs/screenshots/dreams-light-list.png differ
diff --git a/packages/web/e2e/dreams.spec.ts b/packages/web/e2e/dreams.spec.ts
new file mode 100644
index 0000000..d9c7c54
--- /dev/null
+++ b/packages/web/e2e/dreams.spec.ts
@@ -0,0 +1,148 @@
+import { expect, test } from "@playwright/test";
+
+const STORE_KEY = "openconcho:instances";
+const STORE_VALUE = JSON.stringify({
+ instances: [{ id: "i1", name: "Local", baseUrl: "http://localhost:9999", token: "" }],
+ activeId: "i1",
+});
+
+test.describe("Dreams route", () => {
+ test.beforeEach(async ({ context }) => {
+ await context.addInitScript(
+ ([key, value]) => {
+ window.localStorage.setItem(key, value);
+ },
+ [STORE_KEY, STORE_VALUE],
+ );
+ // Stub the conclusions/list endpoint so the route can render real dreams.
+ // :9999 is unreachable; this intercept replaces the network call entirely.
+ // Use a function matcher so the trailing query string (?page=&page_size=) doesn't
+ // break a glob.
+ await context.route(
+ (url) => url.pathname.endsWith("/conclusions/list"),
+ async (route) => {
+ const now = Date.now();
+ const iso = (offsetMs: number) => new Date(now - offsetMs).toISOString();
+ const items = [
+ // Dream A — burst
+ {
+ id: "ind-1",
+ content: "Alice prefers asynchronous communication",
+ observer_id: "alice",
+ observed_id: "bob",
+ session_id: "sess-1",
+ created_at: iso(1000),
+ conclusion_type: "inductive",
+ reasoning_tree: {
+ conclusion_id: "ind-1",
+ premises: [{ conclusion_id: "ded-1" }],
+ },
+ },
+ {
+ id: "ded-1",
+ content: "Alice mentioned email twice and declined two meetings",
+ observer_id: "alice",
+ observed_id: "bob",
+ session_id: "sess-1",
+ created_at: iso(2000),
+ conclusion_type: "deductive",
+ reasoning_tree: {
+ conclusion_id: "ded-1",
+ premises: [{ conclusion_id: "exp-1" }, { conclusion_id: "exp-2" }],
+ },
+ },
+ {
+ id: "exp-1",
+ content: "Alice said 'just email me'",
+ observer_id: "alice",
+ observed_id: "bob",
+ session_id: "sess-1",
+ created_at: iso(3000),
+ conclusion_type: "explicit",
+ },
+ {
+ id: "exp-2",
+ content: "Alice declined the Tuesday standup",
+ observer_id: "alice",
+ observed_id: "bob",
+ session_id: "sess-1",
+ created_at: iso(4000),
+ conclusion_type: "explicit",
+ },
+ // Dream B — 30 minutes ago, different pair → clusters separately
+ {
+ id: "ded-2",
+ content: "Carol responds in the evenings",
+ observer_id: "carol",
+ observed_id: "dan",
+ session_id: "sess-2",
+ created_at: iso(30 * 60_000),
+ conclusion_type: "deductive",
+ },
+ ];
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ items,
+ total: items.length,
+ pages: 1,
+ page: 1,
+ size: items.length,
+ }),
+ });
+ },
+ );
+ });
+
+ test("shows a Dreams entry in the workspace sub-nav", async ({ page }) => {
+ await page.goto("/workspaces/ws-test/dreams");
+ // Sidebar link with the Dreams label
+ const dreamsLink = page.getByRole("link", { name: /^Dreams$/ });
+ await expect(dreamsLink.first()).toBeVisible();
+ });
+
+ test("renders heading and breadcrumb on the dreams route", async ({ page }) => {
+ await page.goto("/workspaces/ws-test/dreams");
+ await expect(page.getByRole("heading", { name: /^Dreams$/ })).toBeVisible();
+ // Breadcrumb specifically — the sidebar has a "Workspaces" link too, so scope.
+ await expect(
+ page.getByLabel("Breadcrumb").getByRole("link", { name: "Workspaces" }),
+ ).toBeVisible();
+ });
+
+ test("clusters mocked conclusions into dreams and opens detail on click", async ({ page }) => {
+ await page.goto("/workspaces/ws-test/dreams");
+
+ // Two dreams: alice→bob burst, and the older carol→dan
+ const rows = page.locator('button[aria-pressed]');
+ await expect(rows).toHaveCount(2);
+
+ // Alice→bob row should show count chips
+ await expect(rows.first()).toContainText("alice");
+ await expect(rows.first()).toContainText("bob");
+ await expect(rows.first()).toContainText("2 explicit");
+ await expect(rows.first()).toContainText("1 deductive");
+ await expect(rows.first()).toContainText("1 inductive");
+
+ // Click → detail panel renders three columns
+ await rows.first().click();
+ await expect(page.getByText("Dream detail")).toBeVisible();
+ await expect(page.getByText("Explicit", { exact: true })).toBeVisible();
+ await expect(page.getByText("Deductive", { exact: true })).toBeVisible();
+ await expect(page.getByText("Inductive", { exact: true })).toBeVisible();
+ });
+
+ test("expands premise tree for an inductive conclusion", async ({ page }) => {
+ await page.goto("/workspaces/ws-test/dreams");
+ await page.locator('button[aria-pressed]').first().click();
+
+ const showPremises = page.getByRole("button", { name: /^Show premises$/i });
+ await expect(showPremises).toBeVisible();
+ await showPremises.click();
+
+ // The reasoning chain renders with the deductive premise (ded-1)
+ await expect(page.getByText("Reasoning chain")).toBeVisible();
+ await expect(page.getByLabel("Premise tree")).toBeVisible();
+ });
+});
diff --git a/packages/web/src/components/dreams/DreamList.tsx b/packages/web/src/components/dreams/DreamList.tsx
index 674bfa3..fe9beff 100644
--- a/packages/web/src/components/dreams/DreamList.tsx
+++ b/packages/web/src/components/dreams/DreamList.tsx
@@ -203,7 +203,7 @@ function CountChip({ label, value, kind }: { label: string; value: number; kind:
}}
>
{value}
- {label}
+ {label}
);
}