Inline text editing in the document viewer - #16
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
sjmiller609
marked this pull request as ready for review
July 27, 2026 17:16
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 —
contentEditableplusdocumentElement.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/rawserving 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.tsturns 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 (
&parses to&), so each save carries two payloads: literal, then entity-escaped on anot_found.newTextis 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
/editsnow 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 ajh_live_key. What each identity may do is unchanged and still enforced bycanEdit: 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:
<strong>and<a>with the tags untouched;R&D→researchvia the entity fallback; ,<!doctype>,<style>,<script>and<meta>byte-identical throughout;<b>x</b>stored as<b>x</b>;<li>Yes</li>) refused, reverted in the view, and surfaced as "text repeats — edit via your agent";patchversion 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_tokenin the response.Full suite (149 tests),
spec:check,tsc --noEmitandnext buildall pass.npm run lintis not configured in this repo and was not run.Notes for review
/editsgains the session option; the sharedkeyOrSessionSecurityconstant was hoisted so comments/reactions and edits share one definition. Generated spec artifacts andSKILL.mdare regenerated by their sync guards.base_versionrather than merged.🤖 Generated with Claude Code
Note
Medium Risk
Touches document write auth and the core
/editspatch 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.tsmaps those to the same{oldText, newText}patches agents use onPOST /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 /editsnow accepts a signed-in session (cookie) as well as an API key, mirroring comment routes, with per-session write rate limits;canEditis unchanged (owner/editor only). The shell wirescanEdit,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.