commits
Sidebar tag clicks now populate the search bar with tag:tagname tokens,
and users can type tag:xxx directly. Active tags show as removable pills
above results, with matching tag suggestions when free text matches.
Checkmark button on reading list cards swaps the reading list tag for
"read". Optimistic update with 5-second undo toast before persisting
to server. Handles custom reading list tags, rapid marking, and
silent revert on server failure.
Progressive loading concatenates pages without re-sorting, so when all
pages replace the initial cached data the order reverts to cursor order
instead of newest-first by createdAt.
Progressive loading showed all tags but only the first page of bookmarks
(~100). Clicking a tag whose bookmark wasn't loaded yet returned 0 results.
No-cache path now waits for all pages before setting state. Cache path
only replaces data when background refresh completes fully. Extracted
filtering logic into shared/bookmark-filters.ts for testability. Added
tests for initial-data endpoint and tag filtering.
IndexedDB getAll() returns records in key order (uri), not insertion
order, causing bookmarks to appear in random order on cache-hit loads.
The build script renamed the bundle to bundle.{hash}.js but didn't
update the sourceMappingURL comment, causing a 404 for bundle.js.map.
Replace broken virtualizer (gaps, scroll issues) with simple incremental
rendering via IntersectionObserver. Optimize tag filtering with pre-normalized
Set lookups instead of repeated toLowerCase. Reduce setBookmarks calls from
N+1 to 2. Guard background PDS migrations to once per user per session.
Add lightweight perf instrumentation (window.__kipclipPerf). Remove dead code
(diff.ts, optimistic.ts) and unused loading state.
For 3000+ bookmarks, the old approach fetched ALL records (32+ sequential
PDS pages) before showing anything (~15s). Now:
- First page: 5 parallel PDS requests (bookmarks, annotations, tags,
settings, preferences) → UI renders in ~1s with newest 100 bookmarks
- Remaining pages: loaded progressively in background, appended to state
as each page arrives
- Uses AT Protocol reverse=true to show newest bookmarks first
- Cache populated after all pages load, so return visits are instant
Backend: /api/initial-data now accepts bookmarkCursor/annotationCursor
params for pagination. Added listOnePage() helper to route-utils.
The cache was never working because openCacheDb() was fire-and-forget
in setSession — the DB wasn't open yet when loadWithCache() tried to
read from it. Now openCacheDb is awaited inside loadInitialData.
When cache has data, loadInitialData returns early (spinner disappears
fast) and background refresh runs silently. On first visit (no cache),
spinner shows during the full server fetch as before.
- Add list virtualization with @tanstack/react-virtual for bookmark and
reading list views, reducing DOM nodes by ~97% for large collections
- Add IndexedDB cache layer (idb) for instant loads on return visits,
with per-user database isolation and graceful fallback
- Add CID-based diffing to skip unnecessary React state updates when
server data hasn't changed
- Add /api/sync-check endpoint for lightweight change detection (3 PDS
requests vs 30+ for full fetch)
- Add tab-refocus background refresh with 60-second debounce
- Add 150ms search input debounce for smoother filtering
- Wire all mutations to update IndexedDB alongside React state
- Clear cache on logout for data hygiene
- Bundle size increase: 141KB -> 146.5KB (+3.9%)
Tags are now compared case-insensitively everywhere: filtering,
bulk operations, imports, tag creation, editing, deletion, and the
reading list. The Settings UI gains a "Merge Duplicate Tags" button.
Extract merge-duplicates logic into shared lib/migration-merge-tags.ts
and create lib/pds-migrations.ts registry so adding a migration is a
one-line change. initial-data.ts now calls runPdsMigrations() instead
of wiring each migration individually.
Apple Mail-style swipe gesture on bookmark rows in list view.
Partial swipe reveals a delete button, full swipe auto-deletes.
Disabled during select mode. Touch-only, no desktop mouse drag.
After deleting bookmarks (single or bulk), detect tags that are no longer
used by any remaining bookmark and offer to delete them via a dialog.
Also fix bulk delete partial failure bug: the frontend incorrectly assumed
successful deletes were always at the start of the URI list. The backend
now returns the actual deletedUris so the frontend correctly identifies
which bookmarks were deleted vs failed.
* feat(bulk): add bulk operations API endpoint
POST /api/bookmarks/bulk supports delete, add-tags, and remove-tags
actions. Uses applyWrites batching for deletes (10 ops per call) and
concurrent putRecord for tag updates. Returns updated bookmarks for
tag operations to enable frontend state sync without full reload.
* feat(bulk): add select mode UI with bulk actions toolbar
Select mode toggle in header (desktop + mobile), checkboxes on bookmark
cards in both card and list views, floating action toolbar with bulk
delete, add tag, and remove tag operations. Disables drag-drop and
pull-to-refresh during select mode. Escape key exits select mode.
* style: format bulk operations files
Session restore errors like expired tokens, missing sessions, and
revoked refresh tokens are now caught and returned as SESSION_EXPIRED
instead of being reported to Sentry as unexpected errors.
Fresh's staticFiles() middleware rejects non-GET/HEAD requests with 405.
Positioning it before API routes caused it to intercept POST/PUT/DELETE
requests before they could reach their handlers.
The applyWrites limit was lowered from 200 to 10 operations per call.
With annotations, each bookmark generates 2 writes, so the previous
200-bookmark chunks sent up to 400 operations — all of which failed.
Now packs bookmarks into sub-batches that fit within the 10-op limit.
Split synchronous import into a two-phase flow to avoid Deno Deploy
timeouts on large imports. The prepare step parses, deduplicates, and
stores chunked work in Turso. The frontend then calls a process endpoint
per chunk, giving a real progress bar with no timeout risk.
Deno Deploy's new platform doesn't support KV queues, causing a fatal
crash at startup. Process imports synchronously in the request handler
instead, removing the KV dependency entirely.
Store readingListTag in the com.kipclip.preferences PDS record alongside
dateFormat instead of the Turso database. This syncs the preference across
devices and keeps the database for secrets (Instapaper credentials) only.
The reading list tag input now instant-applies on blur, consistent with
the date format picker. The Instapaper trigger in the bookmarks route
fetches preferences in parallel with settings to get the tag value.
Two bugs: (1) loadInitialData overwrote localStorage with PDS defaults
before the migration check could read the user's saved value, so the
migration never triggered. (2) updatePreferences waited for the API
response before updating context state, but Settings uses <a href="/">
which triggers a full page reload — wiping React state before the
response arrived.
Fix: read localStorage before overwriting in loadInitialData, and
optimistically update context + localStorage in updatePreferences
before the API call.
Store the date format setting as a com.kipclip.preferences record on the
user's PDS instead of only in localStorage. This syncs the preference
across devices and survives browser data clearing. Includes a one-time
migration that pushes any existing localStorage value to PDS, and a
graceful fallback to localStorage when the user hasn't re-authenticated
with the new OAuth scope.
Imported bookmarks with random UUID rkeys were displayed in arbitrary
order since PDS listRecords returns lexicographic rkey order. Both
API endpoints now sort by createdAt descending after fetching.
Adds listAllRecords helper that follows AT Protocol cursors and
replaces all single-page fetches across bookmarks, tags, annotations,
and initial-data endpoints.
Move bookmark import writes from synchronous HTTP request to background
processing using Deno KV queue. This prevents Deno Deploy's 30-second
request timeout from killing large imports.
The POST /api/import endpoint now parses and deduplicates synchronously
(fast), then stores bookmark chunks in KV and enqueues background
processing. A new GET /api/import/status/:jobId endpoint lets the
frontend poll progress. The frontend shows a progress bar during import.
New files: lib/kv.ts, lib/import-queue.ts, lib/import-parsers.ts,
routes/api/import.ts, frontend/components/ImportBookmarks.tsx
deno fmt --check was failing because the workflow files had trailing
blank lines and single quotes where double quotes were expected.
* "Claude PR Assistant workflow"
* "Claude Code Review workflow"
Shows an "Archived version" link next to the saved date that opens
the bookmark URL on web.archive.org for preservation/fallback access.
The annotation sidecar note was mapped onto EnrichedBookmark but
not checked by matchesSearch(), making user notes unsearchable.
Serves /opensearch.xml so browsers can register kipclip as a search
engine. The ?q= URL parameter initializes the bookmark search state.
The greedy [^>]+ in the favicon regex matched the last href= in the
tag, which on GitHub is data-base-href (an extensionless URL that
404s). Switch to non-greedy [^>]+? to match the first href=.
The non-HTML and error code paths returned no favicon at all, so
bookmarks for sites that block the bot user-agent (e.g. GitHub)
or serve non-HTML content never got a favicon. Now all paths
fall back to origin/favicon.ico, matching what parseHtmlMetadata
already did.
Move favicon repair from a standalone endpoint to a background task
that runs fire-and-forget during initial data load. Adds documentation
for both background tasks (annotation migration and favicon repair)
with removal criteria.
The update handler read enrichment from $enriched on the bookmark
record (legacy storage), but after migration to annotation sidecars
the favicon/image live in the annotation. Now fetches the existing
annotation in parallel with the bookmark to preserve these fields.
Picker in Settings applies instantly via localStorage. All date
displays across the app use the shared formatDate utility.
Open button uses coral brand color as primary action. Share and Edit
use neutral gray outlines. URL link also uses coral instead of blue.
Toggle persisted in localStorage. Desktop shows segmented control
next to search, mobile shows a single toggle button in the toolbar.
List view is compact rows with favicon, title, URL, tags, and date.
Bookmark putRecord, annotation write, and settings fetch now run
concurrently. Tag creation in EditBookmark also runs in parallel
with the PATCH request instead of sequentially.
Move enrichment metadata ($enriched) from bookmark records to a kipclip-specific
annotation sidecar (com.kipclip.annotation) using same-rkey binding. Add optional
user notes per bookmark. Replace hover/tap overlay with a detail modal that shows
all bookmark fields. Background migration runs on login to move existing $enriched
data to annotations. Graceful fallback for sessions without the new OAuth scope.
Warn users when saving a URL that matches an existing bookmark by base
URL (ignoring query params and fragments). Users can cancel or save
anyway. AddBookmark checks client-side via AppContext; Save checks
server-side via new POST /api/bookmarks/check-duplicates endpoint.
Progressive loading showed all tags but only the first page of bookmarks
(~100). Clicking a tag whose bookmark wasn't loaded yet returned 0 results.
No-cache path now waits for all pages before setting state. Cache path
only replaces data when background refresh completes fully. Extracted
filtering logic into shared/bookmark-filters.ts for testability. Added
tests for initial-data endpoint and tag filtering.
Replace broken virtualizer (gaps, scroll issues) with simple incremental
rendering via IntersectionObserver. Optimize tag filtering with pre-normalized
Set lookups instead of repeated toLowerCase. Reduce setBookmarks calls from
N+1 to 2. Guard background PDS migrations to once per user per session.
Add lightweight perf instrumentation (window.__kipclipPerf). Remove dead code
(diff.ts, optimistic.ts) and unused loading state.
For 3000+ bookmarks, the old approach fetched ALL records (32+ sequential
PDS pages) before showing anything (~15s). Now:
- First page: 5 parallel PDS requests (bookmarks, annotations, tags,
settings, preferences) → UI renders in ~1s with newest 100 bookmarks
- Remaining pages: loaded progressively in background, appended to state
as each page arrives
- Uses AT Protocol reverse=true to show newest bookmarks first
- Cache populated after all pages load, so return visits are instant
Backend: /api/initial-data now accepts bookmarkCursor/annotationCursor
params for pagination. Added listOnePage() helper to route-utils.
The cache was never working because openCacheDb() was fire-and-forget
in setSession — the DB wasn't open yet when loadWithCache() tried to
read from it. Now openCacheDb is awaited inside loadInitialData.
When cache has data, loadInitialData returns early (spinner disappears
fast) and background refresh runs silently. On first visit (no cache),
spinner shows during the full server fetch as before.
- Add list virtualization with @tanstack/react-virtual for bookmark and
reading list views, reducing DOM nodes by ~97% for large collections
- Add IndexedDB cache layer (idb) for instant loads on return visits,
with per-user database isolation and graceful fallback
- Add CID-based diffing to skip unnecessary React state updates when
server data hasn't changed
- Add /api/sync-check endpoint for lightweight change detection (3 PDS
requests vs 30+ for full fetch)
- Add tab-refocus background refresh with 60-second debounce
- Add 150ms search input debounce for smoother filtering
- Wire all mutations to update IndexedDB alongside React state
- Clear cache on logout for data hygiene
- Bundle size increase: 141KB -> 146.5KB (+3.9%)
After deleting bookmarks (single or bulk), detect tags that are no longer
used by any remaining bookmark and offer to delete them via a dialog.
Also fix bulk delete partial failure bug: the frontend incorrectly assumed
successful deletes were always at the start of the URI list. The backend
now returns the actual deletedUris so the frontend correctly identifies
which bookmarks were deleted vs failed.
* feat(bulk): add bulk operations API endpoint
POST /api/bookmarks/bulk supports delete, add-tags, and remove-tags
actions. Uses applyWrites batching for deletes (10 ops per call) and
concurrent putRecord for tag updates. Returns updated bookmarks for
tag operations to enable frontend state sync without full reload.
* feat(bulk): add select mode UI with bulk actions toolbar
Select mode toggle in header (desktop + mobile), checkboxes on bookmark
cards in both card and list views, floating action toolbar with bulk
delete, add tag, and remove tag operations. Disables drag-drop and
pull-to-refresh during select mode. Escape key exits select mode.
* style: format bulk operations files
Store readingListTag in the com.kipclip.preferences PDS record alongside
dateFormat instead of the Turso database. This syncs the preference across
devices and keeps the database for secrets (Instapaper credentials) only.
The reading list tag input now instant-applies on blur, consistent with
the date format picker. The Instapaper trigger in the bookmarks route
fetches preferences in parallel with settings to get the tag value.
Two bugs: (1) loadInitialData overwrote localStorage with PDS defaults
before the migration check could read the user's saved value, so the
migration never triggered. (2) updatePreferences waited for the API
response before updating context state, but Settings uses <a href="/">
which triggers a full page reload — wiping React state before the
response arrived.
Fix: read localStorage before overwriting in loadInitialData, and
optimistically update context + localStorage in updatePreferences
before the API call.
Store the date format setting as a com.kipclip.preferences record on the
user's PDS instead of only in localStorage. This syncs the preference
across devices and survives browser data clearing. Includes a one-time
migration that pushes any existing localStorage value to PDS, and a
graceful fallback to localStorage when the user hasn't re-authenticated
with the new OAuth scope.
Move bookmark import writes from synchronous HTTP request to background
processing using Deno KV queue. This prevents Deno Deploy's 30-second
request timeout from killing large imports.
The POST /api/import endpoint now parses and deduplicates synchronously
(fast), then stores bookmark chunks in KV and enqueues background
processing. A new GET /api/import/status/:jobId endpoint lets the
frontend poll progress. The frontend shows a progress bar during import.
New files: lib/kv.ts, lib/import-queue.ts, lib/import-parsers.ts,
routes/api/import.ts, frontend/components/ImportBookmarks.tsx
Move enrichment metadata ($enriched) from bookmark records to a kipclip-specific
annotation sidecar (com.kipclip.annotation) using same-rkey binding. Add optional
user notes per bookmark. Replace hover/tap overlay with a detail modal that shows
all bookmark fields. Background migration runs on login to move existing $enriched
data to annotations. Graceful fallback for sessions without the new OAuth scope.