Skip to content

Stop the docs editor eating a reader's typing, and three things it claimed but never did - #116

Merged
imshashank merged 7 commits into
mainfrom
docs/workspace-depth
Aug 6, 2026
Merged

Stop the docs editor eating a reader's typing, and three things it claimed but never did#116
imshashank merged 7 commits into
mainfrom
docs/workspace-depth

Conversation

@imshashank

Copy link
Copy Markdown
Contributor

Five items off the docs survey, all Tier 1, chosen so the surface stops
claiming things it does not honour. Nothing from Tier 2 is here.

1. The read-only gate, which was a bug that ate typing

DocSurface picked between the editor and the reader from
can(principal, 'doc:write'), a role capability every member holds. The
per-doc gate is a different check: assertDocWritable allows the author,
admins, non-restricted docs, and holders of a write grant. So a member
granted only read on someone's private doc got the full editing
surface, typed into it, and lost the lot to a 403 that surfaced as a
small "Save failed" chip.

getDoc now returns the effective per-doc access level, decided by
docAccessLevel, which assertDocWritable is rewritten in terms of, so
there is exactly one place that answers the question. GET /api/docs/[id]
carries it, docDetailSchema parses it (falling back to read, the safe
direction, if an older server omits it), and the client gate is
canWriteDocs && access === 'write' && archivedAt === null.

An archived doc is read-only for the same reason: everything it accepted
used to 409. A reader now sees a "Read only" or "Archived" pill instead
of wondering.

Not built: "ask to edit". A reader now has a real escape hatch below.

2. Syntax highlighting on the read path

Code was coloured while you wrote it and flat grey the instant anyone
read it: renderMarkdown was plain marked, and docProseClassName
defined no .hljs-* rules. renderMarkdown now runs lowlight over a
fenced block and emits token spans, which the sanitizer already allows
(span + class), and the token colours move out of the editor-only
stylesheet into a shared code-theme.ts that docProseClassName pulls
in. That covers the reader, the guest view, the published page, the PDF
print path and the markdown preview from one definition.

The serializer only ever emits <span class="..."> and escapes
everything else, so a block containing </code></pre><img onerror=...>
comes out as text. There is a test for exactly that.

3. Table row and column controls

Inserting a table was the only thing the editor could do with one. A
bubble menu keyed on editor.isActive('table') now carries insert row
above/below, delete row, insert column left/right, delete column, toggle
header row, and delete table, wired to the commands TableKit already
ships. Merged cells and column widths stay out, deliberately: markdown
cannot represent either and they would vanish on save.

4. Duplicate a doc

duplicateDoc copies the body, collection, project and parent of any doc
the caller can read, names it <title> (copy) inside the 200 character
limit, and hands authorship to whoever asked. Two things it refuses to
do: inherit the publish token of the original, and keep public or
link visibility (a copy of a published page lands as a workspace doc,
it does not silently republish itself).

The button sits in the doc header for anyone with doc:write, including
someone who only has read access to this doc, which is the point: it is
the reader's way out of a page they cannot edit.

Not built: recursive duplication of children. One page at a time.

5. Word count and read time where people write

Read time already existed and was tested, and rendered in exactly one
place: DocReader, which only people without write access ever see.
Both numbers now sit in DocContextRow, so the editing majority sees
them, and the reader stops printing read time twice.

Deliberately not in this PR

Docs in the command palette, doc presence avatars, ?parent= on
/docs/new, tip and danger slash commands, the /docs home, doc icons,
issue mention chips, and doc.sortOrder. Each is real work with its own
test surface and none of them shares a seam with the five above.

Tests

Every test here was watched go red against a deliberate mutation of the
code it covers.

Test Mutation that turns it red
doc-service.test.ts "agrees with updateDoc for every reader" drop the can(principal, 'doc:write') check, or return write for a doc with no write grant
doc-service.test.ts "tells a reader of a published page nothing more than read" report anything but read for principal === null
doc-service.test.ts "copies the body and placement" copy source.authorId instead of the caller
doc-service.test.ts "never hands the copy the published link" copy publishToken and visibility from the source
doc-service.test.ts "lets someone with only read access take their own copy" any of the above
doc-service.test.ts "keeps a very long title inside the stored limit" drop the truncation in copyTitle
doc-service.test.ts "takes a caller supplied title" ignore parsed.title
doc-service.test.ts "refuses a doc the caller cannot read" read the row directly instead of through loadReadableDoc
highlight.test.ts "colours a fenced block" never call lowlight
highlight.test.ts "escapes markup inside a highlighted block" stop escaping text nodes
highlight.test.ts "takes the language from the first token" keep the language- prefix
doc-code-colours.test.tsx remove codeHighlightClassName from docProseClassName
doc-surface-access.test.tsx "read grant gets the reader" gate on canWriteDocs alone, the original bug
doc-surface-access.test.tsx "never offers the editor for an archived doc" drop the archivedAt term
doc-surface-access.test.tsx "lets a reader take their own copy" gate the button on canWrite instead of canWriteDocs
duplicate.test.ts (route) stop forwarding the body, or stop publishing the actions
table-controls.test.tsx (10 cases) isActive('table') forced false, or addRowBefore/addRowAfter swapped
doc-reader.test.tsx "doc stats" count the title instead of the content
docs.test.ts "counts words across every kind of whitespace" split on ' ' instead of /\s+/

bun run verify is green: 1922 tests, 0 failures.

Code was coloured while you wrote it and flat grey the moment anyone
read it. renderMarkdown now runs lowlight over a fenced block and emits
hljs token spans, which the sanitizer already allows, and the token
colours move out of the editor only stylesheet into docProseClassName so
the reader, the published page, the PDF print and the markdown preview
all get them from one place.
Inserting a table was the only thing the editor could do with one. A
bubble menu keyed on editor.isActive('table') now carries insert row
above and below, delete row, insert column left and right, delete
column, toggle the header row and delete the table, all wired to the
commands TableKit already ships.
Duplication is how templates actually get used: people copy last week's
notes rather than start from a static template. duplicateDoc copies the
body, collection, project and parent of any doc the caller can read,
names it after the source, and hands authorship to whoever asked, so
someone with only read access can take their own editable copy. The copy
never inherits the published link of the original, and a public source
lands as a workspace doc rather than silently republishing itself.

getDoc also starts reporting the effective per doc access level, decided
in the same place assertDocWritable enforces it.
The doc surface decided between the editor and the reader from
can(principal, 'doc:write'), a role capability every member holds. A
member granted only read on a restricted doc got the full editing
surface, typed into it, and watched autosave 403 behind a Save failed
chip. The gate now follows the access level GET /api/docs/[id] reports,
which is the same answer the write path enforces, and an archived doc is
read only for the same reason: nothing it accepts can be saved.

A reader sees why the page is read only, and can still take a copy.
Read time already existed and was tested, and rendered in exactly one
place: the reader, which only members without write access ever see.
Both numbers now sit in DocContextRow, so the editing majority sees them
too, and the reader stops printing read time twice.

@greptile-apps greptile-apps 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.

imshashank has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
orbit Ready Ready Preview Aug 6, 2026 2:23pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@imshashank, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fae95c7e-f202-40e4-b62e-fb2ac202fb42

📥 Commits

Reviewing files that changed from the base of the PR and between 74ec96d and db92617.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • apps/web/src/app/(app)/docs/[id]/page.tsx
  • apps/web/src/app/api/docs/[id]/duplicate/route.ts
  • apps/web/src/features/docs/code-theme.ts
  • apps/web/src/features/docs/doc-body.tsx
  • apps/web/src/features/docs/doc-reader.tsx
  • apps/web/src/features/docs/doc-surface.tsx
  • apps/web/src/features/docs/editor/rich-text-editor.tsx
  • apps/web/src/features/docs/editor/styles.ts
  • apps/web/src/features/docs/editor/table-controls.tsx
  • apps/web/src/features/docs/outline.ts
  • apps/web/src/lib/query/schemas.ts
  • apps/web/src/lib/query/use-docs.ts
  • apps/web/tests/app/api/docs/duplicate.test.ts
  • apps/web/tests/features/docs/doc-code-colours.test.tsx
  • apps/web/tests/features/docs/doc-reader.test.tsx
  • apps/web/tests/features/docs/doc-surface-access.test.tsx
  • apps/web/tests/features/docs/doc-surface-anchors.test.tsx
  • apps/web/tests/features/docs/docs.test.ts
  • apps/web/tests/features/docs/editor/table-controls.test.tsx
  • packages/core/src/content/doc-service.ts
  • packages/core/tests/content/doc-service.test.ts
  • packages/services/package.json
  • packages/services/src/markdown/highlight.ts
  • packages/services/src/markdown/index.ts
  • packages/services/tests/markdown/highlight.test.ts
  • packages/services/tests/markdown/markdown.test.ts
  • packages/shared/src/validators/doc.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps greptile-apps 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.

imshashank has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@imshashank
imshashank merged commit 146c1bb into main Aug 6, 2026
7 checks passed
@imshashank
imshashank deleted the docs/workspace-depth branch August 7, 2026 18:52
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