diff --git a/.changeset/agent-session-logos.md b/.changeset/agent-session-logos.md new file mode 100644 index 0000000..4110a8d --- /dev/null +++ b/.changeset/agent-session-logos.md @@ -0,0 +1,12 @@ +--- +"sideshow": minor +--- + +Session rows in the sidebar now show a small monochrome logo for the agent that +authored the session (Claude, OpenCode, Cursor, Copilot, Gemini, …, with a +neutral terminal glyph for anything unrecognized) and carry the session's +surface count as a quiet "(N)" on the title. The marks are single-path Simple +Icons glyphs inlined as SVG and filled with `currentColor`, so they take the +surrounding muted text color and adapt to light/dark with no runtime network +fetch. The result tightens each session's meta line to read simply +"logo · agent · time". diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index 1073138..3ac2f03 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -17,17 +17,34 @@ test("the sidebar groups sessions by recency and sinks empty ones to the bottom" // both were created just now, so they share one recency group await expect(page.locator(".sess-group").first()).toHaveText("Today"); - // the empty session is marked vacant, sinks below the one with work, and - // reads "no surfaces yet" instead of a count + // the empty session is marked vacant and sinks below the one with work const rows = page.locator("#sessionList .sess"); await expect(rows).toHaveCount(2); - // the session with work is on top even though the empty one is more recent - await expect(rows.nth(0)).toContainText("1 surface"); + // the session with work is on top even though the empty one is more recent; + // its count rides the title as "(1)" + await expect(rows.nth(0).locator(".sess-count")).toHaveText("(1)"); await expect(rows.nth(0)).not.toHaveClass(/vacant/); - // the empty session sinks below, marked vacant, reading "no surfaces yet" + // the empty session sinks below, marked vacant, with no count on its title await expect(rows.nth(1)).toHaveClass(/vacant/); await expect(rows.nth(1)).toContainText("Empty one"); - await expect(rows.nth(1)).toContainText("no surfaces yet"); + await expect(rows.nth(1).locator(".sess-count")).toHaveCount(0); +}); + +test("session rows show the agent's logo, with a fallback for unknown agents", async ({ + page, + server, +}) => { + await publish(server.url, { html: "

x

", title: "A", agent: "claude" }); + await publish(server.url, { html: "

y

", title: "B", agent: "some-new-agent" }); + + await page.goto(server.url); + + // every row carries an inline agent mark in its meta — a known brand glyph + // or the neutral fallback for an unrecognized agent + const rows = page.locator("#sessionList .sess"); + await expect(rows).toHaveCount(2); + await expect(rows.nth(0).locator(".sess-meta svg.agent-mark")).toBeVisible(); + await expect(rows.nth(1).locator(".sess-meta svg.agent-mark")).toBeVisible(); }); test("snippet published over HTTP appears live via SSE, no reload", async ({ page, server }) => { @@ -248,19 +265,19 @@ test("Cmd+Option+Up/Down switches between sessions, wrapping at the ends", async await page.goto(server.url); // the newest session sits at the top of the list and is selected on load - await expect(page.locator(".sess.sel .sess-title")).toHaveText("two session"); + await expect(page.locator(".sess.sel .sess-title")).toContainText("two session"); // Down moves to the next (older) session down the list await page.keyboard.press("Meta+Alt+ArrowDown"); - await expect(page.locator(".sess.sel .sess-title")).toHaveText("one session"); + await expect(page.locator(".sess.sel .sess-title")).toContainText("one session"); // Down again wraps back to the top await page.keyboard.press("Meta+Alt+ArrowDown"); - await expect(page.locator(".sess.sel .sess-title")).toHaveText("two session"); + await expect(page.locator(".sess.sel .sess-title")).toContainText("two session"); // Up wraps from the top back to the bottom await page.keyboard.press("Meta+Alt+ArrowUp"); - await expect(page.locator(".sess.sel .sess-title")).toHaveText("one session"); + await expect(page.locator(".sess.sel .sess-title")).toContainText("one session"); }); test("at phone width the sidebar collapses into a drawer and actions stay visible", async ({ diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index 76cb090..1b7fc39 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -1,4 +1,5 @@ import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"; +import { AgentMark } from "./agentMarks.tsx"; import { api, relTime, sessionLabel, type SessionRow } from "./api.ts"; import { Card, cardEls, frameForSource } from "./Card.tsx"; import { renderNotes } from "./notes.ts"; @@ -297,13 +298,15 @@ function SessionItem(props: { session: SessionRow }) { } }} > -
{label()}
+
+ {label()} + 0}> + ({props.session.surfaceCount}) + +
- {props.session.agent} ·{" "} - {props.session.surfaceCount === 0 - ? "no surfaces yet" - : `${props.session.surfaceCount} surface${props.session.surfaceCount === 1 ? "" : "s"}`}{" "} - · {relTime(props.session.lastActiveAt)} + + {props.session.agent} · {relTime(props.session.lastActiveAt)}