From 39070ae1e1c64ea23b8bd7fbd72f7bad57e18306 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Thu, 25 Jun 2026 22:01:50 -0400 Subject: [PATCH] refactor(engine): use canonical /api/posts wire + post-* SSE events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The viewer now fetches the canonical /api/posts(/:id) and /api/sessions/:id/posts paths instead of the legacy /api/surfaces aliases, and the live feed emits and listens for post-created/updated/deleted instead of surface-*. Server and viewer ship together, so the SSE event-type flip is in lockstep. Strictly first-party: every external-facing alias is retained as a deprecated shim — the legacy /api/surfaces + /s/ routes, /api/snippets, the publish_surface/ update_surface/list_surfaces MCP tools, and the surfaceId/surfaceTitle comment feedback wire fields are all unchanged. Removing those stays a separate, telemetry-gated step. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/engine-post-wire.md | 5 +++++ e2e/viewer.spec.ts | 2 +- server/app.ts | 6 +++--- server/events.ts | 4 ++-- viewer/src/Card.tsx | 3 +-- viewer/src/state.ts | 24 +++++++++--------------- 6 files changed, 21 insertions(+), 23 deletions(-) create mode 100644 .changeset/engine-post-wire.md diff --git a/.changeset/engine-post-wire.md b/.changeset/engine-post-wire.md new file mode 100644 index 0000000..678c9f8 --- /dev/null +++ b/.changeset/engine-post-wire.md @@ -0,0 +1,5 @@ +--- +"sideshow": minor +--- + +The engine now uses the canonical `/api/posts` wire and `post-*` SSE events; legacy `/api/surfaces` routes, `surface-*` aliases, and `publish_surface` MCP tools remain as deprecated aliases. diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index c9e297a..9ab0621 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -74,7 +74,7 @@ test("a surface kind this viewer doesn't know shows a refresh hint, not a broken // server returns a valid surface, but rewrite the surface kind to one THIS // viewer build has no Match for. It must degrade to a neutral hint, never // the diff fallback. - await page.route(/\/api\/surfaces\/[^/?]+(\?|$)/, async (route) => { + await page.route(/\/api\/posts\/[^/?]+(\?|$)/, async (route) => { const res = await route.fetch(); const surface = await res.json(); if (Array.isArray(surface.surfaces)) { diff --git a/server/app.ts b/server/app.ts index c6c9adf..1113779 100644 --- a/server/app.ts +++ b/server/app.ts @@ -386,7 +386,7 @@ export function createApp({ title: input.title?.slice(0, MAX_TITLE), }); if (!surface) return { error: "session not found", status: 404 }; - bus.broadcast({ type: "surface-created", id: surface.id, sessionId, version: 1 }); + bus.broadcast({ type: "post-created", id: surface.id, sessionId, version: 1 }); return { surface, userFeedback: await collectFeedback(sessionId) }; } @@ -444,7 +444,7 @@ export function createApp({ const surface = await store.updatePost(id, { surfaces: patch.parts, title: patch.title }); if (!surface) return { error: "surface not found", status: 404 }; bus.broadcast({ - type: "surface-updated", + type: "post-updated", id: surface.id, sessionId: surface.sessionId, version: surface.version, @@ -886,7 +886,7 @@ export function createApp({ const surface = await store.getPost(c.req.param("id")); if (!surface) return c.json({ error: "surface not found" }, 404); await store.removePost(surface.id); - bus.broadcast({ type: "surface-deleted", id: surface.id, sessionId: surface.sessionId }); + bus.broadcast({ type: "post-deleted", id: surface.id, sessionId: surface.sessionId }); return c.json({ ok: true }); }; app.delete("/api/surfaces/:id", remove); diff --git a/server/events.ts b/server/events.ts index e8aa0f7..130a7f7 100644 --- a/server/events.ts +++ b/server/events.ts @@ -1,7 +1,7 @@ export type FeedEvent = | { type: "session-created" | "session-updated" | "session-deleted"; id: string } - | { type: "surface-created" | "surface-updated"; id: string; sessionId: string; version: number } - | { type: "surface-deleted"; id: string; sessionId: string } + | { type: "post-created" | "post-updated"; id: string; sessionId: string; version: number } + | { type: "post-deleted"; id: string; sessionId: string } | { type: "comment-created"; id: string; diff --git a/viewer/src/Card.tsx b/viewer/src/Card.tsx index 813e86d..37d70c4 100644 --- a/viewer/src/Card.tsx +++ b/viewer/src/Card.tsx @@ -362,8 +362,7 @@ export function Card(props: { post: Post; standalone?: boolean }) { aria-label={`Delete "${props.post.title}"`} onClick={async () => { if (confirm(`Delete "${props.post.title}"?`)) { - // /api/surfaces/:id is the legacy wire alias for a post. - await api(`/api/surfaces/${props.post.id}`, { method: "DELETE" }); + await api(`/api/posts/${props.post.id}`, { method: "DELETE" }); } }} > diff --git a/viewer/src/state.ts b/viewer/src/state.ts index d6ed1e4..afce2a7 100644 --- a/viewer/src/state.ts +++ b/viewer/src/state.ts @@ -183,8 +183,7 @@ export async function bootstrap() { // Fetch a post and switch into standalone mode. No-op if already showing it. export async function enterStandalone(id: string) { if (standalonePost()?.id === id) return; - // /api/surfaces/:id is the legacy wire alias for fetching a post. - const post = await api(`/api/surfaces/${encodeURIComponent(id)}`).catch(() => null); + const post = await api(`/api/posts/${encodeURIComponent(id)}`).catch(() => null); if (post) setStandaloneInternal(post); } @@ -192,8 +191,7 @@ export async function refreshSessions(targetPostId?: string | null) { if (isReadonly() && publicReadMode() === "session") { const route = host().router.get(); if (!route.sessionId && targetPostId) { - // /api/surfaces/:id is the legacy wire alias for fetching a post. - const target = await api(`/api/surfaces/${encodeURIComponent(targetPostId)}`).catch( + const target = await api(`/api/posts/${encodeURIComponent(targetPostId)}`).catch( () => null, ); if (!target) return; @@ -217,8 +215,7 @@ export async function refreshSessions(targetPostId?: string | null) { await refreshSessionsQuiet(); if (selected() && !sessions.some((s) => s.id === selected())) setSelectedInternal(null); if (targetPostId) { - // /api/surfaces/:id is the legacy wire alias for fetching a post. - const target = await api(`/api/surfaces/${encodeURIComponent(targetPostId)}`).catch( + const target = await api(`/api/posts/${encodeURIComponent(targetPostId)}`).catch( () => null, ); if (target && sessions.some((s) => s.id === target.sessionId)) { @@ -268,10 +265,9 @@ export async function select( setCommentsInternal([]); setTraceStepsInternal([]); void fetchTrace(id); - // /api/sessions/:id/surfaces and /api/surfaces/:id are the legacy wire aliases. - const metas = await api<{ id: string }[]>(`/api/sessions/${id}/surfaces`).catch(() => []); + const metas = await api<{ id: string }[]>(`/api/sessions/${id}/posts`).catch(() => []); const details = ( - await Promise.all(metas.map((m) => api(`/api/surfaces/${m.id}`).catch(() => null))) + await Promise.all(metas.map((m) => api(`/api/posts/${m.id}`).catch(() => null))) ).filter((s) => s !== null); if (selected() !== id) return; // user switched away mid-load setPostsInternal(reconcile(details, { key: "id" })); @@ -341,8 +337,7 @@ export async function selectAdjacent(delta: 1 | -1) { // Fetch a post and insert/update it in the open session's stream. async function upsertPost(id: string, { scroll = true } = {}) { - // /api/surfaces/:id is the legacy wire alias for fetching a post. - const s = await api(`/api/surfaces/${id}`).catch(() => null); + const s = await api(`/api/posts/${id}`).catch(() => null); if (!s || s.sessionId !== selected()) return; const idx = posts.findIndex((x) => x.id === s.id); if (idx >= 0) { @@ -452,11 +447,11 @@ export function connect() { applyTheme(e.id); } else if (e.type.startsWith("session-")) { await refreshSessions(); - } else if (e.type === "surface-created" || e.type === "surface-updated") { + } else if (e.type === "post-created" || e.type === "post-updated") { if (away && e.sessionId) markUnread(e.sessionId); if (e.sessionId === selected()) await upsertPost(e.id); await refreshSessionsQuiet(); - } else if (e.type === "surface-deleted") { + } else if (e.type === "post-deleted") { const idx = posts.findIndex((s) => s.id === e.id); if (idx >= 0) setPostsInternal(produce((arr) => arr.splice(idx, 1))); await refreshSessionsQuiet(); @@ -481,8 +476,7 @@ async function resyncSelected() { await refreshSessions(); if (!before || selected() !== before) return; // select() rebuilt the stream void fetchTrace(before); - // /api/sessions/:id/surfaces is the legacy wire alias. - const metas = await api<{ id: string }[]>(`/api/sessions/${before}/surfaces`).catch(() => []); + const metas = await api<{ id: string }[]>(`/api/sessions/${before}/posts`).catch(() => []); const ids = new Set(metas.map((m) => m.id)); setPostsInternal( produce((arr) => {