Skip to content

fix(api): consistent, addressable surface ids & indexes across read/write responses#192

Merged
elucid merged 8 commits into
mainfrom
elucid/api-consistency-fixes
Jul 1, 2026
Merged

fix(api): consistent, addressable surface ids & indexes across read/write responses#192
elucid merged 8 commits into
mainfrom
elucid/api-consistency-fixes

Conversation

@elucid

@elucid elucid commented Jun 30, 2026

Copy link
Copy Markdown
Member

Why

While mirroring a session's posts between two sideshow servers, I noticed the API gives three different, partly-inconsistent answers to the same question — "what surfaces does this post have?" depending on which endpoint you hit. The drift made surfaces hard to address for per-surface edits, and one read path returned an ambiguous, lossy shape. This PR converges the read/write responses on one surface model so a surface is consistently identifiable and addressable everywhere.

What changed

Same post (an html + a diff surface), before → after across the affected REST and MCP surfaces. Every nested surface now deliberately uses one of the shared response views: lean { id, kind, index }, full indexed payloads, or indexed preview payloads.

1. POST /api/posts — write response

The response only named the kinds, so to address a surface you'd just created you had to GET the post again to learn its ids.

// before
{
  "id": "29d0P0uH4fQ",
  "version": 1,
  "kinds": [
    "html",
    "diff"
  ]
}

// after — server-assigned ids + indexes handed back (no follow-up GET; kind now lives on each surface)
{
  "id": "29d0P0uH4fQ",
  "version": 1,
  "surfaces": [
    {
      "id": "VrS23LSd45U",
      "kind": "html",
      "index": 0
    },
    {
      "id": "1EFViLFanDs",
      "kind": "diff",
      "index": 1
    }
  ]
}

2. GET /api/sessions/:id/posts — list

Legacy parts key; the html surface lost its id and its body was blanked to "" (indistinguishable from a genuinely empty surface).

// before
{
  "id": "29d0P0uH4fQ",
  "parts": [
    {
      "kind": "html",
      "html": ""
    },
    {
      "kind": "diff",
      "patch": "--- a/x.ts\n+++ b/x.ts\n@@ …",
      "id": "1EFViLFanDs"
    }
  ]
}

// after — canonical `surfaces`; html keeps its id; omitted body is absent (not ""); index added
{
  "id": "29d0P0uH4fQ",
  "surfaces": [
    {
      "id": "VrS23LSd45U",
      "kind": "html",
      "index": 0
    },
    {
      "id": "1EFViLFanDs",
      "kind": "diff",
      "patch": "--- a/x.ts\n+++ b/x.ts\n@@ …",
      "index": 1
    }
  ]
}

(parts stays as a deprecated alias of surfaces.)

3. GET /api/posts/:id — detail

Already canonical, but no surface stated its position — yet edit_surface / remove_surface / --surface <N> take a 0-based index.

// before
{
  "id": "29d0P0uH4fQ",
  "surfaces": [
    {
      "kind": "html",
      "html": "<svg …>",
      "id": "VrS23LSd45U"
    },
    {
      "kind": "diff",
      "patch": "--- a/x.ts\n@@ …",
      "id": "1EFViLFanDs"
    }
  ]
}

// after — derived `index` per surface (computed at serialization, never stored)
{
  "id": "29d0P0uH4fQ",
  "surfaces": [
    {
      "kind": "html",
      "html": "<svg …>",
      "id": "VrS23LSd45U",
      "index": 0
    },
    {
      "kind": "diff",
      "patch": "--- a/x.ts\n@@ …",
      "id": "1EFViLFanDs",
      "index": 1
    }
  ]
}

4. MCP list_posts — tool response

The MCP list tool still returned kinds, even after the write responses and REST reads had moved to addressable surface metadata. It now matches the lean list shape: no surface bodies, but every surface has { id, kind, index }. The deprecated list_surfaces alias follows the same shape.

// before
[
  {
    "id": "29d0P0uH4fQ",
    "sessionId": "sJnFLMTOxB4",
    "title": "Review",
    "kinds": [
      "html",
      "diff"
    ],
    "version": 1,
    "updatedAt": "2026-06-30T12:00:00.000Z"
  }
]

// after — kind moves onto each addressable surface; ids + indexes are present
[
  {
    "id": "29d0P0uH4fQ",
    "sessionId": "sJnFLMTOxB4",
    "title": "Review",
    "version": 1,
    "updatedAt": "2026-06-30T12:00:00.000Z",
    "surfaces": [
      {
        "id": "VrS23LSd45U",
        "kind": "html",
        "index": 0
      },
      {
        "id": "1EFViLFanDs",
        "kind": "diff",
        "index": 1
      }
    ]
  }
]

5. GET /api/surfaces/recent / GET /api/posts/recent — recent feed preview

The cross-session recent feed still used only legacy parts / partKinds, and nested surfaces had no index. It now has canonical surfaces with indexed preview payloads, while keeping parts and partKinds for compatibility. GET /api/posts/recent is a canonical alias with the same auth behavior as /api/surfaces/recent.

// before
[
  {
    "id": "29d0P0uH4fQ",
    "sessionId": "sJnFLMTOxB4",
    "sessionTitle": "Review session",
    "agent": "claude-code",
    "title": "Review",
    "version": 1,
    "partKinds": ["html", "diff"],
    "parts": [
      {
        "kind": "html",
        "html": "<svg …>",
        "id": "VrS23LSd45U"
      },
      {
        "kind": "diff",
        "patch": "--- a/x.ts\n@@ …",
        "id": "1EFViLFanDs"
      }
    ]
  }
]

// after — canonical surfaces first; previews still capped/truncated as before
[
  {
    "id": "29d0P0uH4fQ",
    "sessionId": "sJnFLMTOxB4",
    "sessionTitle": "Review session",
    "agent": "claude-code",
    "title": "Review",
    "version": 1,
    "surfaces": [
      {
        "kind": "html",
        "html": "<svg …>",
        "id": "VrS23LSd45U",
        "index": 0
      },
      {
        "kind": "diff",
        "patch": "--- a/x.ts\n@@ …",
        "id": "1EFViLFanDs",
        "index": 1
      }
    ],
    "parts": [
      {
        "kind": "html",
        "html": "<svg …>",
        "id": "VrS23LSd45U",
        "index": 0
      },
      {
        "kind": "diff",
        "patch": "--- a/x.ts\n@@ …",
        "id": "1EFViLFanDs",
        "index": 1
      }
    ],
    "partKinds": ["html", "diff"]
  }
]

6. GET /api/sessions — post count naming

Sessions were reporting surfaceCount, but the value counts posts. The response now exposes canonical postCount and keeps surfaceCount as the deprecated alias used by existing viewers.

// before
[
  {
    "id": "sJnFLMTOxB4",
    "agent": "claude-code",
    "title": "Review session",
    "surfaceCount": 3
  }
]

// after
[
  {
    "id": "sJnFLMTOxB4",
    "agent": "claude-code",
    "title": "Review session",
    "postCount": 3,
    "surfaceCount": 3
  }
]

7. Agent feedback payloads — canonical post ids plus aliases

Piggyback feedback on write responses and MCP wait_for_feedback still named the target as surfaceId, even though comments attach to posts. Feedback now includes canonical postId / postTitle plus the legacy aliases.

// before
{
  "userFeedback": [
    {
      "surfaceId": "29d0P0uH4fQ",
      "surfaceTitle": "Review",
      "text": "Can you expand the diff?",
      "at": "2026-06-30T12:05:00.000Z"
    }
  ]
}

// after
{
  "userFeedback": [
    {
      "postId": "29d0P0uH4fQ",
      "postTitle": "Review",
      "surfaceId": "29d0P0uH4fQ",
      "surfaceTitle": "Review",
      "text": "Can you expand the diff?",
      "at": "2026-06-30T12:05:00.000Z"
    }
  ]
}

8. Shared response views

The response shapes above now flow through shared helpers in server/apiViews.ts (postWriteView, postDetailView, sessionPostListRowView, mcpPostListRowView, recentPostRowView, feedbackView, sessionRowView) so future nested-surface shape changes are made in one place instead of being patched endpoint-by-endpoint.

@elucid elucid force-pushed the elucid/api-consistency-fixes branch from e8036a9 to b256378 Compare June 30, 2026 23:58
@elucid elucid marked this pull request as draft July 1, 2026 00:18
elucid added 8 commits July 1, 2026 09:14
REST create/update responses now include a lean surfaces array of {id, kind} entries so callers can target server-assigned surface ids without a follow-up read.

The existing kinds array stays as a deprecated compatibility alias because the CLI publish output and its tests still consume it.
The session posts list now exposes the canonical surfaces key with one shape: each entry has id and kind, non-html payloads remain available, and elided html bodies omit the html key instead of sending an empty string.

The legacy parts key remains as an alias because sideshow list emits this endpoint's raw JSON and external CLI users may already consume that field; viewer code only reads ids from the list before fetching full post details.
Post detail responses now serialize current and historical surfaces with a derived zero-based index, and session post lists include the same derived index even when html bodies are elided.

The index is computed at response serialization time and is not stored or accepted in write bodies. Lean write receipts remain limited to {id, kind}; they identify minted surfaces but are not positional read payloads.
Make the agent-facing docs reflect the API changes already shipped in
this branch: the show CLI help and the get_post MCP description now list
surface indexes (the coordinate the --surface <N> / edit_surface target
commands consume), and the publish/update MCP descriptions note they
return the new surface ids (so an agent can target a surface without a
get_post round-trip).

No changeset: this only makes docs match behavior already covered by the
api-write-surface-ids and surface-read-indexes changesets.
@elucid elucid force-pushed the elucid/api-consistency-fixes branch from 7d0af9d to 7cfd380 Compare July 1, 2026 16:18
@elucid elucid marked this pull request as ready for review July 1, 2026 16:19
@elucid elucid requested a review from benvinegar July 1, 2026 16:20
@elucid elucid merged commit 2bc9db6 into main Jul 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant