Projects

ProjectProductivity · 2026

Perch

A private, local-first browser extension for organizing, stashing and restoring tab groups, built with WXT and React. No account, no server, no telemetry.

  • WXT
  • React
  • TypeScript
  • Browser Storage
A new tab dashboard lists every open tab, and a stash closes them for real without losing themNew tab dashboardevery open tab, groupedstashessaved for laterCOMMAND PALETTEgrouping.tsvitest covers itby domainlanding pageslocal filesPURE MODULEBackground scripttabs · alarms · menusstorage.localthe source of truthstorage.syncquota bound, chunkedStashclosed, one click backTABSWRITESMIRRORSSTASHRESTORENO ACCOUNT, NO SERVER, NO TELEMETRYNEWER REV WINS ON LOAD

Productivity · Project

Chapter
Extensions and automation
Year
Availability
Open source
Last commit

Perch replaces the new-tab page with a dashboard of every tab I have open, grouped by domain and sorted by how busy each group is. It is a browser extension built with WXT, React and TypeScript. There is no server and no account, and the concept came from Zara’s tab-out.

The reason I kept tabs open was never that I needed them on screen. It was that closing one felt like losing it. A bookmark is a different place that I never go back to, so the tab stayed instead. Listing tabs prettily does not fix that. Making closing safe does.

# new tab: 34 tabs grouped by domain, all localHomepages (5)    GitHub (7)    localhost:5173 (3)Local Files (2)  YouTube (4)   Figma (2)stash "Research" (6 tabs) saved, tabs closedundo close reopened 1 tabduplicate flagged (2x), closed 1 duplicate12 tabs untouched for 7+ days, ready to stashsuspended 9 inactive tabs, ~380 MB freed
Perch in use

Making closing safe

A stash is a named group of tabs, saved and then closed for real. It reopens in one click, or into a new window. An undo-close stack catches the ones I did not mean to close. Saved for later is a per-tab checklist with a collapsible, searchable archive. Tabs drag onto an existing stash, onto a new-stash dropzone, or into Saved for later, and stashes drag to reorder. A right-click menu offers Stash and Save for later without opening the dashboard at all. A command palette on ⌘K, or /, fuzzy searches open tabs, saved tabs and stashes, then jumps, opens or restores by keyboard.

Grouping is the work

Everything in utils/grouping.ts is pure, with no browser API inside it, which is exactly why it is the part Vitest covers. Tabs bucket by domain and sort by count. isLandingPage pulls homepages into a single card, so an inbox tab can go without disturbing a deep link underneath it. file:// tabs collect under Local Files, and localhost tabs show their port so dev projects stay distinguishable. matchCustomGroup merges subdomains or splits a site by path, driven by CUSTOM_GROUPS in utils/config.ts beside FRIENDLY_DOMAINS and LANDING_PAGE_PATTERNS. Titles lose their notification counts and trailing suffixes, and get synthesized from the URL for repositories, issues, posts and videos.

Sync without a server

utils/syncedStore.ts is the piece I am happiest with. storage.local stays the always-available source of truth, and the same list mirrors into storage.sync so stashes follow the browser profile between a person’s own devices. No account and no backend appear anywhere in that path. storage.sync allows roughly 8 KB per item and 100 KB overall, so the list is JSON encoded and split across numbered chunk keys. New chunks are written before stale ones are pruned, so the meta record never points at a missing chunk. Both areas carry a monotonic rev, and a load takes whichever copy is newer. A write that exceeds quota is caught and skipped, and local still holds everything.

Decisions and costs

  • Make closing safe rather than make tabs pretty. Stashes are the whole answer to why tabs accumulate. They also add a second store that has to stay coherent with the live tab list.
  • Keep grouping and formatting pure. Neither utils/grouping.ts nor utils/format.ts touches a browser API, so both are unit tested directly. What is left untested is a thin layer of typed wrappers over the tabs and storage APIs.
  • Mirror into storage.sync instead of running a sync service. It costs nothing and needs no login. It is also quota bound, so a large collection quietly stays on one device, and that is the decision I would revisit.
  • Use Web Locks around every read, modify and write. Two dashboard tabs can mutate the same list at once, and an atomic mutation was cheaper than reconciling a conflict later.

Permissions and limits

The manifest asks for tabs, storage, tabGroups, alarms and contextMenus, and nothing else. alarms drives an optional hourly auto-stash of stale tabs, off by default. Perch surfaces tabs untouched for seven days and suspends inactive ones with an estimate of the memory freed. It also flags duplicate URLs for a one-click close, reflects Chrome’s native tab groups, and updates live as tabs change. There is no analytics and no telemetry, and Inter ships bundled rather than fetched. The one network call loads favicons from Google’s public favicon service. Everything exports to a local JSON file, and uninstalling removes it all. pnpm test is a typecheck plus Vitest, and the build targets Chrome and Firefox under MIT.

Next projectDesign Automation

Fill Data Figma Plugin

View project