Skip to content

Inline text editing in the document viewer - #16

Merged
sjmiller609 merged 2 commits into
mainfrom
hypeship/inline-doc-editing
Jul 27, 2026
Merged

Inline text editing in the document viewer#16
sjmiller609 merged 2 commits into
mainfrom
hypeship/inline-doc-editing

Conversation

@sjmiller609

@sjmiller609 sjmiller609 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Owners and editor grantees can click a pencil in the viewer bar and type directly on the page. Clicking a block makes that block contentEditable; blur or ⌘-Enter saves.

The design decision

Saving never re-serializes the DOM. The obvious implementation — contentEditable plus documentElement.outerHTML — would rewrite the entire document on every save (attribute order, entity forms, void-element spelling, Mermaid's rendered SVG replacing its own source), and /d/:slug/raw serving the author's bytes verbatim is the product.

Instead the overlay diffs the edited block's text nodes and reports {before, after} pairs. lib/docs/inline-edit.ts turns those into the same {oldText,newText} patch an agent posts to /edits, so versioning, comment re-anchoring, quotas and the 409/422 outcomes are all existing paths, and every byte outside the edited run is untouched.

A text node's content is verbatim in the source except where the author used entities (&amp; parses to &), so each save carries two payloads: literal, then entity-escaped on a not_found. newText is always escaped, so typing <b> lands as text rather than markup.

Structural edits can't be expressed as text patches, so they're prevented rather than guessed at: Enter is suppressed, paste is flattened to text, and a node split/merge/removal restores the block and reports "structure changed". Those remain an agent job.

Permissions

/edits now accepts a signed-in session in addition to an API key — the same dual identity the comment routes already take — because the browser posts with a cookie, not a jh_live_ key. What each identity may do is unchanged and still enforced by canEdit: owner or editor grant only. A view-token holder or public-doc reader resolves to no access and gets the same 404 as before, so read links stay read-only. The session gets its own rate-limit bucket at the existing write ceiling.

Verification

Unit tests (lib/docs/inline-edit.test.ts, 8 cases) run the overlay's output through the real edit engine and assert the untouched bytes stay untouched.

Beyond that, this was driven end-to-end against a local Postgres + dev server with real Chromium over CDP. Starting from a blog-shaped document, typing in the page produced exactly:

  • a heading typo fixed;
  • a run edited on both sides of <strong> and <a> with the tags untouched;
  • R&amp;Dresearch via the entity fallback;
  • &nbsp;, <!doctype>, <style>, <script> and <meta> byte-identical throughout;
  • typed <b>x</b> stored as &lt;b&gt;x&lt;/b&gt;;
  • an ambiguous edit (two identical <li>Yes</li>) refused, reverted in the view, and surfaced as "text repeats — edit via your agent";
  • one patch version snapshot per save, so history and rollback are unaffected.

The permission boundary was exercised the same way: anonymous → 401; anonymous holding a valid view token → 401; signed-in non-grantee holding a valid view token → 404; commenter grant → no pencil rendered and 404 on POST; editor grant → pencil rendered, edit accepted, and no view_token in the response.

Full suite (149 tests), spec:check, tsc --noEmit and next build all pass. npm run lint is not configured in this repo and was not run.

Notes for review

  • The OpenAPI security list for /edits gains the session option; the shared keyOrSessionSecurity constant was hoisted so comments/reactions and edits share one definition. Generated spec artifacts and SKILL.md are regenerated by their sync guards.
  • Known limits, by design: text only (no adding/reordering/deleting blocks, no bold/link insertion), one version snapshot per save so saves are coarse (blur / ⌘-Enter, not per keystroke), and conflicts are detected via base_version rather than merged.

🤖 Generated with Claude Code


Note

Medium Risk
Touches document write auth and the core /edits patch path; permissions stay narrow but session-based writes add a new credential surface alongside existing versioning and conflict handling.

Overview
Adds inline text editing in the document viewer for owners and editor grantees: a pencil in the bar toggles edit mode; clicking a block makes it contentEditable, and blur or ⌘-Enter saves.

Saves never re-serialize the DOM. The sandbox overlay diffs edited text nodes and sends {before, after} pairs; lib/docs/inline-edit.ts maps those to the same {oldText, newText} patches agents use on POST /api/v1/docs/:slug/edits, preserving byte-exact HTML outside the edited runs. Entity mismatches get a literal-then-escaped retry; structural changes (Enter, paste structure, node splits) are blocked or reverted with a user-facing message.

POST /edits now accepts a signed-in session (cookie) as well as an API key, mirroring comment routes, with per-session write rate limits; canEdit is unchanged (owner/editor only). The shell wires canEdit, base_version, optimistic revert on failure, and comment reload after save. OpenAPI/docs/skills note session auth on /edits.

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

Owners and editor grantees can now click a pencil in the viewer bar and
type directly on the page. Clicking a block makes that block
contentEditable; blur or Cmd-Enter saves.

Saving never re-serializes the DOM. A round-trip through innerHTML would
rewrite the whole document on every save (attribute order, entity forms,
script-rendered subtrees) and byte-exact /raw is the point of the format.
Instead the overlay diffs the block's text nodes and reports before/after
pairs, which become the same deterministic {oldText,newText} patch an
agent posts to /edits — so versioning, comment re-anchoring, quotas and
the 409/422 outcomes are all existing paths, and bytes outside the edited
run are untouched.

A text node's content is verbatim in the source except where the author
used entities, so each save carries two payloads: literal, then
entity-escaped on a not_found. newText is always escaped, so typing <b>
lands as text rather than markup.

Structural edits are not expressible as text patches: Enter is
suppressed, paste is flattened, and a node split/merge/removal restores
the block and reports it instead of guessing. Those stay an agent job.

/edits now accepts a signed-in session in addition to an API key (the
same dual identity the comment routes already take), since the browser
posts with a cookie. Permissions are unchanged and still enforced by
canEdit: owner or editor grant only. A view-token holder or public-doc
reader gets the same 404 as before, so read links stay read-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
justhtml Ready Ready Preview, Comment Jul 27, 2026 5:14pm

@sjmiller609
sjmiller609 marked this pull request as ready for review July 27, 2026 17:16

@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 using high effort and found 2 potential issues.

Fix All in Cursor

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

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c5ce874. Configure here.

Comment thread lib/docs/overlay.ts
Comment thread app/d/[slug]/CommentsShell.tsx
@sjmiller609
sjmiller609 merged commit 6a1b6e8 into main Jul 27, 2026
5 checks passed
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.

1 participant