chore: track openspec work
This commit is contained in:
parent
d891cc9362
commit
1037722196
7 changed files with 276 additions and 0 deletions
2
openspec/changes/add-notes-extension/.openspec.yaml
Normal file
2
openspec/changes/add-notes-extension/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-08-03
|
||||
50
openspec/changes/add-notes-extension/design.md
Normal file
50
openspec/changes/add-notes-extension/design.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
## Context
|
||||
|
||||
Greenfield Manifest V3 Chrome extension (no existing code). See proposal.md for motivation. Must also run on Brave and other Chromium-based browsers, which rules out relying on `chrome.storage.sync` (Google-account-bound) for any future sync story, and puts some weight on verifying newer APIs (`chrome.sidePanel`) behave the same outside Chrome itself.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Establish a storage layer and data model that a future sync change can build on without reshaping existing records.
|
||||
- Keep the editing surface's text-interaction behavior indistinguishable from native browser text editing.
|
||||
- Reuse a single URL-to-key function everywhere a note needs to be located (saving, loading, and matching against currently open tabs), so key derivation logic never drifts between call sites.
|
||||
|
||||
**Non-Goals:**
|
||||
- Implementing remote/multi-device sync (data model only).
|
||||
- Search or delete UI within the notes-list page.
|
||||
- Markdown rendering/syntax highlighting in the editor.
|
||||
- Handling modifier-click (open-in-new-tab) semantics on notes-list entries.
|
||||
- A UI for changing the URL-key derivation rule (code-level only).
|
||||
|
||||
## Decisions
|
||||
|
||||
**Architecture**: a background service worker (tab/navigation event handling, tab-focus-or-navigate logic for notes-list clicks), a side panel page (per-tab, via `chrome.sidePanel`), and a separate notes-list extension page (its own tab). All three share one storage module so key derivation and the IndexedDB schema aren't duplicated.
|
||||
|
||||
**URL key derivation**: one pure function, `buildNoteKey(url, config)`, used identically by (a) the side panel when loading/saving a note for the active tab, and (b) the notes-list page's click handler when checking whether any open tab already matches the clicked note. Default config: `scheme://domain/path?search`, hash excluded. The function and its config live in one module so a future code-level change to the rule is a single edit, not a hunt across call sites.
|
||||
|
||||
**In-page navigation detection**: `chrome.webNavigation.onHistoryStateUpdated` (pushState/replaceState) and `onReferenceFragmentUpdated` (hash-only changes) re-trigger key resolution for a tab without a full page load. This is the "naive" SPA support agreed on — it only reacts to URL changes; a SPA that changes visible content without ever changing the URL has nothing for this extension to key off, and that's fine since notes are keyed by URL. Requires the `webNavigation` permission.
|
||||
|
||||
**Storage**: IndexedDB, one object store (`notes`), keyed by the note's own `id` (not by URL key), with a non-unique index on the derived URL key and an index on `updatedAt`. Non-unique on the key (rather than enforcing uniqueness in the DB) because soft-deleted records for a URL may coexist with a later, newer record for the same URL once delete ships in a future change; the application invariant ("at most one non-deleted note per key") is enforced by retrieval logic (filter `deletedAt == null`), not by a DB constraint. The `updatedAt` index lets the notes-list page pull a reverse-chronological cursor directly instead of loading everything into memory to sort.
|
||||
|
||||
**Editor**: a plain `<textarea>` is the only interactive element (see specs/note-editor for why). Autosave is a debounced write to IndexedDB on keystroke. In-panel find intercepts Ctrl+F/Cmd+F at the document level while the panel has focus, searches `textarea.value` directly, and on a match uses `setSelectionRange` plus a scroll calculation to bring the match into view — no separate highlight styling is needed since the browser's native selection rendering *is* the highlight.
|
||||
|
||||
**Cross-panel consistency**: a `BroadcastChannel` (shared by all extension pages, same origin) carries `{ key, updatedAt }` messages on every save. A panel showing that key reloads from IndexedDB, unless its own textarea currently has focus, in which case it defers the reload until the textarea loses focus.
|
||||
|
||||
**Notes-list click handling**: query `chrome.tabs.query({})`, compute `buildNoteKey` for each tab's current URL, and compare to the clicked note's key.
|
||||
- Match found → `chrome.tabs.update(tabId, { active: true })` + `chrome.windows.update(windowId, { focused: true })`, then `chrome.sidePanel.open({ tabId })`.
|
||||
- No match → navigate the tab currently hosting the notes-list page to the note's URL, then `chrome.sidePanel.open({ tabId })` on that same tab.
|
||||
|
||||
**Manifest permissions**: `sidePanel`, `tabs`, `webNavigation`, `unlimitedStorage` (IndexedDB itself needs no permission, but removing the default storage quota ceiling is worth doing proactively since note content is expected to accumulate over the extension's lifetime).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] Changing the URL-key derivation rule after notes already exist orphans previously saved notes (their old keys no longer match freshly computed keys for the same pages) → Mitigation: none in this change; this is an accepted, documented limitation of the current code-level-only configuration.
|
||||
- [Risk] `chrome.sidePanel.open()` requires being called within a live user gesture; a click handler that awaits async work (e.g., tab creation) before calling it risks the gesture window having expired → Mitigation: call `sidePanel.open()` as the first synchronous statement in the click handler, verify empirically in both Chrome and Brave during implementation.
|
||||
- [Risk] MV3 background service workers are terminated when idle, so any tab-to-key mapping cached only in worker memory would be lost → Mitigation: derive the mapping on demand from `chrome.tabs.query`/navigation events rather than caching it.
|
||||
- [Risk] Two tabs editing the same note at the literal same instant can still race at the IndexedDB write level (last write wins), even with the focus-based refresh guard → Mitigation: accepted for this change; real conflict resolution is deferred to future sync work.
|
||||
- [Risk] `chrome.sidePanel` is a newer API; Brave/other Chromium forks may lag or differ subtly (per-tab `setOptions`, gesture requirements for `open()`) → Mitigation: manual verification during implementation (see Open Questions).
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Does `chrome.sidePanel` (per-tab `setOptions`, gesture requirements for `open()`) behave identically in Brave and other Chromium forks as in Chrome? To be verified empirically during implementation; a difference would change the implementation approach for that platform, not the specified behavior.
|
||||
- Exact autosave debounce interval — a tuning detail with no effect on any requirement.
|
||||
30
openspec/changes/add-notes-extension/proposal.md
Normal file
30
openspec/changes/add-notes-extension/proposal.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
## Why
|
||||
|
||||
Notes about a specific web page (research, follow-ups, half-finished thoughts) currently have nowhere to live that's actually attached to that page. They end up scattered across separate notes apps, disconnected from the URL they're about, and lost between browser restarts. A browser extension that keeps a markdown note per page, available instantly from a side panel and persisted locally, solves this directly — and if the underlying data model is shaped correctly from day one, it also sets up a path to sync notes across devices/browsers later without a rewrite.
|
||||
|
||||
## What Changes
|
||||
|
||||
- New Chrome (Manifest V3) extension with a `chrome.sidePanel`-based UI, opened per tab via toolbar icon or keyboard shortcut.
|
||||
- One markdown note per page, keyed by a normalized URL (configurable key-derivation, SPA-aware), edited in a plain `<textarea>` for fully native copy/paste/select/undo behavior.
|
||||
- In-panel find (Ctrl/Cmd+F intercepted while the panel has focus) that highlights matches via the textarea's native text selection.
|
||||
- Local persistence in IndexedDB with a data model (`id`, `url`, `pageTitle`, `favIconUrl`, `content`, `createdAt`, `updatedAt`, `deletedAt`) designed to be sync-ready, though sync itself is out of scope for this change.
|
||||
- Live refresh across side panel instances that are open on the same note key (e.g. the same URL open in two tabs), so edits made in one aren't silently clobbered by a stale save from another.
|
||||
- A dedicated notes-list extension page, styled after `chrome://history` / `brave://history`: entries grouped by day and sorted by `updatedAt`, showing favicon + page title. Clicking an entry focuses an already-open tab showing that page if one exists, otherwise navigates the current tab to it — then opens the side panel with that note.
|
||||
- Dark theme by default.
|
||||
|
||||
Out of scope for this change (explicitly deferred): remote/multi-device sync, search and delete within the notes-list page, markdown syntax highlighting, modifier-click (new-tab) handling on notes-list entries.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `notes-storage`: Local persistent storage of one note per normalized page URL, including the URL-key derivation rules (default and configurable) and a data model shaped for future sync.
|
||||
- `note-editor`: The side-panel markdown editor scoped to a browser tab — native text editing, in-panel find, autosave, and live consistency when the same note is open in multiple panels.
|
||||
- `notes-list`: A dedicated page listing all notes grouped by day/sorted by recency, and the navigation behavior (focus-existing-tab vs. navigate-current-tab) when an entry is clicked.
|
||||
|
||||
### Modified Capabilities
|
||||
(none — this is the first change in the project)
|
||||
|
||||
## Impact
|
||||
|
||||
- New codebase: extension manifest (permissions: `sidePanel`, `tabs`; IndexedDB needs no manifest permission), background service worker, side panel page, notes-list page.
|
||||
- No existing code or specs affected (repository currently has no application code).
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
## Purpose
|
||||
|
||||
Provides the per-tab side panel where a user reads and edits the markdown note for the current page, preserving native text-editing behavior and staying consistent when the same note is open in more than one tab.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Side panel scoped to a browser tab
|
||||
The system SHALL provide a side panel, opened via the toolbar action or a keyboard shortcut, that displays the note for the active tab's current page. Each tab SHALL have its own panel instance, so editor state in one tab is unaffected by other tabs.
|
||||
|
||||
#### Scenario: Opening the panel shows the current tab's note
|
||||
- **WHEN** a user opens the side panel while viewing a page
|
||||
- **THEN** the panel loads and displays the note associated with that page's URL key
|
||||
|
||||
#### Scenario: Switching tabs shows the new tab's note
|
||||
- **WHEN** a user activates a different tab that also has the side panel open
|
||||
- **THEN** that tab's panel shows the note for its own current URL, independent of other tabs' panels
|
||||
|
||||
### Requirement: Native text editing
|
||||
Note content SHALL be edited in a plain text input area, so that text selection, copy, cut, paste, and undo/redo behave exactly as native browser text-field editing, with no custom interaction to learn.
|
||||
|
||||
#### Scenario: Copy and paste behave like any text field
|
||||
- **WHEN** a user selects text in the note and copies it, then pastes it elsewhere
|
||||
- **THEN** the pasted text matches the selection exactly, using the browser's native clipboard behavior
|
||||
|
||||
### Requirement: Autosave on edit
|
||||
Edits to a note SHALL be saved automatically as the user types, without requiring an explicit save action.
|
||||
|
||||
#### Scenario: Content is saved without a save button
|
||||
- **WHEN** a user types in the note editor and then closes the panel or navigates away
|
||||
- **THEN** the typed content is already persisted and reappears the next time the note is opened
|
||||
|
||||
### Requirement: In-panel find
|
||||
WHEN the panel has focus and the user presses the find shortcut (Ctrl+F / Cmd+F), the system SHALL open an in-panel find control instead of triggering the browser's page-level find, allow searching the note's text, and indicate matches using the editor's native text selection.
|
||||
|
||||
#### Scenario: Find highlights a match via selection
|
||||
- **WHEN** a user opens in-panel find and types a search term that matches note text
|
||||
- **THEN** the matching text becomes selected in the editor and is scrolled into view
|
||||
|
||||
#### Scenario: Browser page find is not triggered
|
||||
- **WHEN** the side panel's editor has focus and the user presses Ctrl+F / Cmd+F
|
||||
- **THEN** the browser's own page-level find UI does not open
|
||||
|
||||
### Requirement: Consistency across panels sharing a note
|
||||
WHEN the same note is open in more than one tab's panel and one panel saves an edit, the other open panel(s) showing that note SHALL refresh to the latest saved content, except that a panel whose editor currently has input focus SHALL defer the refresh until focus leaves the editor.
|
||||
|
||||
#### Scenario: Unfocused panel picks up the update
|
||||
- **WHEN** a note open in tab A is edited and saved, and the same note is open in tab B without focus in B's editor
|
||||
- **THEN** tab B's panel updates to show the content saved from tab A
|
||||
|
||||
#### Scenario: Focused panel is not overwritten mid-edit
|
||||
- **WHEN** a note open in tab A is edited and saved, and the same note is open in tab B with focus currently in B's editor
|
||||
- **THEN** tab B's editor content is not replaced until focus leaves B's editor
|
||||
|
||||
### Requirement: Dark theme by default
|
||||
The side panel SHALL render using a dark color theme by default.
|
||||
|
||||
#### Scenario: Panel opens in dark theme
|
||||
- **WHEN** a user opens the side panel with no prior theme preference set
|
||||
- **THEN** the panel is displayed using the dark theme
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
## Purpose
|
||||
|
||||
Provides a dedicated page for browsing all saved notes, grouped and sorted the way a browser history page is, and lets the user navigate back to a note's page from that list.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Notes grouped by day and sorted by recency
|
||||
The notes-list page SHALL display all non-deleted notes grouped into sections by the calendar day of each note's last-updated timestamp, with sections ordered most-recent-day first, and notes within a section ordered by last-updated time descending.
|
||||
|
||||
#### Scenario: Notes updated today appear before older notes
|
||||
- **WHEN** one note was last updated today and another was last updated yesterday
|
||||
- **THEN** the notes-list page shows a section for today containing the first note above a section for the earlier day containing the second note
|
||||
|
||||
### Requirement: Entries show page title and favicon
|
||||
Each entry in the notes-list SHALL display the page's captured title as its primary text, with the page's favicon, rather than the raw URL. If no title was captured for a note, the entry SHALL fall back to displaying the URL.
|
||||
|
||||
#### Scenario: Title shown instead of URL
|
||||
- **WHEN** a note has a captured page title
|
||||
- **THEN** its entry in the list shows that title as the primary text, not the raw URL
|
||||
|
||||
#### Scenario: Missing title falls back to URL
|
||||
- **WHEN** a note has no captured page title
|
||||
- **THEN** its entry in the list shows the note's URL as the primary text
|
||||
|
||||
### Requirement: Clicking an entry focuses an already-open matching tab
|
||||
WHEN a user clicks a notes-list entry and a browser tab is already open at a URL that resolves to the same note key, the system SHALL bring that tab and its window into focus and open the side panel on it showing that note, without opening a new tab or navigating any tab.
|
||||
|
||||
#### Scenario: Existing tab is focused instead of duplicated
|
||||
- **WHEN** the note's page is already open in another tab and the user clicks its entry in the notes list
|
||||
- **THEN** that existing tab becomes the active tab, its window is brought to the front, and its side panel opens showing the note, with no new tab created
|
||||
|
||||
### Requirement: Clicking an entry navigates the current tab when no match exists
|
||||
WHEN a user clicks a notes-list entry and no open tab currently resolves to that note's key, the system SHALL navigate the tab currently showing the notes-list page to the note's URL and open the side panel on it showing that note.
|
||||
|
||||
#### Scenario: No existing tab is open for the note
|
||||
- **WHEN** the user clicks an entry for a note whose page is not open in any tab
|
||||
- **THEN** the tab that was showing the notes-list page navigates to that note's URL and its side panel opens with that note loaded
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
## Purpose
|
||||
|
||||
Persists exactly one markdown note per page, keyed by a normalized form of the page's URL, in local storage that survives restarts and is shaped to support multi-device sync in a future change.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Note identified by normalized URL key
|
||||
The system SHALL derive a storage key for a page from its URL using a normalization rule that, by default, includes the scheme, domain, path, and query string, and excludes the fragment/hash.
|
||||
|
||||
#### Scenario: Two URLs differing only by hash resolve to the same note
|
||||
- **WHEN** a user visits `https://example.com/article#section1` and later `https://example.com/article#section2`
|
||||
- **THEN** both pages resolve to the same note key and show the same note content
|
||||
|
||||
#### Scenario: Two URLs differing by query string resolve to different notes
|
||||
- **WHEN** a user visits `https://example.com/search?q=cats` and `https://example.com/search?q=dogs`
|
||||
- **THEN** each URL resolves to a distinct note key with independent content
|
||||
|
||||
### Requirement: Key derivation rule is configurable
|
||||
The system SHALL allow the URL normalization rule used for key derivation to be changed at the code level, without requiring a UI for end users in this change.
|
||||
|
||||
#### Scenario: Key builder is swapped
|
||||
- **WHEN** a developer changes the configured key-derivation rule (e.g., to include the hash)
|
||||
- **THEN** notes saved after the change use keys produced by the new rule
|
||||
|
||||
### Requirement: In-page navigation is detected
|
||||
The system SHALL detect URL changes that occur without a full page load within a tab (e.g., History API `pushState`/`replaceState` navigation used by single-page applications) and re-resolve the note key for that tab accordingly.
|
||||
|
||||
#### Scenario: In-page navigation changes the resolved note
|
||||
- **WHEN** a single-page application changes the browser URL via the History API without a full page load
|
||||
- **THEN** the note associated with the new URL is resolved for that tab without requiring a manual reload
|
||||
|
||||
### Requirement: Note content persists across restarts
|
||||
The system SHALL persist each note's content in local storage that survives browser restarts and computer restarts, without requiring network access.
|
||||
|
||||
#### Scenario: Note survives a browser restart
|
||||
- **WHEN** a user writes content in a note and later restarts the browser
|
||||
- **THEN** reopening the side panel on the same page shows the previously written content
|
||||
|
||||
### Requirement: Note record data model
|
||||
Each stored note record SHALL include: a stable unique identifier, the derived URL key, the page title and favicon URL captured at save time, the markdown content, a creation timestamp, a last-updated timestamp, and an optional deletion timestamp.
|
||||
|
||||
#### Scenario: Metadata captured on first save
|
||||
- **WHEN** a user's edits to a page's note are saved for the first time
|
||||
- **THEN** the stored record includes a unique id, the derived key, the page's title, its favicon URL, the content, and matching creation and last-updated timestamps
|
||||
|
||||
### Requirement: Soft deletion is honored by retrieval
|
||||
A note record marked with a deletion timestamp SHALL be excluded from normal retrieval (opening a note for a page) and from any listing of notes, while remaining present in storage.
|
||||
|
||||
#### Scenario: Deleted note is hidden but not erased
|
||||
- **WHEN** a note record has a deletion timestamp set
|
||||
- **THEN** it is not returned when its page's note is requested or when notes are listed, and its record still exists in storage
|
||||
47
openspec/changes/add-notes-extension/tasks.md
Normal file
47
openspec/changes/add-notes-extension/tasks.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
## 1. Extension scaffold
|
||||
|
||||
- [ ] 1.1 Create Manifest V3 `manifest.json` with `sidePanel`, `tabs`, `webNavigation`, `unlimitedStorage` permissions, action icon, and keyboard command binding to open the side panel
|
||||
- [ ] 1.2 Set up the build/dev tooling for a background service worker, a side panel page, and a notes-list page as separate entry points
|
||||
- [ ] 1.3 Wire `chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })` in the background service worker
|
||||
|
||||
## 2. Shared storage & key module
|
||||
|
||||
- [ ] 2.1 Implement `buildNoteKey(url, config)` with the default rule (`scheme://domain/path?search`, hash excluded) and a swappable config
|
||||
- [ ] 2.2 Define the IndexedDB schema: `notes` object store keyed by `id`, non-unique index on the derived URL key, index on `updatedAt`
|
||||
- [ ] 2.3 Implement `getNoteByKey(key)`, `saveNote(key, content, pageMeta)`, `listNotes()` (excludes soft-deleted), filtering `deletedAt == null` in retrieval
|
||||
- [ ] 2.4 Implement soft-delete support at the storage layer (`markDeleted(id)`), even though no UI triggers it in this change
|
||||
|
||||
## 3. In-page navigation detection
|
||||
|
||||
- [ ] 3.1 Listen for `chrome.webNavigation.onHistoryStateUpdated` and `onReferenceFragmentUpdated` in the background service worker
|
||||
- [ ] 3.2 On either event, re-resolve the note key for the affected tab and notify that tab's open side panel (if any) to reload
|
||||
|
||||
## 4. Side panel note editor
|
||||
|
||||
- [ ] 4.1 Build the side panel page shell with a dark theme applied by default
|
||||
- [ ] 4.2 On panel load/tab activation, resolve the active tab's URL to a key via the shared module and load (or initialize) that note
|
||||
- [ ] 4.3 Render the note content in a plain `<textarea>` with no other interactive text-editing controls
|
||||
- [ ] 4.4 Implement debounced autosave on keystroke, writing through the shared storage module and capturing `pageTitle`/`favIconUrl` from the active tab at save time
|
||||
- [ ] 4.5 Implement in-panel find: intercept Ctrl+F/Cmd+F while the panel document has focus, prevent the browser's page-level find, render a find bar, search `textarea.value`, and move the native selection (`setSelectionRange` + scroll into view) to each match
|
||||
- [ ] 4.6 Handle tab switching: when the panel's hosting tab changes its active URL (activation or navigation), reload the note for the new key
|
||||
|
||||
## 5. Cross-panel consistency
|
||||
|
||||
- [ ] 5.1 Open a shared `BroadcastChannel` from every side panel instance
|
||||
- [ ] 5.2 On save, post `{ key, updatedAt }` on the channel
|
||||
- [ ] 5.3 On receiving a message for the currently displayed key, reload from storage unless the textarea currently has focus; if focused, defer the reload until blur
|
||||
|
||||
## 6. Notes-list page
|
||||
|
||||
- [ ] 6.1 Build the notes-list page shell (own tab, history-page-inspired layout)
|
||||
- [ ] 6.2 Query notes via the `updatedAt` index in descending order and group them into day sections (Today / Yesterday / date) in local time
|
||||
- [ ] 6.3 Render each entry with favicon + page title (falling back to URL when no title was captured) and a formatted time
|
||||
- [ ] 6.4 On entry click: query `chrome.tabs.query({})`, compute each tab's key via the shared module, and compare to the clicked note's key
|
||||
- [ ] 6.5 If a matching tab is found: focus its window (`chrome.windows.update`) and activate it (`chrome.tabs.update`), then call `chrome.sidePanel.open({ tabId })` as the first synchronous action in the click handler
|
||||
- [ ] 6.6 If no matching tab is found: navigate the tab hosting the notes-list page to the note's URL, then call `chrome.sidePanel.open({ tabId })` on it
|
||||
|
||||
## 7. Cross-browser verification
|
||||
|
||||
- [ ] 7.1 Load the unpacked extension in Chrome and manually verify: per-tab side panel isolation, autosave/reload across restart, in-panel find, cross-panel refresh, notes-list grouping and click behavior (both matched-tab and no-match paths)
|
||||
- [ ] 7.2 Repeat the same manual verification in Brave, specifically checking `chrome.sidePanel` per-tab `setOptions`/`open()` gesture behavior parity with Chrome
|
||||
- [ ] 7.3 Note any Brave-specific discrepancies found and decide whether a workaround is needed before considering this change complete
|
||||
Loading…
Add table
Reference in a new issue