Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/api-write-surface-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sideshow": minor
---

Return per-surface ids, kinds, and indexes in write responses without echoing surface payload bodies, and remove the redundant top-level `kinds` array. Read kinds from `surfaces.map((surface) => surface.kind)` instead.
5 changes: 5 additions & 0 deletions .changeset/session-posts-surfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sideshow": patch
---

List session posts with canonical `surfaces` entries that include surface ids and omit elided html bodies.
5 changes: 5 additions & 0 deletions .changeset/surface-read-indexes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sideshow": patch
---

Expose derived 0-based surface indexes on post detail, session post list, and post history read responses.
2 changes: 1 addition & 1 deletion bin/sideshow.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ usage:
--surface is a deprecated alias)
--author <name> defaults to agent name
sideshow list [--session <id>|--all] list posts
sideshow show <id> show a single post (surfaces, ids, version, history)
sideshow show <id> show a single post (surfaces, indexes, ids, version, history)
sideshow sessions list sessions
sideshow demo seed two example sessions to explore the viewer
sideshow guide print the design contract for posts
Expand Down
14 changes: 6 additions & 8 deletions mcp/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { feedbackView, mcpPostListRowView } from "../server/apiViews.ts";
import {
MCP_INSTRUCTIONS,
MCP_SERVER_INFO,
Expand Down Expand Up @@ -95,7 +96,8 @@ server.registerTool(
{ description: MCP_TOOL_DESCRIPTIONS.listPostsStdio, inputSchema: {} },
async () => {
if (!sessionId) return text([]);
return text(JSON.parse(await api(`/api/sessions/${sessionId}/posts`)));
const rows = JSON.parse(await api(`/api/sessions/${sessionId}/posts`));
return text(rows.map(mcpPostListRowView));
},
);

Expand Down Expand Up @@ -196,12 +198,7 @@ server.registerTool(
return text({ comments: [], note: "no user feedback yet — continue, or wait again later" });
}
return text({
comments: result.comments.map((c: any) => ({
surfaceId: c.postId,
surfaceTitle: c.postTitle,
text: c.text,
at: c.createdAt,
})),
comments: result.comments.map(feedbackView),
});
},
);
Expand All @@ -228,7 +225,8 @@ server.registerTool(
{ description: MCP_TOOL_DESCRIPTIONS.listSurfacesStdio, inputSchema: {} },
async () => {
if (!sessionId) return text([]);
return text(JSON.parse(await api(`/api/sessions/${sessionId}/surfaces`)));
const rows = JSON.parse(await api(`/api/sessions/${sessionId}/surfaces`));
return text(rows.map(mcpPostListRowView));
},
);

Expand Down
218 changes: 218 additions & 0 deletions server/apiViews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import type { Comment, CommentAnchor, Post, Session, Surface } from "./types.ts";

export interface Feedback {
postId: string | null;
postTitle: string | null;
surfaceId: string | null;
surfaceTitle: string | null;
text: string;
at: string;
anchor?: CommentAnchor;
}

export const surfaceRef = (surface: Pick<Surface, "id" | "kind">, index: number) => ({
id: surface.id,
kind: surface.kind,
index,
});

export const fullSurfaceView = (surface: Surface, index: number) => ({ ...surface, index });

// Session REST lists keep non-html structured payloads for the viewer list, but
// omit arbitrary html bodies. Legacy `parts` aliases the same array at the row.
export const sessionListSurfaceView = (surface: Surface, index: number) =>
surface.kind === "html" ? surfaceRef(surface, index) : fullSurfaceView(surface, index);

export const postWriteView = (post: Post) => ({
id: post.id,
sessionId: post.sessionId,
title: post.title,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
version: post.version,
surfaces: post.surfaces.map(surfaceRef),
});

export const postDetailView = (post: Post) => ({
...post,
surfaces: post.surfaces.map(fullSurfaceView),
history: post.history.map((version) => ({
...version,
surfaces: version.surfaces.map(fullSurfaceView),
})),
});

export const sessionPostListRowView = (post: Post) => {
const surfaces = post.surfaces.map(sessionListSurfaceView);
return {
id: post.id,
sessionId: post.sessionId,
title: post.title,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
version: post.version,
surfaces,
parts: surfaces,
};
};

export const mcpPostListRowView = (
post: Pick<Post, "id" | "sessionId" | "title" | "version" | "updatedAt"> & {
surfaces: Pick<Surface, "id" | "kind">[];
},
) => ({
id: post.id,
sessionId: post.sessionId,
title: post.title,
version: post.version,
updatedAt: post.updatedAt,
surfaces: post.surfaces.map(surfaceRef),
});

const PART_TEXT_CAP = 8_000;
const TRACE_STEP_PREVIEW_LIMIT = 25;
type CappedSurface = Surface & { truncated?: true };

function capText(text: string): { value: string; truncated: boolean } {
return text.length > PART_TEXT_CAP
? { value: text.slice(0, PART_TEXT_CAP), truncated: true }
: { value: text, truncated: false };
}

function capSurface(surface: Surface): CappedSurface {
switch (surface.kind) {
case "html": {
const { value, truncated } = capText(surface.html);
return truncated ? { ...surface, html: value, truncated: true } : surface;
}
case "markdown": {
const { value, truncated } = capText(surface.markdown);
return truncated ? { ...surface, markdown: value, truncated: true } : surface;
}
case "mermaid": {
const { value, truncated } = capText(surface.mermaid);
return truncated ? { ...surface, mermaid: value, truncated: true } : surface;
}
case "code": {
const { value, truncated } = capText(surface.code);
return truncated ? { ...surface, code: value, truncated: true } : surface;
}
case "terminal": {
const { value, truncated } = capText(surface.text);
return truncated ? { ...surface, text: value, truncated: true } : surface;
}
case "diff": {
let truncated = false;
const next: CappedSurface = { ...surface };
if (surface.patch !== undefined) {
const capped = capText(surface.patch);
next.patch = capped.value;
truncated ||= capped.truncated;
}
if (surface.files !== undefined) {
next.files = surface.files.map((file) => {
const before = capText(file.before);
const after = capText(file.after);
const filename = capText(file.filename);
const language = file.language ? capText(file.language) : undefined;
truncated ||= before.truncated || after.truncated || filename.truncated;
if (language) truncated ||= language.truncated;
return {
...file,
filename: filename.value,
before: before.value,
after: after.value,
...(language && { language: language.value }),
};
});
}
return truncated ? { ...next, truncated: true } : surface;
}
case "image": {
const alt = surface.alt ? capText(surface.alt) : undefined;
const caption = surface.caption ? capText(surface.caption) : undefined;
const truncated = !!alt?.truncated || !!caption?.truncated;
return truncated
? {
...surface,
...(alt && { alt: alt.value }),
...(caption && { caption: caption.value }),
truncated: true,
}
: surface;
}
case "trace": {
let truncated = false;
const title = surface.title ? capText(surface.title) : undefined;
if (title?.truncated) truncated = true;
const steps = surface.steps?.slice(0, TRACE_STEP_PREVIEW_LIMIT).map((step) => {
const label = capText(step.label);
const kind = step.kind ? capText(step.kind) : undefined;
const detail = step.detail ? capText(step.detail) : undefined;
const ts = step.ts ? capText(step.ts) : undefined;
truncated ||=
label.truncated || !!kind?.truncated || !!detail?.truncated || !!ts?.truncated;
return {
label: label.value,
...(kind && { kind: kind.value }),
...(detail && { detail: detail.value }),
...(ts && { ts: ts.value }),
};
});
if ((surface.steps?.length ?? 0) > TRACE_STEP_PREVIEW_LIMIT) truncated = true;
return truncated
? {
...surface,
...(title && { title: title.value }),
...(steps && { steps }),
truncated: true,
}
: surface;
}
case "json": {
const serialized = JSON.stringify(surface.data);
const { value, truncated } = capText(serialized);
return truncated ? { ...surface, data: value, truncated: true } : surface;
}
default:
return surface;
}
}

export const recentSurfacePreviewView = (surface: Surface, index: number) => ({
...capSurface(surface),
index,
});

export const recentPostRowView = (post: Post, session: Session | null | undefined) => {
const surfaces = post.surfaces.map(recentSurfacePreviewView);
return {
id: post.id,
sessionId: post.sessionId,
sessionTitle: session?.title ?? null,
agent: session?.agent ?? null,
title: post.title,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
version: post.version,
surfaces,
parts: surfaces,
partKinds: post.surfaces.map((surface) => surface.kind),
};
};

export const feedbackView = (comment: Comment): Feedback => ({
postId: comment.postId,
postTitle: comment.postTitle,
surfaceId: comment.postId,
surfaceTitle: comment.postTitle,
text: comment.text,
at: comment.createdAt,
...(comment.anchor && { anchor: comment.anchor }),
});

export const sessionRowView = (session: Session, postCount: number) => ({
...session,
postCount,
surfaceCount: postCount,
});
Loading
Loading