Skip to content

FE-1669: Share Petrinaut snapshots through self-contained URLs - #9680

Open
kube wants to merge 3 commits into
codex/fe-1660-readonly-feedbackfrom
codex/fe-1669-snapshot-sharing
Open

kube wants to merge 3 commits into
codex/fe-1660-readonly-feedbackfrom
codex/fe-1669-snapshot-sharing

Conversation

@kube

@kube kube commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Saved nets live in one browser, so their local URLs only work there. This PR adds Share, which builds a link that carries a copy of the whole net, with the current view if wanted.

Recipients open the snapshot read-only at /share and can save an editable local copy. Nothing is uploaded, and later edits to the sender's net do not change the link.

9680.mp4

Links

Changes

Snapshot format

  • /share#v1.br.<payload> link carries the net

    Payload is compact canonical JSON, Brotli quality 11, unpadded base64url.
    v1.br. prefix versions the format.

  • Worker compresses and decompresses

    Each worker ends on completion, on cancellation, or after 30 seconds.

  • Limits of 2 MiB per net and 16,000 characters per fragment

    Decoding enforces the size limit while it streams.
    A larger net is offered as a YAML download.

  • Sentry strips the fragment from events, transactions, spans and breadcrumbs

Website

  • Share icon in a local document's top bar

    Opens a dropdown with the link and Copy, Include current view, and Download.
    A message appears only when preparing or copying fails.

  • /share opens the snapshot read-only and saves nothing

    Make a local copy saves it at its own /local/<net id> URL with the current view.

  • Examples and snapshots share one read-only page without Share

    Their own URL is the link.

Review fixes

  • Sharing stays out of the Petrinaut packages

    No user-guide page, assistant catalog entry or changeset.
    The website README documents snapshot links.

  • Share is a ghost icon button that opens a dropdown

Test coverage

  • snapshot.test.ts:

    Every core example round-trips; malformed, future, truncated and oversized links fail.

  • snapshot-client.test.ts:

    Worker release, cancellation, a stalled worker stopped after 30 seconds.

  • -share.test.tsx:

    Opening without saving, view changes, Back and Forward, a new fragment, local copy.

  • share-snapshot-button.test.tsx:

    Fixed capture, optional view, file fallback, clipboard failure, cancellation.

  • strip-snapshot-links.test.ts:

    Payloads removed from URLs, breadcrumbs and span attributes; other URLs untouched.

  • Verified in a browser against the dev server:

    Share from a local net, open the link, read-only page offering only Make a local copy.

How to test

  • Open Petrinaut preview on Vercel
  • Switch to Simulate > Scenarios
  • Share
    • Copy
  • Open link in a private window

    Expect snapshot in Scenarios view, read-only

  • Drag a place

    Expect read-only notice with Make a local copy

  • Make a local copy

    Expect editable copy at a new /local/<id> URL

  • Share
    • Untick Include current view

      Expect link without query parameters

@kube kube self-assigned this Sep 11, 2026
@vercel

vercel Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hash Ready Ready Preview Oct 9, 2026 8:28pm UTC
petrinaut Ready Ready Preview Oct 9, 2026 8:28pm UTC
petrinaut-docs Ready Ready Preview Oct 9, 2026 8:28pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Oct 9, 2026 8:28pm UTC

Request Review

@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps labels Sep 11, 2026
@kube
kube added this pull request to stack #9663 September 11, 2026 14:51
): Uint8Array => {
if (hash.length > maxSnapshotHashLength) throw new SnapshotError("too-large");
if (!hash.startsWith(prefix)) {
throw new SnapshotError(/^v\d+\./u.test(hash) ? "unsupported" : "invalid");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:
Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by regex_dos.

You can view more details about this finding in the Semgrep AppSec Platform.

): Uint8Array => {
if (hash.length > maxSnapshotHashLength) throw new SnapshotError("too-large");
if (!hash.startsWith(prefix)) {
throw new SnapshotError(/^v\d+\./u.test(hash) ? "unsupported" : "invalid");
@cursor

cursor Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
New read-only client route and sharing UI with bounded decode paths; snapshot payloads stay in the URL fragment and are redacted from Sentry, with no backend persistence changes.

Overview
Adds client-side snapshot sharing so local nets can be distributed as self-contained /share#v1.br.<payload> URLs (canonical JSON → Brotli → base64url) with no server upload.

A Share control on editable local documents opens a popover to copy the link (optional current view in query params), download YAML when the net exceeds link size limits, and freeze the net at open time. A new /share route decodes the fragment in a Web Worker (30s timeout, 2 MiB / 16k-char caps), shows a read-only editor via shared ReadonlyDocumentPage (also used by refactored example pages), and Make a local copy persists to /local/<id>. Sentry hooks strip snapshot fragments from breadcrumbs, events, transactions, and spans. Docs and unit/route tests cover codec edge cases, worker lifecycle, UI flows, and telemetry redaction.

Reviewed by Cursor Bugbot for commit 2f14564. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Clipboard completion can report a stale URL as copied, and the critical oversized-snapshot download fallback is not exercised.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds self-contained, read-only Petrinaut snapshot sharing with optional editor-view restoration, local copying, and file fallback.

Changes:

  • Adds versioned Brotli snapshot encoding through cancellable workers.
  • Adds sharing UI, /share routing, local-copy behavior, and Sentry redaction.
  • Adds comprehensive documentation, tests, dependency updates, and changesets.
File summaries
File Description
yarn.lock Locks the Brotli dependency.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/petrinaut-docs-content.ts Registers sharing documentation.
libs/@hashintel/petrinaut/docs/sharing.md Documents snapshot sharing.
libs/@hashintel/petrinaut/docs/README.md Links the sharing guide.
libs/@hashintel/petrinaut-core/src/ai.ts Exposes sharing documentation to AI.
apps/petrinaut-website/src/sharing/snapshot.ts Serializes and validates snapshots.
apps/petrinaut-website/src/sharing/snapshot.test.ts Tests snapshot codec behavior.
apps/petrinaut-website/src/sharing/snapshot-worker.ts Runs Brotli operations in a worker.
apps/petrinaut-website/src/sharing/snapshot-worker-protocol.ts Defines worker messages.
apps/petrinaut-website/src/sharing/snapshot-codec.ts Implements bounded Brotli encoding.
apps/petrinaut-website/src/sharing/snapshot-client.ts Manages worker lifecycle and URLs.
apps/petrinaut-website/src/sharing/snapshot-client.test.ts Tests cancellation and failures.
apps/petrinaut-website/src/sharing/share-snapshot-button.tsx Adds the sharing dialog.
apps/petrinaut-website/src/sharing/share-snapshot-button.test.tsx Tests sharing interactions.
apps/petrinaut-website/src/sentry/strip-snapshot-links.ts Redacts snapshot payloads.
apps/petrinaut-website/src/sentry/strip-snapshot-links.test.ts Tests telemetry redaction.
apps/petrinaut-website/src/sentry/instrument.ts Applies redaction to Sentry hooks.
apps/petrinaut-website/src/routes/share.tsx Adds the snapshot route.
apps/petrinaut-website/src/routes/-share.test.tsx Tests snapshot navigation and copying.
apps/petrinaut-website/src/main/app/readonly-document-page.tsx Extracts shared read-only document UI.
apps/petrinaut-website/src/main/app/local-storage-demo/local-storage-demo-app.tsx Adds sharing to local documents.
apps/petrinaut-website/src/examples/full-example-page.tsx Reuses the read-only document page.
apps/petrinaut-website/README.md Documents snapshot architecture.
apps/petrinaut-website/package.json Adds brotli-wasm.
.changeset/snapshot-sharing-guide.md Records the Petrinaut documentation change.
.changeset/snapshot-sharing-doc-catalog.md Records the core catalog change.
Review details
  • Files reviewed: 25/26 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +73 to +80
const copy = async () => {
if (url === null) return;
try {
await navigator.clipboard.writeText(url);
setCopyState("copied");
} catch {
setCopyState("failed");
}
Comment on lines +90 to +93
expect(screen.getByRole("button", { name: "Download file" })).toHaveProperty(
"disabled",
false,
);
SnapshotResponse,
} from "./snapshot-worker-protocol";

self.onmessage = async (event: MessageEvent<SnapshotRequest>) => {

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c188a7e. Configure here.

let position = 0;
for (const chunk of chunks) {
output.set(chunk, position);
position += chunk.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decompress reuses freed chunk buffers

Medium Severity

decompressSnapshot stores each result.buf and then calls result.free() before the next stream.decompress. Those buffers are only copied into output after the loop, so a later WASM allocation can overwrite earlier chunks. Nets that expand past the 32 KiB output step can open as a corrupted document instead of the shared snapshot.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c188a7e. Configure here.

kube added 3 commits October 9, 2026 22:04
Share in a local document's top bar builds a /share#v1.br.<payload> link that
carries the whole net, Brotli-compressed in a worker, so nothing is uploaded.
/share opens the snapshot read-only and saves nothing until Make a local copy.
Sentry strips the fragment before sending.

Examples and snapshots now share one read-only page and offer no Share button,
since their own URL is already the link. The Share dialog is one link row and
one options row, with a message only when something fails.
Snapshot links are a demo-website feature, so their guide does not belong in
the @hashintel/petrinaut user guide, the assistant's documentation catalog in
@hashintel/petrinaut-core, or either package's patch notes. The website
README carries what a snapshot holds.
Share is a ghost icon button in the top bar's trailing section, beside version
history, and opens a dropdown in place of the dialog: a short hint, the link
with an inline Copy, then Include current view and Download on one row. The
popover animates in and out, and the button stays pressed while it is open.
@kube
kube force-pushed the codex/fe-1669-snapshot-sharing branch from 8f2f386 to 2f14564 Compare October 9, 2026 20:17
@github-actions github-actions Bot removed area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Oct 9, 2026

This branch was successfully deployed

3 active deployments
Preview – hash — 2f145644 Deployed Oct 9, 2026 by vercel[bot]
Preview – petrinaut-docs — 2f145644 Deployed Oct 9, 2026 by vercel[bot]
Preview – petrinaut — 2f145644 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area)

Development

Successfully merging this pull request may close these issues.

3 participants