Skip to content

feat: add rich-text selection copy with a separate shortcut - #2191

Merged
backnotprop merged 8 commits into
backnotprop:mainfrom
utpalsinghdev:fix/1972-rich-text-copy
Oct 26, 2026
Merged

backnotprop merged 8 commits into
backnotprop:mainfrom
utpalsinghdev:fix/1972-rich-text-copy

Conversation

@utpalsinghdev

Copy link
Copy Markdown
Contributor

Summary

Adds a dedicated rich-text copy action for document selections while preserving regular Markdown copying.

Closes #1972.

Do not merge until the owner confirms the keyboard shortcuts and Vim copy behavior below.

Changes

  • Adds Copy as rich text to the selection toolbar on desktop and touch devices.
  • Writes text/html and text/plain together, preserving headings, emphasis, links, nested lists, code, and tables.
  • Removes annotation markup, internal attributes, unsafe content, and theme styling from copied HTML.
  • Preserves ordered-list numbering, nesting, and checkbox state.
  • Keeps full clipboard text separate from the HTML viewer’s capped annotation quote.
  • Supports a copy-event fallback over plain HTTP and reports plain-text fallback accurately when rich copying is unavailable.
  • Registers the new shortcut in the shared shortcut registry and generated shortcut documentation.

Keyboard shortcuts and copy actions

Shortcut or action Behavior
Ctrl+C / Cmd+C Keeps regular copying unchanged. Captured Markdown selections copy plain text; native copying inside HTML/live iframes retains existing browser behavior.
Ctrl+Alt+C / Cmd+Option+C Copies the captured selection as rich text, with a plain-text fallback.
y in Vim mode Copies the selection or semantic target as rich text, falling back to plain text when unavailable.
Copy Keeps the existing plain-text toolbar action.
Copy as rich text Performs the same rich-copy operation as the new shortcut.
Copy Markdown / Copy CSV Remain unchanged.
Ctrl+Shift+Y / Cmd+Shift+Y Retains the existing review-feedback copy binding.

The rich-copy shortcut requires a captured selection and does not intercept typing in editable controls or run during IME composition. macOS Option+C is handled through its physical key when the keyboard layout produces a symbol.

Verification

  • Focused clipboard, toolbar, Vim, HTML bridge, and shortcut suites: 335 passed, 0 failed.
  • bun run typecheck passed.
  • bun test scripts/dom-test-allowlist.test.ts passed.
  • git diff --check passed.
  • Live paste-service tests: 3 passed, 0 failed.
  • Real Chromium copy/paste confirmed rich HTML plus plain text for Ctrl+Alt+C, plain text only for Ctrl+C, and working plain-HTTP fallback.

Full CI/e2e and Outlook/Slack/macOS paste checks remain unverified.

utpalsinghdev and others added 8 commits October 26, 2026 10:24
Co-authored-by: utpalsinghdev <utpalsinghdev@users.noreply.github.com>
The owner's call on backnotprop#2191: Copy as rich text stays as a selection-toolbar
button (desktop and touch); everything else behaves as on main.

- Remove the Mod+Alt+C shortcut (registry entry, toolbar handler, the
  bridge's rich-copy forwarding and message) and the matcher change in
  shortcuts/core.ts it needed. Cmd+Option+C is Chrome's Inspect Element.
- Vim `y` copies plain text again (useVimSelection, the bridge's vim-copy
  message, HtmlViewer and the shortcut text reverted).
- Ctrl+C / Cmd+C: the Viewer's copy handler is main's again; the extra
  keydown path is gone. The button's plain-HTTP fallback takes its own
  execCommand copy event (window capture, only during that call) so no
  page copy handler sees it.
- No BRIDGE_PROTOCOL_VERSION bump: the selection message's html and
  clipboardText fields are optional; a malformed one is dropped without
  dropping the selection.
- The button gets its own icon (the copy sheets with a T).
- Tests: the button writes text/html + text/plain; Ctrl+C and Vim y make
  the same clipboard calls as main; no surface binds Alt+C; the sanitizer
  keeps no script, handler or unsafe link. Docs in guides-and-shared-ui.md.

Co-authored-by: utpalsinghdev <utpalsinghdev@users.noreply.github.com>
…loopback

Review fixes on backnotprop#2191:

- The rich HTML is built only when Copy as rich text is pressed. The
  toolbar takes a builder (getRichCopy). In the Viewer it spans the
  pending highlight's marks; on HTML surfaces the parent asks the bridge
  (rich-copy-request) and the bridge builds the pending range's markup
  inside a try, answering rich-copy with html, text and tooLarge. The
  selection message only carries richCopy: true, so an older bridge shows
  no button. The asynchronous answer is handed to the clipboard as promise
  blobs inside the click, so the write starts in the gesture.
- The sanitized HTML is capped at 512 KiB (the bridge still carries at
  most 256 KiB). Past a cap, plain text is copied and the button says
  "too large for rich text", not "unavailable".
- Links to /api/html-assets/ lose their href on this origin and on every
  loopback spelling at any port (localhost, *.localhost, 127.0.0.0/8,
  [::1], 0.0.0.0, IPv4-mapped), judged after decoding (repeated),
  backslashes, dot segments, repeated slashes and case.

Co-authored-by: utpalsinghdev <utpalsinghdev@users.noreply.github.com>
Second review of backnotprop#2191:

- Links: every same-origin or loopback http(s) link loses its href and
  keeps its words (token routes that decode past the asset path, local
  document paths, live-app proxy links). [::7f00:1] counts as loopback.
  title attributes are dropped.
- Safari / WKWebView: the promised text/html blob never rejects. With no
  usable markup (timeout, too large, none) it is the plain text, escaped,
  in a <pre>, so one write inside the gesture always copies something,
  and the tooltip still says plain text and why.
- HTML surfaces: the bridge sends the text up to 256 KiB (cut and marked
  past it) even when the markup is too large; without an answer the quote
  is copied and, when the bridge cut it (textCut), the tooltip says
  "first N characters only". The tooltip names what was copied
  (richCopyOutcomeLabel), never "rich clipboard unavailable" for a timeout.
- configurePlannotatorUI({ richCopy }) (utils/richCopyPolicy), default
  false: a @plannotator/ui host keeps its toolbar. Plannotator's plan
  review / annotate, code review and Inbox window turn it on. Documented
  in packages/ui/HANDOFF.md and the guide.
- Tests: the 1.5 s timeout, a late answer, forged and wrong-id answers,
  one write for missing / too-large markup, sanitized HTML growing past
  512 KiB, the cut text, and the host default off
  (htmlRichCopy.test.tsx, added to the DOM list).

Co-authored-by: utpalsinghdev <utpalsinghdev@users.noreply.github.com>
… a fixed 25 ms

The 300,000-character selection test missed its message under the full
DOM run's load.

Co-authored-by: utpalsinghdev <utpalsinghdev@users.noreply.github.com>
Owner feedback on backnotprop#2191: where richCopy is on, the toolbar's existing Copy
button (same copy icon, tooltip "Copy") copies rich text, text/html plus
text/plain in one write, and shows also where hideCopyButton hid it (the
Viewer on desktop). The separate "Copy as rich text" button and its T icon
are gone. "Copied!" after a rich copy; a plain-text fallback keeps its
note. With richCopy off, Copy is exactly as before. Ctrl+C stays plain.

Co-authored-by: utpalsinghdev <utpalsinghdev@users.noreply.github.com>
Co-authored-by: utpal singh <utpalsingh915@gmail.com>
@backnotprop
backnotprop marked this pull request as ready for review October 26, 2026 18:41
@backnotprop
backnotprop merged commit 68ed68f into backnotprop:main Oct 26, 2026
37 of 38 checks passed
utpalsinghdev pushed a commit to utpalsinghdev/plannotator that referenced this pull request Oct 26, 2026
The rich-copy answer built its text from Range.toString(), the raw source
text: block line breaks were lost and source indentation came through.
Plain text now comes from the selection's own text, as in 0.28.31, still
capped at 256 KiB and marked when cut.

Regression from backnotprop#2191.
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.

[Feature Request] Copy selection as rich text (for Outlook and Slack)

2 participants