Summary
The tracking infrastructure for recently viewed items already exists but is never shown to the user. This issue is about surfacing that data in the main vault list as a "Recently Viewed" section at the top, reducing the number of taps/clicks needed to reach frequently accessed items — which is the primary friction point in a copy-paste workflow without browser autofill.
What already exists
apps/web/src/lib/recentlyViewed.ts — pushRecentlyViewed(vaultId, itemId), getRecentlyViewed(vaultId): string[], clearRecentlyViewed(vaultId). Stores up to 10 item IDs per vault in localStorage under the key bp:recent:<vaultId>.
apps/web/src/hooks/useRecentlyViewed.ts — calls pushRecentlyViewed when an item is viewed; already called from $itemId.tsx.
getRecentlyViewed is never called by any UI component — only by tests.
Desired behaviour
- When the vault list is open and has items, show a "Recently Viewed" section above the main item list (or as a pinned top section within the list).
- The section shows the last N items (suggested: 5) the user opened in the current vault, in reverse-chronological order (most recent first).
- Items that no longer exist (deleted, moved) are silently excluded.
- If there are no recently viewed items yet (fresh vault, cleared state), the section is hidden — no empty state needed.
- Tapping an item in the section navigates to the item detail, same as the regular list.
- The section should be visually distinct but not dominant — a small label ("Recently Viewed") and standard
ItemCard rows is sufficient.
Scope notes
- Cross-vault "All Vaults" view: recently viewed could optionally show items across all vaults. Keep this out of scope for the initial implementation; per-vault is sufficient.
- "All Vaults" aggregate view: the existing
bp:recent:<vaultId> key is per-vault. The aggregate view may show a merged list in a follow-up.
- Persistence:
localStorage is intentional (survives page reloads, cleared on session.clear()/logout via existing clear calls). No server changes required.
- IndexedDB vs localStorage: the current implementation uses
localStorage. This is acceptable; no migration needed unless a future change requires it.
Implementation notes
- Read recently viewed IDs via
getRecentlyViewed(activeVaultId) in the vault list component.
- Resolve IDs against the already-loaded
items array from useVaultItems() — no extra network call.
- Filter out IDs that are no longer in the items array (deleted items).
- Limit display to 5 items maximum.
- The
clearRecentlyViewed call on session.lock() / session.clear() already exists; verify it covers the "lock screen" path so recent history does not persist after lock.
Acceptance criteria
Summary
The tracking infrastructure for recently viewed items already exists but is never shown to the user. This issue is about surfacing that data in the main vault list as a "Recently Viewed" section at the top, reducing the number of taps/clicks needed to reach frequently accessed items — which is the primary friction point in a copy-paste workflow without browser autofill.
What already exists
apps/web/src/lib/recentlyViewed.ts—pushRecentlyViewed(vaultId, itemId),getRecentlyViewed(vaultId): string[],clearRecentlyViewed(vaultId). Stores up to 10 item IDs per vault inlocalStorageunder the keybp:recent:<vaultId>.apps/web/src/hooks/useRecentlyViewed.ts— callspushRecentlyViewedwhen an item is viewed; already called from$itemId.tsx.getRecentlyViewedis never called by any UI component — only by tests.Desired behaviour
ItemCardrows is sufficient.Scope notes
bp:recent:<vaultId>key is per-vault. The aggregate view may show a merged list in a follow-up.localStorageis intentional (survives page reloads, cleared onsession.clear()/logout via existing clear calls). No server changes required.localStorage. This is acceptable; no migration needed unless a future change requires it.Implementation notes
getRecentlyViewed(activeVaultId)in the vault list component.itemsarray fromuseVaultItems()— no extra network call.clearRecentlyViewedcall onsession.lock()/session.clear()already exists; verify it covers the "lock screen" path so recent history does not persist after lock.Acceptance criteria