diff --git a/.changeset/bright-homes-gather.md b/.changeset/bright-homes-gather.md new file mode 100644 index 0000000..4c17ebf --- /dev/null +++ b/.changeset/bright-homes-gather.md @@ -0,0 +1,5 @@ +--- +"sideshow": patch +--- + +Show a recent-posts Home view for first-time visitors to multi-session workspaces, with live updates and safe themed previews. diff --git a/e2e/embed-home-view.spec.ts b/e2e/embed-home-view.spec.ts index ea2961d..43eda5a 100644 --- a/e2e/embed-home-view.spec.ts +++ b/e2e/embed-home-view.spec.ts @@ -64,6 +64,7 @@ test("homeView: a session-less route lands with NO session selected", async ({ p await expect(page.locator(".sess.sel")).toHaveCount(0); await expect(page.locator(".sess[aria-current='true']")).toHaveCount(0); await expect(page.locator(".card:not(#whatsNew)")).toHaveCount(0); + await expect(page.locator(".home-page")).toHaveCount(0); }); test("homeView OFF (self-hosted default): a session-less route auto-selects the latest", async ({ diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index 74a551d..2530419 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -368,6 +368,93 @@ test("a surface kind this viewer doesn't know shows a refresh hint, not a broken await expect(card.locator(".diff-error")).toHaveCount(0); }); +test("the workspace root shows a live recent posts home", async ({ page, server }) => { + const recentLimits: string[] = []; + page.on("request", (request) => { + const url = new URL(request.url()); + if (url.pathname === "/api/posts/recent") + recentLimits.push(url.searchParams.get("limit") ?? ""); + }); + + const first = await publish(server.url, { + html: "
slow
", @@ -684,10 +771,13 @@ test("Cmd+Option+Up/Down switches between sessions, wrapping at the ends", async await publish(server.url, { html: "b
", title: "Second", agent: "two" }); await page.goto(server.url); - // the newest session sits at the top of the list and is selected on load + await expect(page.getByRole("heading", { name: "Home" })).toBeVisible(); + + // With no selected session on Home, Down opens the newest session. + await page.keyboard.press("Meta+Alt+ArrowDown"); await expect(page.locator(".sess.sel .sess-title")).toContainText("two session"); - // Down moves to the next (older) session down the list + // Down then moves to the next (older) session down the list. await page.keyboard.press("Meta+Alt+ArrowDown"); await expect(page.locator(".sess.sel .sess-title")).toContainText("one session"); diff --git a/package-lock.json b/package-lock.json index c2678dc..c373d27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3953,9 +3953,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.36", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.36.tgz", - "integrity": "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4059,9 +4059,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -4079,11 +4079,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -4230,9 +4230,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -4658,9 +4658,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.372", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", - "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", "dev": true, "license": "ISC" }, @@ -5011,9 +5011,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "funding": [ { "type": "github", @@ -6701,9 +6701,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, "license": "MIT", "engines": { @@ -7248,12 +7248,13 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -8381,9 +8382,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { diff --git a/server/apiViews.ts b/server/apiViews.ts index 82fcf89..71b6086 100644 --- a/server/apiViews.ts +++ b/server/apiViews.ts @@ -246,8 +246,23 @@ export const recentSurfacePreviewView = (surface: Surface, index: number) => ({ index, }); -export const recentPostRowView = (post: Post, session: Session | null | undefined) => { - const surfaces = post.surfaces.map(recentSurfacePreviewView); +// The self-hosted Home uses one preview per post. Rich surfaces render from their +// immutable /s document and trace only needs a kind label, so only image/json +// retain inline data. This bounds Home's response without weakening the +// full recent-feed contract used by embedders. +export const recentHomeSurfaceView = (surface: Surface, index: number) => + surface.kind === "image" || surface.kind === "json" + ? recentSurfacePreviewView(surface, index) + : surfaceRef(surface, index); + +export const recentPostRowView = ( + post: Post, + session: Session | null | undefined, + opts?: { homePreview?: boolean }, +) => { + const surfaces = opts?.homePreview + ? post.surfaces.slice(0, 1).map(recentHomeSurfaceView) + : post.surfaces.map(recentSurfacePreviewView); return { id: post.id, sessionId: post.sessionId, diff --git a/server/app.ts b/server/app.ts index 530c4ff..23478b4 100644 --- a/server/app.ts +++ b/server/app.ts @@ -1063,13 +1063,14 @@ export function createApp({ // agent for the feed card, canonical surfaces, legacy partKinds, and capped // previews. // - // Previews are bounded by recentPostRowView (large inline text clipped with - // truncated:true); images travel as plain assetId refs (served at /a/:id), - // so the response stays cheap. Same auth as /api/sessions — see + // Full previews are bounded by recentPostRowView (large inline text clipped + // with truncated:true); `?preview=home` returns only one compact preview per + // post for the self-hosted Home. Same auth as /api/sessions — see // isPublicReadAllowed, which intentionally does NOT expose this path on a // session-scoped publicRead workspace. const listRecentPosts = async (c: any) => { const limit = parseRecentLimit(c.req.query("limit")); + const homePreview = c.req.query("preview") === "home"; const posts = await store.listRecentPosts(limit); // Resolve each post's session once (agent + session title for the feed card). const sessions = new Map