Skip to content

v1.0.0: rewrite as Page Notes on the Hacktoolkit skeleton - #22

Open
jontsai wants to merge 33 commits into
masterfrom
feature/v1-rewrite
Open

jontsai wants to merge 33 commits into
masterfrom
feature/v1-rewrite

Conversation

@jontsai

@jontsai jontsai commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Replaces the 2021 Next.js / Manifest V2 "URL Notes" app, which no longer loads in current Chrome, with a vanilla Manifest V3 extension built from skeleton-chrome-extension.

What it does

  • One note per page, dated entries, no Save button. The editor is today's entry and saves as you type; earlier entries edit in place. The note follows the page across URL variants using the Organize Tabs rules (copied for now, Extract URL canonicalization and duplicate rules into a shared module for other Hacktoolkit extensions organize-tabs-chrome-extension#12 tracks sharing them). Count on the toolbar icon.
  • Captures: a local record of what you saw. Every popup open keeps the page's title, core metadata and text, as history (capped, identical captures collapse). "Capture page" keeps a page without writing anything. Nothing runs in the background; nothing is transmitted.
  • Changed since your last visit: line diff between the two newest captures, in the popup and on the note detail page.
  • Profile insights. On a LinkedIn profile the capture is parsed into a work history and analysed: overlapping full-time roles, two current roles, backwards dates, short stints, gaps, a truncated "Show all N" capture, a stated "N+ years" the visible history does not reach, and edits between visits. Years, employers, average tenure and a one-line card grouped by employer with promotions in order, ready to copy. Phrased as questions, never verdicts. Built and tested against text fixtures only.
  • Stickies: comments pinned to parts of the page. Pick an element, type; a numbered pin sits there. Anchored three ways (css rooted at a stable id, text quote with context, structural path), verified by similarity, orphaned rather than guessed. In-page layer in a shadow root, injected on the user's click; per-site always-show via optional host permissions. Copy feedback exports a numbered list with each quote and comment. Stickies round-trip through the Markdown export.
  • Sidebar: the same view beside the page, following the active tab; the icon can open it instead of the popup.
  • Capture a quote from selected text via the context menu.
  • Reminders and snooze as one-click chips under the editor; also context menu and shortcut.
  • Markdown with YAML frontmatter: zip export (captures included as plain text), import with merge, and an Obsidian vault folder connection that writes every save.
  • All notes with search, reminders view, note detail page, settings with a rules tester, store listing kit, screenshot generator.

Naming

The display name is only in manifest.json and read at runtime everywhere else. Currently "Page Notes"; "Marginalia" is the other candidate.

Verification

54 unit tests (note model, Markdown round-trip, zip writer, URL rules, profile parser and analysis) and a headless e2e suite covering every feature including re-import merge and reminders; an adversarial review pass found ten issues, all fixed with tests. CI runs everything and attaches the zip.

Closes #16 (delete), #17 (edit), #10 (all notes view). Supersedes #3 (encryption) and #4 (cloud sync), which are explicit non-goals for v1.

🤖 Generated with Claude Code

jontsai and others added 30 commits October 10, 2026 16:26
Replaces the 2021 Next.js / Manifest V2 "URL Notes" app with a vanilla
Manifest V3 extension built from skeleton-chrome-extension.

- One note per canonical page, with dated entries. Page identity uses
  the Organize Tabs URL rules (copied for now, see organize-tabs#12), so
  a note on a PR shows on its Files tab and a Jira issue on its board.
- Presence badge with the entry count on the toolbar icon.
- Capture selected text as a quote from the context menu.
- Reminders (presets or a time) that notify and reopen the page;
  snooze a tab (close now, reopen later) from the popup, context menu
  or a keyboard shortcut.
- Page snapshot (title, description, leading text) stored locally with
  the first entry, via the activeTab grant. Nothing is transmitted.
- Markdown with YAML frontmatter as the format: zip export, import with
  merge, and an Obsidian vault folder connection that writes every save.
- All notes page with search, reminders view, settings with a rules
  tester. Notes in chrome.storage.local; settings in sync.
- Display name lives only in the manifest; code reads it at runtime.
- 31 unit tests (note model, Markdown round-trip, zip writer, URL rules)
  and a 20-check headless e2e suite; CI with a zip artifact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Every save or popup open on a noted page captures the page locally:
  title, core metadata (canonical, OpenGraph, author, published, JSON-LD
  types) and text. Captures are kept as history (capped, first kept,
  identical text collapses), so there is a record of what you saw.
- Changed-since-last-visit: line diff between the two newest captures,
  surfaced in the popup and on a new note detail page.
- Profile insights: a text-based LinkedIn profile parser (flat and
  grouped positions, locations, education) and an analysis that asks
  about overlapping full-time roles, two current roles, backwards
  dates, short stints and gaps, with years, employers, average tenure
  and a one-line card grouped by employer. Edits to the history between
  visits are detected and listed. All phrased as questions, never
  verdicts; built against fixtures only.
- Note detail page with insights, diff, entries and every capture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- LinkedIn renders most text twice (visible plus a visually hidden
  copy); consecutive duplicate lines now collapse so parses match clean
  text. Fixture test doubles every line and expects the same result.
- "Show all N experiences" is read as a truncation marker and surfaced:
  N listed, M captured, open Show all and capture again.
- A stated "N+ years of experience" that the visible, complete history
  does not reach becomes an ask.
- "Capture page" in the popup keeps a page's current state without
  writing an entry; such notes are retained and listed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ettings copy

The shared ui.css now forces [hidden] to display:none, which the flex
containers in the popup were overriding. Applied to the skeleton and
Organize Tabs copies of ui.css too (uncommitted there).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Self-employed and internship rows, a company line with no employment
type, lowercase present, en dashes, duplicated lines, education with no
degree line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Chrome's HSTS preload upgrades linkedin.com to https on the CI runner,
so an exact http URL match found no tab.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Markdown round-trip: entries merge by minute and text, so re-importing
  a backup over an existing note no longer duplicates every entry.
- Imports keep the existing note's reminder; a file's reminder is only
  honoured for a new note and only if it is still in the future, so a
  restore cannot fire a burst of stale alarms.
- Snoozing without text records a "Snoozed until" entry, so the note
  survives the reminder firing even with tab-opening off.
- Index writes are serialised; concurrent saves no longer drop rows.
- Badge passes load settings and the index once and read no notes.
- Repeat identical captures within the hour do not rewrite the note.
- One captureFor helper; per-parser capture budgets live in profile.js.
- Rules editor preserves regex flags. package.json version matches.
- el/send/fmt live in src/lib/ui.js, shared by the three pages.
- make screenshots renders README images and four store screenshots.
- e2e covers re-import merge, past reminders on import, text-less
  snooze; unit tests cover merge keys, capture throttling, budgets.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Interaction model
- The editor is today's entry: it saves as you type (debounced, and on
  blur, hide and close). No Save button. Reopen the same day and the
  text is there to keep editing. Earlier entries edit in place on click
  and save as you type; delete on hover.
- Reminders and snooze are one-click chips under the editor; the
  current reminder shows inline with a clear link.
- Opening the popup captures the page automatically, creating a
  capture-only note when none exists. All notes hides capture-only
  pages behind an "Include pages you only captured" toggle.
- Esc saves and closes. Page favicon in the header.

Visual pass
- Refined token system in ui.css: indigo-biased neutrals, a type scale,
  radii, layered shadows, an ease curve, reduced-motion support, and
  color-scheme set per theme so native controls follow.
- Editor as a focused card with an uppercase day label and a quiet
  saved status; chips as soft pills; earlier entries as a dated
  timeline with a rail; insight rows with warn/accent treatments.

Tests: setToday unit test; e2e covers draft create/update/remove,
editor restore on open, autosave status, no Save button, the
captures filter. 46 unit, 36 e2e, all green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brave ships with the File System Access API off, so showDirectoryPicker
does not exist there. The settings page now disables Connect, names the
flag (brave://flags/#file-system-access-api) with a copy button, and
notes that export and import work everywhere. e2e checks the state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
"(3) Jane Doe | LinkedIn" becomes "Jane Doe". Applied when a note is
created and on every capture, so a renamed profile renames the note
while captures keep the raw title. Also: an undated preamble in an
imported file sorts before the first dated section instead of at import
time, which made that test date-sensitive.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The site name is part of what the page is and carries into Obsidian
filenames and frontmatter titles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- src/panel.html is the popup view in panel layout (fluid width, full
  height). It follows its own window's active tab, refreshes when a note
  changes elsewhere, and saves what is being typed before switching.
- "Sidebar" in the popup footer opens it; a setting makes the toolbar
  icon open the sidebar instead of the popup. The icon click still
  grants activeTab, so the page is captured on that click.
- Uncaptured pages in the panel show a one-line hint, since a capture
  needs the icon click on that page.
- e2e: panel follows tab switches within its window and shows the
  active tab's entry; the setting flips openPanelOnActionClick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- src/lib/anchor.js: a sticky remembers its element three ways (css path
  rooted at the nearest stable id, text quote with context, structural
  path) and resolves css → quote → path, verified by text similarity;
  unresolvable stickies are orphans, never guesses. Unit-tested on a
  fake DOM.
- src/content/: an in-page layer in a shadow root with numbered pins,
  hover ring, click-to-edit popover that saves as you type, and a pick
  mode to add a sticky to any element. Injected on the user's click via
  activeTab; per-site "always show" registers it through optional host
  permissions requested from the popup.
- Note model: stickies ride in the note, export to a "## Stickies"
  section with hidden anchors that round-trip, and merge by id.
  feedbackMarkdown renders a numbered list with quotes for reviews.
- Popup and panel: an "On the page" section with the numbered list,
  + Sticky, Copy feedback, click to scroll to a pin, and the always-show
  toggle. Sticky-only notes count as notes.
- e2e: stickies through messages, feedback export, and the layer in a
  real DOM served locally (pins, placement, popover autosave, pick mode
  anchoring, orphan reporting). Loopback is excluded from the DNS block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d delay

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r matching

- notes.js: `links` on a note is the list of the other pages' canonical keys,
  symmetric, set by linkNotes/unlinkNotes. Keys round-trip through the Markdown
  frontmatter (`links: [...]`) and resolve to ids with noteId(); mergeNotes
  unions them; indexEntry counts them. Title cleanup strips only leading unread
  counters and keeps site names.
- store.js: a note that only holds a link is kept, not treated as empty. One-time
  migration cleans unread counters from saved titles and the index together.
- settings.js: `launch` is 'popup' | 'sidebar' | 'floating'; the earlier
  openInSidePanel switch migrates to 'sidebar'.
- picker.js: matchRows filters the index on every typed word across title,
  site, URL and latest note, title hits first, newest first.
- Tests for all of the above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…layer reboot

- The toolbar icon opens a popup, toggles the side panel, or (floating mode)
  reaches onClicked, which captures the page, injects the layer and shows the
  widget. applyLaunchMode() sets the popup and the panel behaviour: a popup
  takes the click before anything else sees it, so it is cleared for the other
  two modes. The mode is cached so the side panel fallback on browser pages can
  open synchronously inside the click gesture. A failed launch marks the badge.
- layer.js: a draggable floating button and a draggable window holding an
  iframe of the extension's own panel page, inside the shadow root; positions
  remembered through the `widgetLayout` message. The frame asks the layer to
  close by postMessage, accepted only from the frame and the extension origin.
- annotate.js: a layer orphaned by an extension reload (its chrome.runtime.id is
  gone) is replaced, not reused; boot() removes stale hosts.
- Messages: link, unlink, links (resolved rows, missing pages flagged),
  widgetLayout; `current` returns the linked rows; deleteNote removes the page
  from the pages it was linked to. Panel and popup refreshes never capture.
- manifest: panel.html and the icon are web-accessible for the widget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bed mode

- explorer.js/css: a shared Sites > Pages > Page browser for All notes and the
  sidebar. Navigation is a hierarchy, never a toggle: up() climbs one level, a
  breadcrumb jumps to any ancestor, the narrow layout shows one level at a time.
  Across all sites, pages are listed under a heading per site. The preview lists
  linked pages, navigates to them, and links new ones with the picker.
- Sidebar: the header arrow lands on the current site's pages, the library
  arrow hides at the top, "Current page" in the footer returns; the header names
  the level. A close control shuts the panel. Saved-note banner and sticky
  show/hide controls.
- popup.js runs three ways: popup, side panel, and embedded in the floating
  widget (bound to its tab, dismiss() instead of window.close()). Drafts carry
  the displayed page, saves retry, refreshes are versioned.
- "Linked pages" card with the type-to-find picker in popup, sidebar and widget;
  read-only list on the details page.
- Options: three-way launch mode selector; the explorer replaces the old list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- e2e: 144 checks. New coverage: launch modes and the popup/behaviour pair with
  the legacy migration; the widget layout message; the embedded panel's Esc; the
  layer's floating button and window including drag by synthetic pointer events
  and the dead-layer reboot; the sidebar's hierarchical navigation, site groups,
  level titles and close control; link, unlink, links, markdown export of links,
  delete cleanup; the picker's filtering and keyboard; link navigation in the
  library. Panel reliability: passive refreshes, save failure and retry, held
  responses, out-of-order lookups.
- make check covers content scripts and stops on the first error.
- README, STORE_LISTING and HANDOFF describe the new behaviour and the
  verification done in a real headless Brave for floating mode.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- vault.js: syncFolder(folder, store, state) reconciles one profile's notes
  with the Markdown files in the shared folder. Metadata-only scan; a file is
  read when its mtime or size changed and compared by FNV hash before parsing;
  merged with mergeNotes; written only when it differs from the note's
  serialization, so profiles converge instead of ping-ponging. Local changes
  made while the vault was unreachable are caught against the recorded hash.
  Deletions propagate both ways through the per-profile state. A note's file
  follows its title. Capture-only notes get no file. memoryFolder for tests
  and the e2e seam, handleFolder over a FileSystemDirectoryHandle.
- store.js: vaultFolder() returns the adapter with a permission status,
  replacing the write-only helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
share.js: sharePayload (stickies, entries optional), encodeShare/decodeShare
(JSON -> deflate-raw -> optional PBKDF2-SHA256 + AES-256-GCM with fresh salt
and iv -> base64 at 76 columns, wrapped in BEGIN/END PAGE NOTES lines with a
versioned header), inspectShare, applyShare (stickies by id, entries by minute
and text, idempotent), shareFileName. Web APIs only, so it runs in the worker,
the pages and node. Tests cover round trip, encryption, wrong or missing
passphrase, damaged and foreign input, and merge behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, docs

- Worker: persist writes through the folder adapter and records the file;
  deleteNote removes it; syncVault is serialised and runs from a per-profile
  alarm every 5 minutes, at startup, on Sync now, after re-allowing access, and
  at most once a minute when a page is viewed. vault:status carries the last
  report; `current` returns it. Messages: syncVault (writeAllToVault kept as an
  alias), vaultStatus, shareExport, shareImport (page resolved by the
  recipient's own rules, note created if needed).
- Popup/sidebar/widget: once-per-session "Allow" banner for vault access;
  Share… and Import… in the stickies card with copy, download and paste.
- Settings: Sync now with last-sync status, vault copy for synced drives and
  several profiles, import of a shared file with passphrase.
- e2e: vault sync through the in-memory folder (import, write, rename, steady
  state, deletion from elsewhere, status, alarm); share export/import with
  passphrase, wrong passphrase, idempotence, foreign text; the sidebar share
  panel. 164 checks. README, STORE_LISTING and HANDOFF updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… files

- vault.js: observeHandle() wraps FileSystemObserver (feature-detected) and
  reports changed Markdown file names, or "full" when the browser lost track;
  changeCollector() batches a burst into one flush. syncFolder() takes `only`:
  it stats just those files, merges them, handles reported deletions, and
  leaves unrelated notes alone. memoryFolder and handleFolder gain stat().
- The side panel, the floating widget and Settings are long-lived pages and
  watch the vault folder while open, asking the worker for a scoped sync as
  files land. The worker carries the same watch, re-armed on every wake, which
  is a no-op until a browser exposes the observer to service workers (Chromium
  155 does not). The 5-minute alarm and the look-at-a-page sync stay as the
  safety net.
- e2e: a scoped sync over the in-memory folder; the real observer over the
  origin-private file system pins the record shape. 167 checks. Docs updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es today's entry

The entry landed from the simulated other profile was dated noon today; in
CI's UTC clock that made it the newest entry of the day and the editor showed
it instead of the one the later check expects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both plain names are taken in the Chrome Web Store: a "Page Notes" with
10k+ users, a "Marginalia" study-notes extension, and Marginalia Search. The
combined name is deliberate: the brand is Marginalia, and a search for page
notes still finds it. manifest `name` is "Marginalia (Page Notes)",
`short_name` is "Marginalia"; code reads them at runtime, the short name for
compact spots (pins, widget bar, badge tooltip) and the full name for titles
and the store. The store summary fits 132 characters.

User-visible format tokens are renamed now, before any file exists: share
envelope -----BEGIN/END MARGINALIA-----, header marginalia/1, file suffix
.marginalia.txt, frontmatter app: marginalia. Internal keys (IndexedDB name,
storage keys, the data-page-notes host attribute, pn: messages) stay by rule.
The repo name stays notes-chrome-extension.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jontsai and others added 3 commits October 17, 2026 12:18
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The index keeps each note's link keys (indexEntry.linkKeys; old rows rebuild
once) and the list message resolves them to titles and sites. A page row shows
a small mark with the number of linked pages beside its entry and sticky
counts; the names are in the tooltip, and the preview still lists them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nned

The author comes from the manifest ("Hacktoolkit (@hacktoolkit)", parenthetical
dropped), so the name rule still holds. The library header in the sidebar is
sticky and the search field focuses without scrolling, so the title no longer
slides out of view inside the widget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Be Able to Delete Notes

1 participant