Conversation
Every surface on a card gets a row in its share menu — "Download diagram.mmd", "Download change.patch", "Download plan.md" — serving the RAW SOURCE behind it rather than a rendering. A diagram comes back editable and a diff comes back appliable; the rendered form already has a door in "Open as image", and a picture of a diagram is not a diagram. GET /api/posts/:id/surfaces/:target/raw serves the bytes, addressed by surface id or 0-based index like the PATCH/DELETE surface routes. It is always an attachment with nosniff, the content types are inert, and an html surface degrades to application/octet-stream — agent-authored content must never become a live document on the workspace origin. Asset-backed surfaces (image, and trace with an uploaded file) redirect to /a/:id, which already serves blobs under that policy. surfaceDownload.ts is runtime-agnostic so the Worker DO serves the route too, and the viewer imports surfaceDownloadName from it — the menu row's label and the file on disk come from one naming rule, not two that drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AZe6cmKF4ZCjwRZN1w8Yn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every surface on a card gets a row in its share menu — "Download diagram.mmd", "Download change.patch", "Download plan.md" — serving the raw source behind it rather than a rendering. A diagram comes back editable and a diff comes back appliable; the rendered form already has a door in "Open as image", and a picture of a diagram is not a diagram.
Motivation: posts age out by LRU eviction, and there was no way to keep the valuable ones except copying the whole post as markdown. A surface you can save is a surface you can keep.
What's in it
GET /api/posts/:id/surfaces/:target/raw— the bytes, addressed by surface id or 0-based index, the same addressing thePATCH/DELETEsurface routes take. So the CLI and curl tiers get this too, not just the viewer.mermaid → .mmd,markdown → .md,diff → .patch(built fromfilesviaunifiedDiffwhen the surface carries no patch),code →its own filename or the language's extension,terminal → .txt(ANSI kept — the escapes are the output),json → .json,html → .html. Asset-backed surfaces (image, and trace with an uploaded file) redirect to/a/:idrather than re-encoding the blob.Isolation
Always an attachment with
nosniff; the content types are inert and an html surface degrades toapplication/octet-stream, so agent-authored content can never become a live document on the workspace origin. Same stance as the asset route'sATTACH_SAFE_TYPES. The redirect path reuses/a/:id, which already serves blobs under that policy and keeps the asset LRU touch-on-serve honest.server/surfaceDownload.tsis runtime-agnostic (nonode:imports, no DOM) so the Worker DO serves the route too, and the viewer importssurfaceDownloadNamefrom it — the menu row's label and the file on disk come from one naming rule rather than two that drift.Verification
npm test(518 node + 8 viewer),npm run test:worker,npm run typecheck,npm run lint,npm run format:check,npm run coverage,npm run bench:check(no regressions), andnpm run test:e2e— 186 passing on chromium and webkit, including a newe2e/download.spec.tswhose oracle is the saved file itself: it triggers the download and asserts the filename and the bytes on disk.npm run security:auditfails, unchanged frommain— no dependencies were touched.Known follow-up
There is no
sideshow download <id>CLI command yet. The HTTP tier has the route, so curl works; the zero-dependency CLI tier deserves a verb of its own in a separate change.🤖 Generated with Claude Code
https://claude.ai/code/session_015AZe6cmKF4ZCjwRZN1w8Yn