From 6c0672865e13045c8e92ff800031e73d7678e4e9 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 7 Jul 2026 08:20:41 -0400 Subject: [PATCH] feat(viewer): improve first-run empty state --- .changeset/empty-first-post-transition.md | 5 +++++ e2e/viewer.spec.ts | 26 +++++++++++++++++++++++ viewer/src/App.tsx | 16 +++++++++++--- viewer/src/state.ts | 15 +++++++------ 4 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 .changeset/empty-first-post-transition.md diff --git a/.changeset/empty-first-post-transition.md b/.changeset/empty-first-post-transition.md new file mode 100644 index 0000000..1d4499e --- /dev/null +++ b/.changeset/empty-first-post-transition.md @@ -0,0 +1,5 @@ +--- +"sideshow": patch +--- + +Keep first-run onboarding visible until the first post arrives and poll as a fallback while waiting. diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index 6a02943..365ac2e 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -66,6 +66,32 @@ test("snippet published over HTTP appears live via SSE, no reload", async ({ pag await expect(page.locator(".sess-title")).toContainText("e2e session"); }); +test("empty onboarding polls into Home when the first post arrives", async ({ page, server }) => { + await page.route("**/api/events", (route) => route.abort()); + await page.goto(server.url); + await expect(page.locator("#onboard")).toBeVisible(); + + await publish(server.url, { html: "

first

", title: "First post", agent: "e2e" }); + + await expect(page.getByRole("heading", { name: "Home" })).toBeVisible({ timeout: 6_000 }); + await expect(page.locator(".home-card-title")).toHaveText("First post"); + await expect(page.locator("#onboard")).toBeHidden(); +}); + +test("an empty session alone keeps first-run onboarding visible", async ({ page, server }) => { + await fetch(`${server.url}/api/sessions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ agent: "idle", title: "Empty one" }), + }); + + await page.goto(server.url); + + await expect(page.locator("#onboard")).toBeVisible(); + await expect(page.getByRole("heading", { name: "Connect your first agent" })).toBeVisible(); + await expect(page.locator(".sess.sel")).toHaveCount(0); +}); + test("a surface kind this viewer doesn't know shows a refresh hint, not a broken diff", async ({ page, server, diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index b89d8fb..a23d5f3 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -70,8 +70,10 @@ function isConnectPath() { return rest === "/connect"; } const [connectPath, setConnectPath] = createSignal(isConnectPath()); +const hasPosts = () => sessions.some((s) => s.surfaceCount > 0); +const emptyWorkspace = () => initialLoaded() && !selected() && !hasPosts(); const homePath = () => - !streamMode() && !connectPath() && initialLoaded() && sessions.length > 0 && !selected(); + !streamMode() && !connectPath() && initialLoaded() && hasPosts() && !selected(); // Stream-only layout: no sidebar, session list, or session chrome — just the // current session's stream. Driven by the host's `layout` (cloud embed) or the @@ -706,8 +708,16 @@ function SessionTitle(props: { current: SessionRow | undefined }) { } function Onboard() { + createEffect(() => { + if (!emptyWorkspace() || isReadonly() || connectPath()) return; + const poll = setInterval(() => { + if (!document.hidden) void refreshSessionsQuiet(); + }, 1500); + onCleanup(() => clearInterval(poll)); + }); + return ( -