Skip to content

Persistent per-page comments (view/create split, tags, threads, import/export) - #305

Open
craighagan wants to merge 9 commits into
simov:mainfrom
craighagan:main
Open

Persistent per-page comments (view/create split, tags, threads, import/export)#305
craighagan wants to merge 9 commits into
simov:mainfrom
craighagan:main

Conversation

@craighagan

Copy link
Copy Markdown

Adds persistent, per-page comments for rendered markdown: select any text to attach a comment, anchored and highlighted inline, with a sidebar for browsing/filtering/searching.

What's included

  • Comments panel: select text → tooltip or Cmd/Ctrl+Shift+K → comment input. Sidebar lists all comments for the page, filterable by status/tag/severity, with search.
  • Tags & severity: optional note/question/suggestion/issue/outdated/action-needed tag and low/medium/high/critical severity per comment, rendered as pills.
  • Resolve/reopen, reply threads, suggestion mode (propose replacement text for the selected passage, rendered as a diff in the sidebar).
  • Import/export: export as JSON, or export as Markdown with the comments written back into the source as <!-- COMMENT: ... --> markers — a round-trip mechanism for sharing comments via the document itself (e.g. committing them to a repo) instead of separate storage. Import reads a previously exported JSON file back in.
  • Storage: chrome.storage.local, keyed by page URL. No new permissions beyond what the extension already needs for local file access.
  • Two independent settings under Content Options:
    • comments (default true) — view-only: highlights + sidebar browsing.
    • commentsWrite (default false) — adds everything needed to author comments (tooltip, shortcut, context menu, all mutating sidebar actions). Has no effect without comments also on. New installs default to view-only.
  • Full write-up in COMMENTS-README.md, plus a Content Options table entry in README.md.
  • Manifest bumped to 5.4.

Why the read/write split

Auto-highlighting arbitrary local files and letting the extension append/inject comment UI everywhere felt like it warranted a more conservative default than "on and fully interactive everywhere," especially for a viewer that's frequently pointed at other people's files. Splitting view from create lets someone browse comments left by a teammate (via the exported/imported JSON or inline markers) without the extension also being able to mutate anything, and keeps the interactive surface (selection listeners, keyboard shortcuts, context menu) opt-in.

Implementation notes for review

  • All comment-content rendering (sidebar, comment items, tooltip, input/edit/reply boxes) goes through Mithril (m()), matching the rest of the extension's rendering approach — no innerHTML string-building for anything that includes comment text, tags, or IDs.
  • Every comment entering the system (loaded from storage, parsed from inline markers, or imported from JSON) passes through an allowlist sanitizer: tags/severities must match a fixed set, IDs must match [\w-]+, and text fields are length-capped.
  • Cross-element anchoring: a comment anchored to text that spans an inline element boundary (e.g. selection starting in plain text and continuing into <code> or <a>) is highlighted by wrapping each affected text node individually, rather than requiring the whole match to sit in one DOM node.
  • Highlights are automatically reapplied if the rendered content is replaced wholesale — autoreload picking up a file change, a theme switch, or toggling raw view — via a debounced MutationObserver.
  • The comments UI mounts on document.documentElement (a sibling of <body>) rather than as a child of <body>, since the existing app's m.mount($('body'), ...) fully owns and diffs body's children on every redraw.

Testing

  • All touched files pass node --check.
  • build/package.sh chrome builds cleanly.
  • Manually exercised: add/edit/reply/resolve/delete a comment, Resolve All / Delete All (both confirm before acting), tag/severity filtering, search, JSON export/import, Markdown export with inline markers, keyboard shortcuts (Cmd/Ctrl+Shift+K, Ctrl+]/Ctrl+[, Cmd/Ctrl+Enter, Escape), highlight survival across autoreload/theme/raw-view toggles, and the read-only vs. write-enabled setting combinations.
  • No automated test suite exists in this repo; testing above was manual (load-unpacked in Chrome + click-through).

Select text and click the 💬 tooltip or press ⌘⇧K to add comments.
Comments are stored in chrome.storage.local and can be exported to
a .comments.json file via the sidebar download button.

- content/comments.js: selection handling, comment input UI, highlights,
  sidebar panel, text anchoring, export trigger
- content/comments.css: theme-aware styling with dark mode and responsive
- background/comments.js: context menu, chrome.storage.local CRUD, export
  via downloads API
- manifest.chrome.json: +contextMenus, +downloads permissions, v5.3.1
- DESIGN-comments.md: design doc with architecture and review outcomes
Adds a 'comments' toggle in the popup Content tab (defaults to on).
When disabled, comments scripts and CSS are not injected.
Includes storage migration for existing installs.
Resolve All marks all comments as resolved in one click.
Delete All prompts for confirmation then clears all comments.
Two export options in sidebar:
- ⬇ JSON: exports .comments.json (structured data)
- ⬇ MD: exports .commented.md with HTML comments injected
  inline after each anchored text selection

Markdown export finds each comment's anchor text in the source
and inserts <!-- COMMENT: body --> immediately after it.
Resolved comments are tagged <!-- COMMENT [RESOLVED]: body -->.
Click Edit on any comment to turn the body into an editable textarea.
Save with ⌘Enter or the Save button. Cancel with Escape.
Phase 1 — Metadata & Filtering:
- Author name (from chrome.storage.sync 'commentsAuthor')
- Comment tags: note, question, suggestion, issue, outdated, action-needed
- Severity levels: low, medium, high, critical (colored pills)
- Filter sidebar by status, tag, severity
- Search across comment text, anchors, tags, authors
- Keyboard nav: Ctrl+] next, Ctrl+[ previous comment

Phase 2 — Editing Power:
- Suggestion mode: propose replacement text with diff display
- Import comments from .comments.json file (merge or replace)
- Search box in sidebar with real-time filtering
- Auto-linkify URLs in comment body

Phase 3 — Collaboration & Polish:
- Threaded replies on each comment
- Extension icon badge showing unresolved count per tab
- Click anywhere on comment card to scroll to highlighted text
- Orphan handling: unanchored comments show warning pill + heading hint
- Improved button tooltips for clarity
- Roadmap document with collaboration strategy
- Parse <!-- COMMENT: ... --> tags from markdown source on page load
  (fetches raw file via XHR if <pre> element is gone)
- Imported inline comments merge with stored comments and become editable
- Click anywhere in document closes sidebar (configurable via commentsCloseOnClick)
- Orphan detection: comments whose anchor text is missing show ⚠️ pill + heading hint
- Fixed scroll-to-highlight reliability (replaced scrollIntoView with absolute positioning)
- Fixed sidebar header button wrapping (nowrap, tighter padding)
- Keyboard nav: Ctrl+] next, Ctrl+[ previous comment
- Severity-colored highlight borders on anchored text
…n Mithril

Adversarial review of the comments feature (SDE/SysDE/security) found
blocking issues: XSS via unescaped tag/severity/id in innerHTML,
silent data loss on MV3 service worker restart, and comments silently
going unanchored when their anchor text spans an inline element
boundary. A second review round after remediation found further
issues (dead fallback code path, comments UI getting destroyed by the
main app's Mithril redraw, stale controlled-textarea state).

- Split content.comments (view: highlights + sidebar) from
  content.commentsWrite (create: tooltip, cmd+shift+K, context menu,
  all mutating actions). New installs default to view-only; existing
  users who already had comments enabled keep write access via
  migration.
- Rewrite all comment-content rendering (sidebar, comment items,
  tooltip, input/edit/reply boxes) as Mithril components instead of
  innerHTML template strings, eliminating the escaping-surface class
  of bug by construction. Add sanitizeComment/sanitizeTag/
  sanitizeSeverity/sanitizeId allowlist validation on every entry
  point (storage load, inline-comment parse, JSON import, fresh
  creation).
- Guard against treating a failed/asleep-service-worker sendMessage
  response as "zero stored comments," which previously risked
  overwriting real storage with an inline-only merge.
- Fix cross-element highlight anchoring: highlightAcrossNodes() is
  now actually reachable and wraps per-text-node segments when a
  comment's anchor spans an inline element (bold/code/link).
- Mount the comments UI and toggle button on document.documentElement
  instead of document.body, since content/index.js's own body-level
  Mithril mount fully owns and diffs body's children and was
  destroying both on every theme/autoreload/raw-view redraw.
- Add a debounced MutationObserver to reapply highlights after the
  main app's Mithril re-render replaces the html/markdown container
  wholesale.
- Fix stale controlled-textarea state in the edit/reply boxes and
  strip draft scratch fields before persisting.
- Add a confirmation prompt to Resolve All, matching Delete All.
- Clarify popup labels: comments/commentsWrite now display as
  'Show comments' / 'Create comments' instead of raw storage keys.
- Bump manifest to 5.3.2.
- Add COMMENTS-README.md documenting the shipped comments feature:
  the comments/commentsWrite read-write settings split, usage,
  storage/merge behavior, cross-element anchoring, autoreload
  highlight recovery, import/export, data model, and keyboard
  shortcuts. Link it from README.md's Content Options table.
- Remove DESIGN-comments.md and ROADMAP-comments.md from version
  control. Both describe an earlier, abandoned design (native
  messaging host + sidecar JSON file on disk) and a stale roadmap
  that no longer match the shipped chrome.storage.local-based
  implementation. Kept locally for reference, now gitignored.
- Bump manifest to 5.4, matching the project's established two-part
  version scheme (v2.7 through v5.3) rather than continuing the
  5.3.1/5.3.2 three-part departure introduced with this feature.
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