Skip to content

Commit 0162ffa

Browse files
benvinegarclaude
andcommitted
feat(viewer): post/surface vocabulary — engine UI, host contract, helper rename
Adopt the canonical workspace ▸ session ▸ post ▸ surface model in the viewer engine. A post is the published artifact (an ordered list of surfaces); a surface is one block inside a post. Internal rename only — behavior is unchanged and every wire-bound string is kept byte-identical: /api/* paths, /s/ nav URLs, the ?part= query key, SSE event types (surface-created/-updated/-deleted), the comment body { surface } key and author:"surface" value, and the server-provided surfaceCount field. The Route type's surfaceId field is also left intact. - viewer local identifiers/props/signals/functions: artifact surface→post, block part→surface (focusPost, standalonePost, posts, postLink, postImageLink, Card/StandaloneView prop post, surfaceFrames). - component files renamed: ImagePart→ImageSurface, JsonPart→JsonSurface, TracePart→TraceSurface (prop part→surface). - CSS classes: .part-unsupported→.surface-unsupported, .imagepart→.image-surface, .tracepart→.trace-surface, .jsonpart→.json-surface, .tl-surface→.tl-post. - user-visible strings: "surface"→"post"; onboarding copy reworded off the retired board/snippet terms. - host contract: identity.accountSlug→identity.workspaceSlug (embedders must update) in viewer/src/host.ts and viewer/embed.d.ts. - server helper: surfaceParts.ts→postSurfaces.ts, coerceSurfaceParts→coerceSurfaces, validateSurfaceParts→validateSurfaces; importers + mcpHttp coerceParts alias updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 14c48dd commit 0162ffa

19 files changed

Lines changed: 307 additions & 272 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"sideshow": minor
3+
---
4+
5+
Adopt the **post / surface** vocabulary throughout the viewer engine and the
6+
host contract. The canonical hierarchy is **workspace ▸ session ▸ post ▸
7+
surface**: a **post** is the published artifact (an ordered list of surfaces),
8+
and a **surface** is one block inside a post.
9+
10+
This is an internal rename of the viewer's local identifiers, component names,
11+
props, CSS classes, and user-visible strings — behavior is unchanged and all
12+
wire paths, query keys (`?part=`), SSE event types, and server-provided JSON
13+
field names are kept byte-identical for compatibility. The block component
14+
files were renamed (`ImagePart``ImageSurface`, `JsonPart``JsonSurface`,
15+
`TracePart``TraceSurface`), and the server helper `surfaceParts.ts` is now
16+
`postSurfaces.ts` (`coerceSurfaceParts``coerceSurfaces`,
17+
`validateSurfaceParts``validateSurfaces`).
18+
19+
**Host-contract change (embedders must update):** the host identity key
20+
`identity.accountSlug` is renamed to `identity.workspaceSlug`. Any embedder
21+
passing `accountSlug` on the injected host's `identity` must rename it to
22+
`workspaceSlug`.

server/app.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
type TerminalSurface,
3131
type TraceStep,
3232
} from "./types.ts";
33-
import { validateSurfaceParts } from "./surfaceParts.ts";
33+
import { validateSurfaces } from "./postSurfaces.ts";
3434

3535
const MAX_SURFACE_BYTES = 2 * 1024 * 1024;
3636
const MAX_WAIT_SECONDS = 300;
@@ -808,7 +808,7 @@ export function createApp({
808808
if (!body || !Array.isArray(blocks)) {
809809
return c.json({ error: 'body must include a "surfaces" (or legacy "parts") array' }, 400);
810810
}
811-
const parsed = await validateSurfaceParts(blocks);
811+
const parsed = await validateSurfaces(blocks);
812812
if (!parsed.ok) return c.json({ error: parsed.error }, 400);
813813
return publish(c, body, parsed.parts);
814814
};
@@ -823,7 +823,7 @@ export function createApp({
823823
if (!body || typeof body.html !== "string" || !body.html.trim()) {
824824
return c.json({ error: 'body must include non-empty "html" string' }, 400);
825825
}
826-
const parsed = await validateSurfaceParts([htmlSurface(body.html, body.kits)]);
826+
const parsed = await validateSurfaces([htmlSurface(body.html, body.kits)]);
827827
if (!parsed.ok) return c.json({ error: parsed.error }, 400);
828828
return publish(c, body, parsed.parts);
829829
});
@@ -860,11 +860,11 @@ export function createApp({
860860
if (!Array.isArray(blocks)) {
861861
return c.json({ error: '"surfaces" (or legacy "parts") must be an array' }, 400);
862862
}
863-
const parsed = await validateSurfaceParts(blocks);
863+
const parsed = await validateSurfaces(blocks);
864864
if (!parsed.ok) return c.json({ error: parsed.error }, 400);
865865
parts = parsed.parts;
866866
} else if (typeof body.html === "string") {
867-
const parsed = await validateSurfaceParts([htmlSurface(body.html, body.kits)]);
867+
const parsed = await validateSurfaces([htmlSurface(body.html, body.kits)]);
868868
if (!parsed.ok) return c.json({ error: parsed.error }, 400);
869869
parts = parsed.parts;
870870
}

server/kits.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// per part, not a frame every surface is locked into.
77
//
88
// Runtime-agnostic (no node imports): imported by surfacePage (server render),
9-
// surfaceParts (id allowlist), and surfaced over HTTP/MCP for discovery. Every
9+
// postSurfaces (id allowlist), and surfaced over HTTP/MCP for discovery. Every
1010
// class resolves against the theme `--color-*` / `--font-*` / radius tokens, so
1111
// kit output re-themes with the board like any other html part.
1212

server/mcpHttp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type Surface,
1212
} from "./types.ts";
1313
import { HTTP_MCP_TOOLS, MCP_INSTRUCTIONS, MCP_SERVER_INFO } from "./mcpSpec.ts";
14-
import { coerceSurfaceParts } from "./surfaceParts.ts";
14+
import { coerceSurfaces } from "./postSurfaces.ts";
1515

1616
// Stateless MCP over streamable HTTP: every request is self-contained, which
1717
// is what a serverless deployment needs. Session continuity is explicit —
@@ -51,7 +51,7 @@ export interface McpDeps {
5151
// Coerce loosely-typed tool args into validated SurfacePart[]. Unknown kinds
5252
// and empty parts are dropped rather than rejected, so a slightly-off call
5353
// still publishes what it can.
54-
export const coerceParts = coerceSurfaceParts;
54+
export const coerceParts = coerceSurfaces;
5555

5656
export function registerMcp(app: Hono, deps: McpDeps) {
5757
// The view URL's path segment: legacy tools emit /s/<id>; the new post tools
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,10 @@ async function parseSurfaceParts(
269269
return { parts, errors: [] };
270270
}
271271

272-
export const coerceSurfaceParts = (raw: unknown): Promise<Surface[]> =>
272+
export const coerceSurfaces = (raw: unknown): Promise<Surface[]> =>
273273
parseSurfaceParts(raw).then((r) => r.parts);
274274

275-
export async function validateSurfaceParts(
275+
export async function validateSurfaces(
276276
raw: unknown,
277277
): Promise<{ ok: true; parts: Surface[] } | { ok: false; error: string }> {
278278
const result = await parseSurfaceParts(raw, { strict: true });

test/assets.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
selectEvictions,
99
type Surface,
1010
} from "../server/types.ts";
11-
import { validateSurfaceParts } from "../server/surfaceParts.ts";
11+
import { validateSurfaces } from "../server/postSurfaces.ts";
1212

1313
// --- selectEvictions ---
1414

@@ -78,8 +78,8 @@ test("surfacesByteLength counts image/trace surfaces without throwing", () => {
7878

7979
// --- SurfacePart validation/coercion ---
8080

81-
test("validateSurfaceParts accepts all supported part kinds", async () => {
82-
const result = await validateSurfaceParts([
81+
test("validateSurfaces accepts all supported part kinds", async () => {
82+
const result = await validateSurfaces([
8383
{ kind: "html", html: "<p>x</p>" },
8484
{ kind: "html", html: "<div class=tree></div>", kits: ["issues"] },
8585
{ kind: "diff", patch: "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b", layout: "unified" },
@@ -118,7 +118,7 @@ test("validateSurfaceParts accepts all supported part kinds", async () => {
118118
);
119119
});
120120

121-
test("validateSurfaceParts rejects malformed parts", async () => {
121+
test("validateSurfaces rejects malformed parts", async () => {
122122
for (const parts of [
123123
[{ kind: "html", html: 1 }],
124124
[{ kind: "html", html: "<p>x</p>", kits: ["nope"] }], // unknown kit id (strict)
@@ -131,41 +131,41 @@ test("validateSurfaceParts rejects malformed parts", async () => {
131131
[{ kind: "code" }], // missing code
132132
[{ kind: "unknown" }],
133133
]) {
134-
const result = await validateSurfaceParts(parts);
134+
const result = await validateSurfaces(parts);
135135
assert.equal(result.ok, false, JSON.stringify(parts));
136136
}
137137
});
138138

139-
test("validateSurfaceParts rejects a diff patch with no parseable file content", async () => {
139+
test("validateSurfaces rejects a diff patch with no parseable file content", async () => {
140140
for (const patch of [
141141
"not a patch at all",
142142
"hello world\nfoo bar",
143143
"@@ -1 +1 @@\n-a\n+b", // hunk with no --- /+++ file headers
144144
]) {
145-
const result = await validateSurfaceParts([{ kind: "diff", patch }]);
145+
const result = await validateSurfaces([{ kind: "diff", patch }]);
146146
assert.equal(result.ok, false, `patch ${JSON.stringify(patch)} should be rejected`);
147147
if (!result.ok) assert.match(result.error, /did not parse to any file/);
148148
}
149149
});
150150

151-
test("validateSurfaceParts accepts real unified and git-style diff patches", async () => {
151+
test("validateSurfaces accepts real unified and git-style diff patches", async () => {
152152
for (const patch of [
153153
"--- a/x.ts\n+++ b/x.ts\n@@ -1 +1 @@\n-a\n+b",
154154
"diff --git a/x b/x\nindex 0..1 100644\n--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b",
155155
"--- a/x\n+++ b/x\n@@ -1,2 +1,2 @@\n a\n-b\n+c\n d\n--- a/y\n+++ b/y\n@@ -1 +1 @@\n-e\n+f", // multi-file
156156
]) {
157-
const result = await validateSurfaceParts([{ kind: "diff", patch }]);
157+
const result = await validateSurfaces([{ kind: "diff", patch }]);
158158
assert.equal(result.ok, true, `patch ${JSON.stringify(patch)} should be accepted`);
159159
}
160160
});
161161

162-
test("validateSurfaceParts accepts valid mermaid diagrams (supported types)", async () => {
162+
test("validateSurfaces accepts valid mermaid diagrams (supported types)", async () => {
163163
for (const mermaid of [
164164
'pie title Pets\n "Dogs" : 386\n "Cats" : 85',
165165
"gitGraph\n commit\n commit\n branch develop",
166166
"architecture-beta\n group api(cloud)[API]",
167167
]) {
168-
const result = await validateSurfaceParts([{ kind: "mermaid", mermaid }]);
168+
const result = await validateSurfaces([{ kind: "mermaid", mermaid }]);
169169
assert.equal(
170170
result.ok,
171171
true,
@@ -174,7 +174,7 @@ test("validateSurfaceParts accepts valid mermaid diagrams (supported types)", as
174174
}
175175
});
176176

177-
test("validateSurfaceParts lets unsupported mermaid types through (Jison types)", async () => {
177+
test("validateSurfaces lets unsupported mermaid types through (Jison types)", async () => {
178178
// flowchart, sequence, class, state, er, gantt are still on Jison — the
179179
// official parser doesn't cover them, so validation is skipped and the
180180
// viewer's graceful fallback handles any render failure.
@@ -186,7 +186,7 @@ test("validateSurfaceParts lets unsupported mermaid types through (Jison types)"
186186
"gantt\n title Project\n section Phase 1\n Task 1 :a1, 2024-01-01, 30d",
187187
"classDiagram\n Animal <|-- Dog",
188188
]) {
189-
const result = await validateSurfaceParts([{ kind: "mermaid", mermaid }]);
189+
const result = await validateSurfaces([{ kind: "mermaid", mermaid }]);
190190
assert.equal(
191191
result.ok,
192192
true,
@@ -195,12 +195,12 @@ test("validateSurfaceParts lets unsupported mermaid types through (Jison types)"
195195
}
196196
});
197197

198-
test("validateSurfaceParts rejects invalid mermaid with a parse error (supported types)", async () => {
198+
test("validateSurfaces rejects invalid mermaid with a parse error (supported types)", async () => {
199199
for (const mermaid of [
200200
'pie title Pets\n "Dogs" : broken !!@@',
201201
"gitGraph\n commit\n !!bad syntax!!",
202202
]) {
203-
const result = await validateSurfaceParts([{ kind: "mermaid", mermaid }]);
203+
const result = await validateSurfaces([{ kind: "mermaid", mermaid }]);
204204
assert.equal(
205205
result.ok,
206206
false,

test/kits.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
22
import { test } from "node:test";
33
import { isKnownKit, KIT_IDS, kitAssets, kitSummaries } from "../server/kits.ts";
44
import { renderHtmlPage } from "../server/surfacePage.ts";
5-
import { coerceSurfaceParts, validateSurfaceParts } from "../server/surfaceParts.ts";
5+
import { coerceSurfaces, validateSurfaces } from "../server/postSurfaces.ts";
66

77
// --- kitAssets ---
88

@@ -75,29 +75,29 @@ test("kitSummaries advertises each kit without leaking the css/js payload", () =
7575

7676
// --- validation: strict (REST) rejects, loose (MCP) filters ---
7777

78-
test("validateSurfaceParts accepts an html part with known kits", async () => {
79-
const r = await validateSurfaceParts([
78+
test("validateSurfaces accepts an html part with known kits", async () => {
79+
const r = await validateSurfaces([
8080
{ kind: "html", html: "<p>x</p>", kits: ["issues", "slides"] },
8181
]);
8282
assert.equal(r.ok, true);
8383
if (r.ok)
8484
assert.deepEqual(r.parts[0], { kind: "html", html: "<p>x</p>", kits: ["issues", "slides"] });
8585
});
8686

87-
test("validateSurfaceParts rejects an unknown kit id with the valid set", async () => {
88-
const r = await validateSurfaceParts([{ kind: "html", html: "<p>x</p>", kits: ["bogus"] }]);
87+
test("validateSurfaces rejects an unknown kit id with the valid set", async () => {
88+
const r = await validateSurfaces([{ kind: "html", html: "<p>x</p>", kits: ["bogus"] }]);
8989
assert.equal(r.ok, false);
9090
if (!r.ok) assert.match(r.error, /unknown kit "bogus".*issues/);
9191
});
9292

93-
test("coerceSurfaceParts filters unknown kits rather than dropping the part", async () => {
94-
const parts = await coerceSurfaceParts([
93+
test("coerceSurfaces filters unknown kits rather than dropping the part", async () => {
94+
const parts = await coerceSurfaces([
9595
{ kind: "html", html: "<p>x</p>", kits: ["issues", "bogus"] },
9696
]);
9797
assert.deepEqual(parts, [{ kind: "html", html: "<p>x</p>", kits: ["issues"] }]);
9898
});
9999

100-
test("coerceSurfaceParts drops an all-unknown kits field entirely", async () => {
101-
const parts = await coerceSurfaceParts([{ kind: "html", html: "<p>x</p>", kits: ["nope"] }]);
100+
test("coerceSurfaces drops an all-unknown kits field entirely", async () => {
101+
const parts = await coerceSurfaces([{ kind: "html", html: "<p>x</p>", kits: ["nope"] }]);
102102
assert.deepEqual(parts, [{ kind: "html", html: "<p>x</p>" }]);
103103
});

viewer/embed.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface SideshowHost {
1818
basePath: string;
1919
router: HostRouter;
2020
/** The caller's own identity, when the host knows it. */
21-
identity?: { login: string; accountSlug?: string; role?: string };
21+
identity?: { login: string; workspaceSlug?: string; role?: string };
2222
/**
2323
* Layout the engine renders. "full" (default) shows the sidebar + stream;
2424
* "stream" shows only the current session's stream — no sidebar, session list,

0 commit comments

Comments
 (0)