Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
path: |
.scratch/m1-uat/run-*/*.png
.scratch/m1-uat/run-*/*-failure.json
.scratch/m2-uat/run-*/*.png
.scratch/m2-uat/run-*/*-failure.json
if-no-files-found: ignore
retention-days: 7
- run: npm pack --dry-run
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ docs/demo/.stage/

# demo video renders, published to YouTube instead
artifacts/

# Reproducible packaged Excalidraw browser build (generated by pretest/prepack)
src/edit/editor-dist/
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
[Semantic Versioning](https://semver.org/).

## [1.17.0] — 2026-10-16

### Added

- A locally bundled Excalidraw editor in edit mode, with native scene and SVG export, JSON import and one-way Mermaid import.
- Shape-attached comment threads in the editor and on published feedback pages, retaining stable IDs and revision context when shapes move or are removed.
- Owner-reviewed scene proposals, accepted diagram revisions, and stale-source/revision conflict detection shared by browser and agents.
- A local stdio MCP server bound to one registered artifact, with reviewer tools and explicit owner create/save/accept/reject tools.
- Accepted scenes and static SVG previews embedded in self-contained HTML, surviving publish/pull and automatic encrypted publication history. Private local diagram history remains outside published staging.

### Changed

- Diagram editor assets load on demand from the installed package; editor CSP restricts external resources and web embeds are disabled.
- Source reload waits while the diagram editor is open, so accepted source changes do not silently navigate away from an active canvas draft.

## [1.16.0] — 2026-10-16

### Added
Expand Down
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ _Click the diagram for the interactive release note._
- [Example walkthroughs](#example-walkthroughs)
- [The agent loop](#the-agent-loop)
- [Edit mode (local, pre-publish)](#edit-mode-local-pre-publish)
- [Excalidraw diagrams and MCP](#excalidraw-diagrams-and-mcp)
- [Multi-provider AI (Anthropic / OpenAI / Gemini)](#multi-provider-ai)
- [Architecture](#architecture)
- [Security model](#security-model)
Expand Down Expand Up @@ -275,8 +276,9 @@ since the password gate uses built-in WebCrypto.

## Release notes

Each release ships a short, visual release note — published with htmldrop itself, so you can read it as a live page:
Release summaries are published on [GitHub Releases](https://github.com/ooiyeefei/htmldrop/releases). The complete change history is in [CHANGELOG.md](CHANGELOG.md). Earlier visual release notes remain available below:

- **v1.16.0** — automatic history and redlines on every publish, safe declared sibling assets, expiring private HTTPS review and a layout inbox. → [Release notes](https://github.com/ooiyeefei/htmldrop/releases/tag/v1.16.0)
- **v1.10.0** — playbooks, a one-command design starter kit, smarter edit-mode guidance, and `htmldrop pull` + shared team identity (hand a doc off to a teammate, no git). → **[Read the release note](https://yooi.surge.sh/htmldrop-v1.10.0.html)**

---
Expand Down Expand Up @@ -531,7 +533,7 @@ Unchanged source is deduplicated. Content or protection changes create a revisio

For an existing feedback link, password protection is registered before deploying its new page, including when feedback is turned off. If deployment then fails, that remote review link stays protected and its previous password capability may stop working. Retry the same push to synchronize it; the CLI reports this partial transition explicitly. It never automatically reopens private access to recover a failed deploy.

For isolated profiles or test servers, set `HTMLDROP_CONFIG_DIR` to an explicit state directory; the default remains `~/.htmldrop`. Excalidraw editing, shape comments and MCP are planned together for milestone 2 and are not included in this release.
For isolated profiles or test servers, set `HTMLDROP_CONFIG_DIR` to an explicit state directory; the default remains `~/.htmldrop`. Excalidraw editing, shape comments and local MCP are included; see [Excalidraw diagrams and MCP](#excalidraw-diagrams-and-mcp).

---

Expand Down Expand Up @@ -574,6 +576,56 @@ It’s symmetric: when your teammate publishes *their* doc, they’re the owner

---

## Excalidraw diagrams and MCP

Edit mode includes a locally bundled Excalidraw canvas. Open **Diagrams** to create a diagram, import native `.excalidraw` JSON, or import supported Mermaid as new editable shapes. Select a diagram, draw or move shapes, then **Save diagram**. Saves update the local HTML; they do not publish it.

The HTML embeds the accepted native scene, stable document/diagram/shape identifiers and a static SVG image. A published page remains readable without React, Excalidraw or an editor service. With `--feedback`, readers can select a shape beside the preview and comment; the anchor keeps the shape ID across moves and text changes. Removed shapes retain their threads with a visible removed label. A comment records the revision it referred to.

### Proposals and diagram revisions

Private edit reviewers can change a canvas and **Propose changes**. The accepted page stays unchanged until the local owner accepts the proposal. Owners can accept or reject in the editor; acceptance renders a fresh preview from the proposed scene. Conflicting source or diagram changes return an explicit conflict rather than replacing someone else's work. Export a conflicting draft before reopening the latest diagram.

The owner editor lists local accepted diagram revisions and can export each as native JSON or its corresponding SVG preview. Reviewers see only the current accepted revision and proposals based on the current source, so a newly shared review link cannot reveal removed content from older local drafts. This local diagram history is separate from the automatic history of published HTML versions. Current accepted scene/IDs travel with the HTML and survive `pull`; private local proposals and unpublished diagram revisions remain in the original profile. Earlier published diagram source remains available through published document history.

Incoming comments refresh without discarding unsent text. Source-file reloads are deferred while the diagram editor is open; close or save the diagram before reloading the surrounding page. The canvas loads only when opened. Its editor code and fonts are packaged locally, with an editor CSP restricting network access; there is no required CDN, Excalidraw+ account or hosted collaboration service.

### Connect a local MCP agent

First register the HTML artifact in edit mode:

```bash
htmldrop edit start /absolute/path/report.html --no-open
htmldrop mcp --artifact /absolute/path/report.html --role owner
```

The MCP command speaks stdio JSON-RPC and stays running for its client. Configure your client's stdio MCP server with:

```json
{
"mcpServers": {
"htmldrop": {
"command": "htmldrop",
"args": ["mcp", "--artifact", "/absolute/path/report.html", "--role", "owner"]
}
}
}
```

Each connection binds one already-registered artifact. Tools cannot choose arbitrary files, URLs or another session. The default role is `reviewer`: read the current scene/comments, inspect current proposals, propose changes, comment on a shape and export the current revision. Explicit `--role owner` additionally exposes create, save, accept and reject. Neither role publishes the HTML or exposes edit-server controls.

Read `diagram_list`, then `diagram_read` for the current IDs and source hash. Mutations require the returned `baseRevisionId` and `baseSourceHash` (creation needs only the source hash). Resources expose the bound artifact's diagram list and revisions. Native JSON export needs no browser; an accepted save requires a safe SVG preview rendered from that scene, and SVG export returns the stored preview for that exact revision. The owner acceptance UI can render a proposal that has no preview. MCP does not invent a preview or silently reuse an older one.

### First-release boundaries

- Publication still requires self-contained HTML; diagram images must be embedded PNG, JPEG, WebP or GIF data. Web embeds and shape links are disabled.
- The full accepted source must stay within the existing 2 MB HTML limit; reduce embedded images or diagram size if a save is refused.
- Mermaid import is one-way. There is no claim of arbitrary lossless canvas-to-Mermaid conversion.
- MCP is local stdio; private browser review uses `edit share`. Public remote MCP and simultaneous multiplayer are separate work.
- `push --password` encrypts accepted diagram source and published history under the existing password model. Local diagram revision/proposal archives use the local profile's encryption key; keep the profile private. Publishing does not upload those local archives.

---

## Multi-provider AI

> ### The AI key is optional — and bring-your-own
Expand Down
15 changes: 15 additions & 0 deletions bin/htmldrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ program
.description('Publish HTML files and get shareable links via Surge.sh')
.version(pkg.version);

program
.command('mcp')
.description('Serve diagram tools over local stdio for one registered HTML artifact')
.requiredOption('--artifact <file>', 'HTML file already registered with htmldrop edit start')
.option('--role <role>', 'reviewer or owner', 'reviewer')
.action(async (options) => {
try {
const { startMcp } = await import('../src/mcp/index.js');
await startMcp(options);
} catch (err) {
console.error(`Error: ${err.message}`);
process.exitCode = 1;
}
});

program
.command('init')
.description('Set up htmldrop (login to Surge and pick a subdomain)')
Expand Down
49 changes: 49 additions & 0 deletions docs/plans/2026-10-16-excalidraw-milestone-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Excalidraw milestone 2

Deliver editable Excalidraw diagrams, comments attached to stable shapes, reviewable diagram revisions/proposals, and a local stdio MCP server together. Automatic publication history remains independent of optional feedback. No blog post, simultaneous multiplayer, publicly hosted remote MCP, or arbitrary lossless canvas-to-Mermaid conversion.

## Shared contract (implementation boundary)

Accepted diagrams are authoritative inside the HTML source. Use one `<meta name="htmldrop-document-id" content="UUID">` and managed blocks:

```html
<!-- htmldrop:diagram:DIAGRAM_ID:start -->
<figure data-htmldrop-diagram="DIAGRAM_ID">
<img alt="Diagram title" src="data:image/svg+xml;base64,...">
<script type="application/json" data-htmldrop-scene="DIAGRAM_ID">ESCAPED_JSON</script>
</figure>
<!-- htmldrop:diagram:DIAGRAM_ID:end -->
```

Embedded JSON: `{version:1, documentId, diagramId, revisionId, parentRevisionId, title, scene}`. `scene` is native `{type:'excalidraw',version:2,elements,appState,files}`. Escape `<` in JSON. UUID identifiers are server-assigned except stable Excalidraw element IDs. Preserve comments for deleted shapes, visibly marking them removed. Managed scenes and SVG previews survive single-file publication and pull. Full diagram edit history and pending proposals stay in private local profile state; published document history retains earlier published diagram source.

Shared service (browser and MCP call the same HTTP adapter):

- `GET /api/edit/:key/diagrams` -> `{documentId,sourceHash,diagrams:[{diagramId,revisionId,title}],permissions:{create,save,accept,propose}}`.
- `POST /api/edit/:key/diagrams` owner create -> body `{baseSourceHash,title,scene,svg,summary}`; returns read payload.
- `GET /api/edit/:key/diagrams/:diagramId` -> `{documentId,diagramId,revisionId,parentRevisionId,sourceHash,title,scene,svg,proposals,revisions,permissions}`.
- `POST .../:diagramId/save` -> `{baseRevisionId,baseSourceHash,title,scene,svg,summary}`.
- `POST .../:diagramId/proposals` -> same fields but SVG optional. Immutable proposal; no source write.
- `POST .../:diagramId/proposals/:proposalId/accept` -> `{baseRevisionId,baseSourceHash,svg,summary}`; acceptance uses stored candidate scene and its original base, generates new revision. UI regenerates SVG using Excalidraw before accept.
- `POST .../:diagramId/proposals/:proposalId/reject` -> `{baseRevisionId,baseSourceHash}`.
- `GET .../:diagramId/export?format=excalidraw|svg&revisionId=...` -> JSON `{format,content,mimeType,revisionId}`. SVG is the validated stored preview for precisely that revision. Never return stale preview.
- Existing `/api/feedback/:key` threads accept `anchor:{type:'diagram_shape',documentId,diagramId,shapeId,revisionId}`. MCP comment mutation supplies baseRevision/sourceHash and HTTP adapter checks them before inserting via existing store.
- `POST .../:diagramId/comments` -> `{baseRevisionId,baseSourceHash,shapeId,text,authorName}`; validates shape membership and returns created comment. `GET .../:diagramId/comments` returns `{items}` filtered to diagram.

Actor comes from authenticated transport, never body fields. Same-origin local author browser can save; remote gateway reviewer cannot create/save/accept/reject at both gateway and backend. Reviewer can read/propose/export/comment for the current revision. Reviewer projections must omit older local scenes and outdated proposals; noncurrent revision export is owner-only, preventing removed confidential content from leaking through a new share. CLI/stdio agent controls keep their separate local discovery credential. Every mutation checks current diagram revision plus source SHA256 inside a per-file serialized mutation. Stale writes and proposals return409 with actionable conflict state; no silent overwrite. Preserve source around managed block and use temp+rename. Recheck source bytes immediately before rename to detect external edits. API errors carry `statusCode` and safe `message`.

## Module ownership

- `src/diagrams/document.js`, `src/diagrams/service.js` (core lane): parse/validate/serialize accepted document blocks, safe SVG preview, bounded native scenes, atomic serialized source writes, proposal and revision state. Export `createDiagramService({getSession,configDir})`. Returned methods: `list(key,actor)`, `read(key,id,actor)`, `create(key,body,actor)`, `save(key,id,body,actor)`, `propose(key,id,body,actor)`, `accept(key,id,proposalId,body,actor)`, `reject(key,id,proposalId,body,actor)`, `exportDiagram(key,id,{format,revisionId},actor)`, `checkAnchor(key,id,body,actor)` (validates base+shape, returns canonical anchor). `actor` is `'owner'|'reviewer'`. No source writes for read/list/export/comment checks. Parent can add optional nonbreaking exports as needed.
- `src/edit/diagrams.js`, edit/server.js + remote.js, templates/annotation-widget.html + worker mirror and worker/src/schema.ts (HTTP/annotation lane): route shared service, comments validation and highlighting, SSE, gateway permissions.
- `src/edit/browser/`, `src/edit/editor-assets.js`, `scripts/build-editor.mjs` (editor lane): pinned locally built Excalidraw, lazy iframe editor, diagram toolbar in injected edit runtime, selection/comments, revision/proposal controls, JSON/SVG import/export and optional one-way Mermaid import. No CDN or runtime external requests. Dist files `src/edit/editor-dist/` are reproducible build outputs (not committed), included in npm tarball via prepack.
- `src/mcp/`, Commander command in bin/htmldrop.js and test/mcp.test.mjs (MCP lane): SDK1.32.1 + Zod4.6.7 preserves Node>=18. `htmldrop mcp --artifact /abs/file.html --role reviewer|owner` binds an explicitly registered HTML session, default reviewer. No per-tool path/URL/role arguments. Tools/resources expose list/read/create(owner)/save(owner)/propose/accept(owner)/reject(owner)/history/comments/export. Stdout is JSON-RPC only.
- Parent owns package.json/lock, plan/docs/release integration, final review and verification. No concurrent edits to another lane's owned files; coordinate small seams through parent.

## Rendering and limits

Native browser `exportToSvg` is authoritative for graphical rendering. Require fresh SVG on accepted writes; proposals can omit it. MCP exports native scene JSON without a renderer; scene save requires caller-provided safe SVG, with a clear error if absent. The owner acceptance UI regenerates reviewer previews. Embed as SVG data-URL image, never active inline SVG. Reject scripts, event handlers, foreignObject, external URLs, unsafe CSS, invalid XML, ambiguous markers/IDs and unsupported scenes. Raster images are bounded data URLs only; no SVG attachments or web embeds. Enforce finite geometry, unique shape IDs, supported types, per-field/file/scene/HTML byte and element limits. Disabled editor embeds/AI/library links must not cause outbound requests. CSP constrains all editor assets/fonts/network to self/data/blob as needed.

## Acceptance evidence

Execute real browser Excalidraw edits and shape selection/comment/replies, proposal/accept/reject, revision preview/export, stale conflict and remote owner-route denial. Actual SDK Client/StdioClientTransport must discover/call tools and read resources without protocol stdout pollution. Verify scene+SVG single-file publish/pull, encrypted private history, deleted-shape anchors, SVG/JSON injection, source conflicts, asset isolation and lazy/offline browser loading. Run existing Node20/22 regression suites, Worker typecheck/dry build, npm pack/install checks, independent high-reasoning review, PR CI and runtime proof before release. Distinguish built/tested/merged/deployed/registry-available proof. Preserve dirty original main checkout and existing shared daemons.
Loading
Loading