Skip to content

Show a draft's figures, embeds and videos, and let drafts use them - #312

Merged
egeozin merged 1 commit into
feat/page-bundles-contractfrom
feat/page-bundles-reader
Oct 5, 2026
Merged

egeozin merged 1 commit into
feat/page-bundles-contractfrom
feat/page-bundles-reader

Conversation

@egeozin

@egeozin egeozin commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

What and why

Page bundles, part 1b: the experience. It builds on #311 (the contract), so merge #311 first. Plan: https://with.md/s/6bxvmfrh

#311 lets an article draft carry figures and embeds and have them approved with the article. This PR shows them to the founder exactly as they'll ship, says what a draft carries in Decisions, and turns the feature on for the two article workflows.

Reader

  • render_markdown(markdown, asset_folder=…): with an article's assets folder, its blocks render as placeholders the viewer fills in. Every other document renders as before, except that > [!NOTE] quotes now become callouts everywhere.
    • Figures (![alt](./<folder>/x.svg "caption")) render without a URL. The viewer loads them with the member's session through the raw file endpoint, and an SVG becomes an inert data: image, as Files already does.
    • Embeds (a tin-embed fenced block) run in an <iframe sandbox="allow-scripts" srcdoc> with a Content-Security-Policy meta tag injected first: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; media-src data: blob:. That gives an opaque origin with no Tin cookies, storage or page, and no network. A src outside the folder stays a code block.
    • Videos (a tin-video fenced block):
      • YouTube (privacy-enhanced domain), Vimeo, Loom and Mux play in their own player;
      • a video file (.mp4, .webm, .m4v, .mov) plays in place with preload="none", which suits a 97 MB trailer;
      • any other address stays a link, and non-http(s) addresses stay code.
    • Callouts: > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] and [!CAUTION] become a soft labelled panel, with no colored edge.
    • Left-out files: a note under the title names any file Tin left out and why, e.g. "unsafe.svg was left out: the SVG contains script, event handlers or outside references."
  • Endpoints: the run document endpoint returns the kept assets (from the published checkpoint) and asset_notes. The Files document endpoint returns the referenced files that exist at that revision, so the same article reads the same in Files.

Decisions

The review explanation says what the draft carries besides its words, e.g. "With 2 figures, 1 interactive piece and 1 video." Diagrams count as figures.

Drafts

  • Versions: content.public_article 1.9.0 and content.generate 1.10.0 declare an assets folder (12 files, 2 MB).
  • A new page-figures skill in both packages:
    • add a figure only where it shows what the words can't;
    • Mermaid for diagrams, in the brand's colours;
    • SVG figures with no script or outside references;
    • interactive pieces as self-contained HTML with no network;
    • only videos that already exist at a public address, never invented;
    • one or two callouts at most;
    • keep the figures of an existing page instead of describing them;
    • list every figure in Generation notes.
  • content.generate applies it to articles only. Answer pages and page refreshes keep their strict formats.

Rollout

  1. Merge and deploy Let article drafts carry figures and embeds, approved with the article #311.
  2. Rebuild the Codex sandbox templates. Without the rebuild, a draft run with an assets folder stops at --check-assets before any model call. The rebuild also activates Finish runs instead of crashing on recoverable checks, and write down how to fail well #298's task-bridge fix.
  3. Deploy this PR.

Delivery doesn't carry bundles yet; that's plan PR 2 (content.deliver and website.change map the blocks to the site's components). Until it lands, an approved draft with figures delivers its words, and its figures stay in Tin. So I'd ship plan PR 2 soon after, or hold delivering bundle drafts until then.

No migration and no Temporal change. Runs keep the versions they pinned.

Verification

  • tests/test_page_assets.py, new here:
    • Server rendering of every block: escaping, the height clamp, a folder escape and a javascript: URL kept as code, both callout forms, and other documents unchanged.
    • Through the real endpoints on the bundle fixture: the run document returns the kept assets, the notes for the two left-out files and the placeholders, and Files returns the same assets.
    • The Decisions explanation reads "With 1 figure and 1 interactive piece."
  • web/page-bundles.browser.test.js, in Chromium:
    • The SVG figure becomes a data: image with its caption, and a missing one shows its placeholder.
    • The embed's own script reports that it can't read Tin's page, cookies or storage. Its fetch is blocked, and neither the fetch nor the tracking image reaches the server.
    • YouTube plays from the privacy-enhanced domain, the film never preloads, and an unknown host stays a link.
    • The callout and the note render, and the screenshot was reviewed by eye.
  • Pins updated: the version pins for both workflows. The shared draft fixture's historical contracts drop the assets folder, as they never had one.
  • Python suite: 5,283 passed, 22 skipped against a disposable Postgres. The 2 macOS-only localtime tests fail as on main.
  • Browser suite: 129 of 129. ruff, format, lint-imports and dump_catalog.py --check are clean.

A real run comes after the template rebuild: a draft with figures, a diagram, an embed and a video, reviewed in Tin.

🤖 Generated with Claude Code

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@egeozin
egeozin requested a review from a team as a code owner October 5, 2026 20:01
@egeozin
egeozin merged commit 4b286c7 into feat/page-bundles-contract Oct 5, 2026
9 of 11 checks passed
egeozin added a commit that referenced this pull request Oct 5, 2026
# Conflicts:
#	tests/test_page_assets.py
egeozin added a commit that referenced this pull request Oct 5, 2026
#311)

* Let article drafts carry figures and embeds, approved with the article

Page bundles, part 1a (the contract). An article draft whose definition declares
output.assets may write SVG figures and self-contained HTML embeds into its
assets folder (<article>.assets/) and refer to them from the Markdown.

- The article declares its assets: Tin keeps exactly the files it refers to,
  records their checksums with the checkpoint and publishes them with the
  article. A missing, oversized or unsafe file (an SVG with script, event
  handlers or outside references) is left out with its reason; it never fails
  the run. Files nothing refers to are never published.
- The sandbox runner allows files directly in the folder, discards anything
  else there, applies runaway limits and the identity/card leak scan, and
  commits them; --check-assets lets the host refuse an old image before any
  model call.
- The review token binds the asset list with the article; approved-page
  consumers re-check each file; a revision copies the files it keeps into its
  own folder; compare lists each version's assets.
- The delivery wording check skips embed, video and diagram blocks.

No workflow turns this on yet: definitions without assets keep their exact
contract and tokens. The sandbox templates need a rebuild before part 1b
enables it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Show a draft's figures, embeds and videos, and let drafts use them (#312)

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
egeozin added a commit that referenced this pull request Oct 5, 2026
* Let article drafts carry figures and embeds, approved with the article

Page bundles, part 1a (the contract). An article draft whose definition declares
output.assets may write SVG figures and self-contained HTML embeds into its
assets folder (<article>.assets/) and refer to them from the Markdown.

- The article declares its assets: Tin keeps exactly the files it refers to,
  records their checksums with the checkpoint and publishes them with the
  article. A missing, oversized or unsafe file (an SVG with script, event
  handlers or outside references) is left out with its reason; it never fails
  the run. Files nothing refers to are never published.
- The sandbox runner allows files directly in the folder, discards anything
  else there, applies runaway limits and the identity/card leak scan, and
  commits them; --check-assets lets the host refuse an old image before any
  model call.
- The review token binds the asset list with the article; approved-page
  consumers re-check each file; a revision copies the files it keeps into its
  own folder; compare lists each version's assets.
- The delivery wording check skips embed, video and diagram blocks.

No workflow turns this on yet: definitions without assets keep their exact
contract and tokens. The sandbox templates need a rebuild before part 1b
enables it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Show a draft's figures, embeds and videos, and let drafts use them

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Deliver a page's figures, embeds, diagrams and videos to the site

Page bundles, part 2, on top of #312.

- content.deliver 1.7.0 and website.change 1.6.0 copy each approved asset
  from the project checkout, now pinned to the approved revision when a page
  has assets, and show it the way the site shows media; Mermaid blocks become
  SVG with the sandbox's offline renderer unless the site has its own diagram
  support; videos and callouts use the site's own components.
- figure_check beside copy_check: approved files byte for byte, videos by
  address or ID, diagrams as new SVG or the site's Mermaid support. Unconfirmed
  holds only Tin's own merge; the PR still opens.
- Limits: one pull request or commit may hold 30 files and 2 MB (it held 10
  files and 512 KB, which also refused technical batches the procedure allowed
  at 20 files); a saved patch reads back up to 4 MB. Procedures that declare
  nothing keep their 512 KB contract.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Show a draft's figures, embeds and videos, and let drafts use them (#312)

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants