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 a91a31d..406b90b 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -368,6 +368,85 @@ 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 first = await publish(server.url, { + html: "
slow
", @@ -626,10 +705,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/server/apiViews.ts b/server/apiViews.ts index 82fcf89..dc53f3f 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 30-row 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 46196f2..f3a2f17 100644 --- a/server/app.ts +++ b/server/app.ts @@ -1056,13 +1056,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