Skip to content

Latest commit

 

History

History
624 lines (455 loc) · 24.1 KB

File metadata and controls

624 lines (455 loc) · 24.1 KB
name pagedrop
description Create shareable HTML pages for instant mobile preview and visual collaboration. Use when building documents, visualizations, or anything that needs structure — view it in a browser from any device, then iterate with Google Docs-style annotations.

Pagedrop

Escape the chat window. Render complex content as a shareable web page, get structured feedback via annotations.

Workflow

1. Create the HTML

Write self-contained HTML (inline CSS/JS). Save to a persistent local file using the naming convention:

/tmp/<date>-<subject>-<gist_id>.html
  • <date> — today's date, e.g. 2026-02-05
  • <subject> — short slug describing the content, e.g. api-architecture, quarterly-report
  • <gist_id> — the GitHub Gist ID (assigned after first upload). On initial creation, omit this and add it after uploading.

Why: Using the Gist ID in the filename creates a direct lookup — when annotation feedback references a gist ID, you can find the exact local file to edit. This also lets you make targeted edits instead of regenerating the entire document, which is faster and cheaper.

Default template — use Pico CSS dark theme as the base:

cat > /tmp/2026-02-05-my-preview.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Preview</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
</head>
<body>
  <main class="container">
    <!-- content -->
  </main>
</body>
</html>
HTMLEOF

Default share path: use /g/USER/GIST_ID. This is the normal Pagedrop route; it injects the revision and annotation UI at the edge.

Large-file fallback: if a drop is too large for /g/ injection or returns a 503, use /h/USER/GIST_ID. The /h/ route streams the raw gist HTML without edge body rewriting, so the HTML itself must include the Pagedrop bootstrap script before </body>:

<script src="https://pagedrop.ai/pagedrop.js"></script>

Because /g/ and /h/ serve the same gist, it is safe for large drops to keep this script in the HTML even when someone opens the /g/ URL. The /g/ injector avoids double-loading Pagedrop UI when the bootstrap script is already present. Do not remove it from /h/ drops unless the author explicitly wants to forego annotations, auth, sharing, and revision UI.

2. Upload and share

gh gist create /tmp/2026-02-05-my-preview.html -d "Description #pagedrop"

Always include #pagedrop in the description — this tags it as a drop so it appears in the user's profile.

After creating, capture the Gist ID from the output URL and rename the local file:

mv /tmp/2026-02-05-my-preview.html /tmp/2026-02-05-my-preview-abc123def456.html

Now the filename contains the Gist ID — a direct lookup when annotation feedback references this gist.

Note: Gists are created as secret by default (not listed on profile, not indexed). Anyone with the pagedrop.ai link can still view.

2a. Host Images/Assets (Public GitHub Repo)

If your drop needs images or other static assets, put them in a public GitHub repo (so viewers can load them). Convention:

  • drops/<gist_id>/... (no timestamps)

Copy/paste (example uses placeholders):

# 0) Set your gist id (from https://gist.github.com/<USER>/<GIST_ID>)
export GIST_ID="<GIST_ID>"

# 1) Create (or choose) a public assets repo (one-time; skip if it already exists)
export USER="<USER>"
export REPO="<REPO>" # e.g. "pagedrop-assets"
gh repo create "$USER/$REPO" --public --confirm

# 2) Clone it locally (one-time) into any folder you want
export ASSETS_DIR="/path/to/local/assets-repo"
git clone "git@github.com:$ORG/$REPO.git" "$ASSETS_DIR"

# 3) Add assets for this drop
cd "$ASSETS_DIR"
mkdir -p "drops/$GIST_ID"
cp "/path/to/<FILE>" "drops/$GIST_ID/"

# 4) Commit + push
git add "drops/$GIST_ID"
git commit -m "Add assets for $GIST_ID"
git push

Raw URL pattern (replace the filename):

  • https://raw.githubusercontent.com/<USER>/<REPO>/<BRANCH>/drops/<GIST_ID>/<FILE>
  • Or: open the file on GitHub and click Raw

In your HTML:

<img src="https://raw.githubusercontent.com/<USER>/<REPO>/<BRANCH>/drops/<GIST_ID>/<FILE>" alt="Example">

In your Markdown:

![Example](https://raw.githubusercontent.com/<USER>/<REPO>/<BRANCH>/drops/<GIST_ID>/<FILE>)

Convert the gist URL to a pagedrop URL:

  • Gist: https://gist.github.com/USER/GIST_ID
  • Pagedrop: https://pagedrop.ai/g/USER/GIST_ID (default)
  • Large/passthrough Pagedrop: https://pagedrop.ai/h/USER/GIST_ID (use when /g/ is too large or returns 503; requires the bootstrap script above)

Send the pagedrop.ai URL. Works on phone, tablet, desktop.

3. Share Links

For sharing with others, use the Share button in the revision bar to generate a share link:

  • /s/TOKEN — share link with configurable settings

Share links let you control what viewers see:

  • Show annotations — enable/disable annotation UI
  • Show revisions — enable/disable revision navigation

Or create programmatically:

curl -X POST "https://pagedrop.ai/api/share" \
  -H "Content-Type: application/json" \
  -d '{"gist_user": "USER", "gist_id": "GIST_ID", "settings": {"annotations": true, "revisions": true}}'

4. Iterate

Edit the local file in place (use targeted edits — no need to rewrite the whole document), then push the update. The -f flag takes the gist filename (not the local path), followed by the local source file:

gh gist edit GIST_ID -f my-preview.html /tmp/2026-02-05-my-preview-GIST_ID.html
#                       ^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                       gist filename     local source file

Tip: If you're unsure of the gist filename, list it first with gh gist view GIST_ID --files.

Key: Because the file persists at a known path, you can make surgical edits to specific sections rather than regenerating the full HTML each turn. This saves tokens and preserves any manual tweaks.

GitHub keeps all revisions — accessible via the revision bar or API.

Cache behavior:

  • /g/USER/GIST_ID — latest version (cached ~5 min)
  • /g/USER/GIST_ID/SHA — specific revision (cached longer, immutable)
  • /h/USER/GIST_ID — large-file passthrough route; requires the drop HTML to include https://pagedrop.ai/pagedrop.js
  • /s/TOKEN — share link (cached ~1 min)

Recommended Libraries

Only include libraries when the content needs them. All available via CDN — no build step.

🎨 Pico CSS — Base Styling (always include)

Classless semantic CSS. Write HTML, get beautiful output. Default to dark theme.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">

Theming:

  • <html data-theme="dark"> — dark theme (default for drops)
  • <html data-theme="light"> — light theme
  • <html data-theme="auto"> — follows OS preference

Pico handles typography, tables, forms, buttons, cards, and layout with zero classes. Just use semantic HTML (<table>, <article>, <details>, <form>, etc.).

When to use: Always. It's the base layer.

When to skip: Only if the content demands a fully custom aesthetic (dashboards, landing pages with specific brand styling).

🧜 Mermaid — Architecture & Flow Diagrams

Flowcharts, sequence diagrams, ERDs, state machines, Gantt charts, and more.

<script type="module">
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
  mermaid.initialize({ startOnLoad: true, theme: 'dark', securityLevel: 'loose' });
</script>

Usage in HTML:

<pre class="mermaid">
graph TD
    A[User] -->|visits| B[CloudFront]
    B --> C[Lambda@Edge]
    C --> D[GitHub Gist]
</pre>

Theme consistency: Use theme: 'dark' with Pico dark, theme: 'default' with Pico light.

When to use: Architecture diagrams, flowcharts, sequence diagrams, ERDs, state machines, timelines.

📊 Chart.js — Charts & Plots

Simple, responsive charts with beautiful defaults.

<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>

Usage:

<canvas id="myChart"></canvas>
<script>
new Chart(document.getElementById('myChart'), {
  type: 'bar',  // bar, line, pie, doughnut, radar, scatter, bubble
  data: {
    labels: ['Jan', 'Feb', 'Mar'],
    datasets: [{
      label: 'Revenue',
      data: [12, 19, 3],
      backgroundColor: 'rgba(88, 166, 255, 0.7)',
      borderRadius: 6,
    }]
  },
  options: {
    responsive: true,
    plugins: { legend: { labels: { color: '#c9d1d9' } } },
    scales: {
      x: { ticks: { color: '#8b949e' }, grid: { color: '#30363d' } },
      y: { ticks: { color: '#8b949e' }, grid: { color: '#30363d' } }
    }
  }
});
</script>

Theme consistency (dark): Set color: '#c9d1d9' for labels, color: '#8b949e' for ticks, color: '#30363d' for grid lines. For light theme, omit these (Chart.js defaults are light-friendly).

When to use: Bar charts, line charts, pie/doughnut, scatter plots, radar charts. Any time there's numeric data to visualize.

💻 Prism.js — Syntax Highlighting

VS Code-quality code rendering. 300+ languages.

<!-- Dark theme (matches Pico dark) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css">

<!-- Light theme (matches Pico light) -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"> -->

<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
<!-- Add languages as needed -->
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-typescript.min.js"></script>

Usage:

<pre><code class="language-javascript">
const greeting = "Hello, world!";
console.log(greeting);
</code></pre>

Theme consistency:

  • Pico dark → prism-tomorrow.min.css (dark background, light text)
  • Pico light → prism.min.css (light background, dark text)

When to use: Any content containing code snippets. Skip if there's no code.

🧮 KaTeX — Math Rendering

Fast LaTeX math. Way faster than MathJax.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>

Usage:

<!-- Display math -->
<div id="equation"></div>
<script>
  katex.render('E = mc^2', document.getElementById('equation'), { displayMode: true });
</script>

<!-- Or use auto-render for LaTeX in text -->
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script>renderMathInElement(document.body);</script>

With auto-render, use $$...$$ for display math and \(...\) for inline math directly in HTML text.

Theme consistency: KaTeX inherits text color from CSS — works with both Pico dark and light automatically.

When to use: Math equations, formulas, technical papers, scientific content. Skip if there's no math.


Theme Consistency Cheat Sheet

Library Dark mode setting Light mode setting
Pico CSS data-theme="dark" data-theme="light"
Mermaid theme: 'dark' theme: 'default'
Chart.js Custom colors (see above) Default colors
Prism.js prism-tomorrow.min.css prism.min.css
KaTeX Automatic (inherits) Automatic (inherits)

Rule: Pick dark or light, then match ALL libraries to that choice. Don't mix.


Library Selection Guide

Include only what the content needs:

Content type Libraries to include
Text/docs only Pico CSS
Has code snippets + Prism.js
Has data/metrics + Chart.js
Has architecture/flows + Mermaid
Has math/equations + KaTeX
Dashboard with custom look Skip Pico, use custom CSS

Size budget (gzipped):

Library Size
Pico CSS ~10 KB
Mermaid ~300 KB
Chart.js ~70 KB
Prism.js ~6 KB + langs
KaTeX ~90 KB

A typical drop with all libraries loads in <500 KB. Most drops need only Pico + one other.


Experimental interactive agent inputs

This feature is experimental and optional. Base PageDrops do not require a receiver, capability, or any of the scripts in this section. Use it only when a PageDrop needs buttons, choices, or form fields whose results return to the agent that created it.

Choose the interaction mode per page:

  • Editorial: Use PageDrop's injected annotation UI. The reviewer clicks Finish and copies the structured feedback into chat. No receiver is required.
  • Action: A button or form submits explicit answer fields to the bound agent session.
  • Action with feedback: The same submission includes annotations saved for the active PageDrop revision.

Do not add interactive machinery to an editorial page. A normal PageDrop remains a gist URL plus the injected annotation UI.

Ownership

PageDrop serves stateless HTML. The creating agent owns the receiver, capability state, submission queue, and session delivery.

  • Do not require PageDrop to host a relay or store conversation data.
  • Do not expose the agent Gateway, Gateway token, vault secret, or host administration route.
  • Treat the PageDrop link and embedded capability as bearer access.
  • Treat gist IDs and revision IDs as public identifiers, not signing keys.
  • Treat every submission as untrusted input. A submission never authorizes an external mutation by itself.

Bundled receiver

Use scripts/interactive-receiver.mjs on macOS or Linux when the deployment has no receiver. It requires Node.js 22.13 or newer and has no package dependencies. It uses Node's built-in SQLite module for transactional capability consumption and queueing.

The script:

  • binds to loopback by default;
  • mints 256-bit bearer capabilities and stores only their SHA-256 hashes;
  • binds gist, agent, session, fields, expiry, use count, and optional sealed revision;
  • validates size, shape, gist, revision, expiry, revocation, replay, and one-shot consumption;
  • writes accepted submissions to a private local queue;
  • delivers queued input to its receiver-bound OpenClaw session through the authenticated loopback Gateway RPC and requests normal channel delivery;
  • provides next and ack commands for diagnostics and non-OpenClaw adapters.

Start locally:

node scripts/interactive-receiver.mjs init --state ~/.pagedrop-receiver
node scripts/interactive-receiver.mjs serve --state ~/.pagedrop-receiver --port 8787

Expose it through the deployment's existing TLS reverse proxy or approved tunnel. Publish only POST /c/<capability>; keep administration and queue access local.

Mint a capability:

node scripts/interactive-receiver.mjs mint \
  --state ~/.pagedrop-receiver \
  --base-url https://interact.example.com \
  --gist GIST_ID \
  --agent AGENT_ID \
  --session SESSION_KEY \
  --fields choice,comment \
  --required choice \
  --annotations optional \
  --ttl 1h \
  --uses 1 \
  --output ~/.pagedrop-receiver/mint.json

The output file is mode 0600 and contains bearer material. Read capabilityUrl from it for the PageDrop. Do not log or commit the file; remove it after publishing and sealing.

After the final gist update, bind the capability to that revision:

node scripts/interactive-receiver.mjs seal \
  --state ~/.pagedrop-receiver \
  --token-file ~/.pagedrop-receiver/mint.json \
  --revision GIST_REVISION

A second invocation of the same script delivers queued submissions to OpenClaw:

node scripts/interactive-receiver.mjs run --state ~/.pagedrop-receiver

Set --annotations to none, optional, or required. The default is none. Use optional for a button that can send answers with any annotations the reviewer has added. Use required only when the action is meaningless without annotation feedback.

run calls the local openclaw gateway call chat.send RPC with deliver: true and the receiver-owned agent and session binding. This starts the agent turn and routes its normal final reply through the session's bound channel. It deletes a queue item only after the Gateway accepts the request. The Gateway credential stays in OpenClaw's local configuration or environment and never enters the PageDrop or receiver database. Use --openclaw /path/to/openclaw when the binary is not on PATH.

next and ack remain available for diagnostics or for a deployment with a different supported session-ingress adapter.

Set up a deployment-specific receiver

Before generating interactive HTML:

  1. Inspect the agent deployment for an approved inbound service and a supported local mechanism that can enqueue input into the intended agent session.
  2. If no receiver exists, provision a small deployment-owned service beside the agent Gateway. Give it:
    • TLS on one narrow public route such as POST /c/<capability>;
    • no host-control socket, Gateway credential, or unrelated mount;
    • a private state store for capabilities and accepted submissions;
    • exact request-size, schema, expiry, and rate limits;
    • CORS restricted to the PageDrop origin when browser response handling requires CORS.
  3. Store each capability as a hash plus:
    • destination agent and session;
    • gist ID;
    • interaction generation or sealed revision;
    • accepted payload schema;
    • expiry and revocation state;
    • one-shot or multi-submit policy.
  4. Use at least 256 bits of random capability material. Put the bearer capability only in the PageDrop HTML. Store its hash at the receiver.
  5. Accept a submission in one atomic operation:
    • find and validate the capability;
    • validate content type, size, and schema;
    • reject expired, revoked, stale, or consumed capabilities;
    • deduplicate submissionId;
    • store the bounded payload;
    • consume one-shot capabilities;
    • enqueue the input for the bound agent session.
  6. For OpenClaw, run the bundled script's run mode to submit through chat.send with external delivery enabled. For another runtime, use its supported loopback or session-ingress mechanism. Do not expose the Gateway endpoint publicly.

The receiver may be a sidecar, host service, or managed function owned by that deployment. Match the deployment's isolation and recovery model.

Avoid the revision cycle

Embedding a capability creates a new gist revision. Use one of these contracts:

  • Bind the capability to the gist ID plus a receiver-issued interaction generation.
  • Mint the capability before the final gist update, publish the final HTML, then seal the capability to the resulting revision through an authenticated receiver administration call.

The browser sends the gist ID and revision as claims. The receiver checks them against its stored binding; they do not authenticate the request.

Wire the PageDrop

For interactive pages, inline scripts/interactive-page.js before the page-specific script. The helper derives the gist and revision from PageDrop's injected revision metadata. It exposes:

  • PagedropInteraction.submit(...) for capability submission;
  • PagedropInteraction.annotations() for normalized annotations saved in the active browser for the active revision.

An answer-only button:

await PagedropInteraction.submit({
  capabilityUrl: CAPABILITY_URL,
  answers: { choice: "approve" },
});

A button that also sends the reviewer's annotations:

await PagedropInteraction.submit({
  capabilityUrl: CAPABILITY_URL,
  answers: { choice: "revise", comment: document.querySelector("#comment").value },
  includeAnnotations: true,
});

includeAnnotations reads the annotation data that PageDrop saved for this browser and revision. Signed-in persistent annotations are available after PageDrop loads them into the page. If the helper cannot find or parse annotation state, it submits an empty annotation list; a capability minted with --annotations required rejects that request.

Generate a fresh browser submission ID for retries and double-click deduplication:

const submissionId = crypto.randomUUID();

const response = await fetch(CAPABILITY_URL, {
  method: "POST",
  headers: {"Content-Type": "application/json"},
  body: JSON.stringify({
    submissionId,
    gistId: GIST_ID,
    revision: GIST_REVISION,
    answers,
  }),
});
  • Label all controls.
  • Show pending, accepted, and failed states.
  • Disable one-shot controls after an accepted response.
  • Send only explicit fields. Never serialize page or application state wholesale.
  • If the receiver does not support CORS and the request uses mode: "no-cors", the browser cannot prove acceptance. Say “submitted,” then confirm receipt through the agent conversation.
  • A random submissionId prevents duplicate processing only because the receiver remembers accepted IDs. The PageDrop remains stateless.

Deliver to the agent

Convert an accepted payload into a bounded inbound message that includes:

  • interaction source and gist ID;
  • capability or interaction ID without bearer material;
  • submission ID;
  • validated answers;
  • receipt time.

Route it only to the capability's bound agent and session. Preserve the normal approval gate for any consequential action requested by the answers.

Temporary proof mode

When proving the interaction before a deployment-owned receiver exists, use a disposable external endpoint plus outbound polling:

  1. Create an unguessable temporary endpoint with expiry.
  2. Embed only its bearer URL in the PageDrop.
  3. Poll it outbound from the agent deployment.
  4. Deduplicate the received submission.
  5. report the exact marker to the originating conversation.
  6. Remove the poller and expire the endpoint.

This proves PageDrop HTML wiring. It is not the deployment receiver architecture.

Verify the round trip

Before sharing the PageDrop broadly:

  1. Submit a unique marker from the rendered page.
  2. Confirm the receiver records the exact marker, gist binding, revision or interaction generation, and unique submission ID.
  3. Confirm the bound agent session receives the validated result.
  4. Replay the same submission and verify deduplication.
  5. For one-shot forms, submit a second ID and verify the consumed capability is rejected.
  6. Verify expiry and revocation.
  7. Verify oversized, malformed, wrong-schema, wrong-gist, and wrong-session submissions fail closed.
  8. Inspect the HTML, browser request, receiver logs, and chat output for leaked bearer material, Gateway credentials, host addresses, or unrelated data.
  9. Record the receiver in the deployment's recovery product and re-run the canary after upgrades or restore.

Annotations

The annotation UI is automatically injected on pagedrop.ai pages:

  1. User selects text → "Annotate" button appears
  2. Add comment → saved with location context
  3. Click "Finish" → exports structured markdown
  4. Paste in chat → agent addresses each point

Export Format

## Preview Feedback
**Preview:** Document Title
**Date:** 2/1/2026, 7:44:30 PM
**Annotations:** 3

---

### 1. [by @jalehman]
📍 Section: "Performance Results"

> ...reduced latency across all endpoints. Redis caching reduced validate latency by 87%, making auth nearly invisible...

This is the key win — call it out more prominently!

---

### 2. [by @collaborator]

> Sort key: UUID annotation ID

Another annotation

---

The format includes:

  • Author attribution ([by @username] or [by Anonymous])
  • Location context (section heading, table position, code block)
  • Selected text as blockquote
  • User's comment

Notes

  • Self-contained HTML — inline all CSS/JS to avoid CORS issues (CDN links are fine)
  • Secret gists — not on profile or indexed, but anyone with the pagedrop.ai link can view
  • Annotations — auto-injected by pagedrop.ai. Persistent for Pro users, localStorage for free
  • Mobile-friendly — annotation button positioned for thumb reach
  • You own your content — gists stay in your GitHub, pagedrop just proxies
  • Share links — control annotations/revisions visibility for viewers