Inspiration
An agent can produce a convincing six page proposal in about forty seconds. Nobody on the receiving end got forty seconds faster at reading one.
The pattern I kept watching on real projects: someone sends an AI written spec, and the reply is another AI written counter spec. Two confident documents that never actually answer each other. V1, V2, V3, and no convergence. I wrote a line in an earlier blog post that still sums it up for me: everyone generates, few truly read.
The expensive part is not the writing. Writing got cheap. The expensive part is the decision that never gets made, because the reactions land in four chat threads and a DM and nobody ever reconciles them against the document that caused them.
htmldrop started as a way to make an HTML file easy to open. This is the second half: making it easy to argue with, at the exact sentence or picture being argued about, and then folding that argument back into the document itself.
What it does
htmldrop is a CLI. There is no htmldrop account, no login, no hosted dashboard. It wraps one HTML file in three surfaces.
Edit mode is local and runs before anyone else sees the draft. htmldrop edit start brief.html serves the file on 127.0.0.1 and opens it. You comment on a line in the browser; your agent is blocked on htmldrop edit poll, receives the comment along with the text it was anchored to, edits the file, and replies. The page live reloads over SSE with the comments re anchored, so you stay where you were instead of scrolling back. The agent can also push a question at you with htmldrop edit ask, and your answer comes back on its next poll.
The published link is one command. htmldrop push brief.html --feedback --password encrypts the document client side with AES-256-GCM, key derived by PBKDF2-SHA256 at 600k iterations, before anything is uploaded. The password is held in memory long enough to encrypt and then dropped. It is not in ~/.htmldrop/config.json, not on Surge, not on the Worker. A reviewer needs the link and the password and nothing else.
The annotation widget rides on the published page and supports two kinds of anchor. A text_range stores the selected text plus a CSS selector. An element_rect is a box you drag over a region, and it captures the text sitting underneath that box along with the selector. The second kind is why this works on diagrams, where there is no clean run of text to select.
Then htmldrop converge brief.html pulls every comment with its anchor, builds one prompt, and makes a single model call. With an OpenAI key it goes to gpt-5.6-luna over the Responses API with store: false, raw fetch, no SDK. The output is a whole rewritten HTML document at brief.converged.html. Re pushing reuses the same doc id, so the link stays stable and the existing comments stay attached to the new version.
htmldrop does not author the design or the diagrams. Your agent does. htmldrop prints a design contract (htmldrop design, with Mermaid pinned at 11.16.0 plus SRI) and per shape playbooks (htmldrop playbook diagram), then hosts, anchors, and synthesizes.
How we built it
Plain Node, ESM, no framework. Surge hosts the static HTML. A small Cloudflare Worker with KV holds the comments, because a static page cannot accept writes, and one env var points the CLI at your own Worker instead of mine.
Every provider call is raw fetch against the vendor's HTTP API. For OpenAI that is POST /v1/responses with instructions, input, max_output_tokens, and store: false, plus a reader that takes output_text when it is there and otherwise walks the output array for output_text parts. Skipping the SDK keeps the install small and makes swapping a provider a single function.
Codex with GPT-5.6 built four pieces of this. Edit mode, which is the largest: the loopback server, the SSE channel, the poll / reply / ask protocol, and the session store under ~/.htmldrop/edit/ keyed by file path. The migration of the OpenAI converge path off Chat Completions onto the Responses API. The crypto v2 work. And the demo harness, which is three scripts: stage.mjs publishes and seeds the review thread, record.mjs drives one continuous browser take and emits beats.json, append-closing.mjs renders and appends the closing card.
UAT ran through Codex too, on a real published document with a live review thread, converging it and then reading the generated HTML to check what had actually changed rather than trusting the summary.
htmldrop existed before Build Week. It was first published to npm in May as a way to turn an HTML file into a link. What shipped during the submission window is the half this submission is about: three releases (v1.10.0, v1.11.0, v1.12.0) totalling about 5,000 lines across 49 files. That covers the agent guardrails (playbooks, the design contract, the layout audit, the edit session registry), pull plus shared team identity so a document can be handed off without git, bulk delete and gallery metrics, the generated-password flow and the zero-knowledge writeup, and the migration of the OpenAI converge path onto the Responses API with gpt-5.6-luna. The commits are dated in the repo if you want to check.
Challenges we ran into
Region anchoring is the thing I underestimated. A dragged box has no text node behind it, so there is nothing to store. What shipped captures the text under the rect plus a selector, and the converge prompt still carries a branch for when neither is usable, which tells the model to infer the target from the comment wording. That branch is a guess and I know it is a guess.
The crypto v2 migration was the fiddliest work in the repo. The CLI encrypts with node:crypto and the browser decrypts with WebCrypto, so the envelope has to be byte compatible or the document is simply unreadable. Then the feedback access token had to be derived from the same password without breaking documents published before that token existed. One PBKDF2 pass produces 64 bytes, and because PBKDF2 emits output block by block, bytes 0 through 31 are identical to a plain 32 byte derivation, so older docs still decrypt with the same key. The crypto-js legacy path stays in the file for pre v2 documents. It is not pretty, and deleting either path breaks somebody's live link.
Converge rewriting the whole document is the honest weak point. The model returns a complete HTML file, so a change I did not ask for is not flagged anywhere, it just sits in the output. Stripping the fenced code block that models add despite the instruction was the easy half. The harder half is that I review the converged file by reading it rather than by reading a diff of intent, and I have not solved that.
The system prompt tells the model to pick the stronger argument when reviewers disagree. In the Build Week run it left the one genuinely undecided trade off alone, which is the behavior I want. I cannot claim the prompt guarantees it.
The first demo cut drifted badly because I derived the narration timings from planned waits instead of measuring the encoded MP4. Rewriting the recorder to emit beats.json from the actual file fixed it.
Accomplishments that we're proud of
A reviewer dragged a box over the architecture diagram and asked for the converge node to say "proposes" instead. GPT-5.6 went into that diagram's Mermaid source. The node that read Converge / GPT-5.6 Luna reads Converge / proposes edits in the converged file. Region anchored feedback went through synthesis and came out the other side as a visibly changed picture, which is the thing I was not sure would work.
In the same run it turned a vague success metric into a measurable baseline of eighteen clarification loops a week cut by half, and inverted the rollout order because a reviewer argued for it. The trade off nobody had evidence for, it left alone. GPT-5.6 proposes and the human decides.
The password guarantee also holds up under the only test that matters. A breach of Surge, of the Worker, or of my own config file exposes nothing, because there is no password in any of them.
What we learned
The anchor does more work than the model. Same call, same document: feedback that arrives as "the diagram is misleading" produces nothing useful, and feedback that arrives carrying the text under the box and a selector produces an edit to the correct Mermaid node. Most of the engineering that makes converge look clever lives in the widget, not the prompt.
The other thing is about restraint. Applying clear feedback is the easy behavior to get. The interesting product question is which comments a model should refuse to act on, and how it hands one back without burying it in a paragraph of hedging.
What's next for htmldrop
Structured converge output, so each change points at the comment that caused it and I can review a change list instead of a whole document.
Identity based access. Today it is one password with all or nothing rights. What teams ask for is per person access and revocation when somebody leaves. The path is sketched in docs/plans/2026-05-25-password-capability-design.md, and it costs the zero knowledge property, so it will be a separate tier rather than an upgrade to the current one.
Comment confidentiality. The password protects the document on Surge, but comments sit in plaintext KV and their privacy currently rests on the link being unguessable. Self hosting the Worker is the answer today and it is not a good enough one.
And a smaller fix I want: element_rect should hold a durable handle on its region rather than re deriving it, so an anchor survives a layout change instead of falling back to a best effort re anchor.
Built With
- aes-256-gcm
- anthropic-api
- cli
- cloudflare-kv
- cloudflare-workers
- codex
- commander
- crypto-js
- css
- ffmpeg
- gpt-5.6
- html
- javascript
- mermaid
- node.js
- npm
- openai
- openai-responses-api
- pbkdf2
- playwright
- surge.sh
- typescript
- web-crypto-api
- wrangler
- zod
Log in or sign up for Devpost to join the conversation.