nufftabs is a minimal Chrome (MV3) extension to condense all tabs from the current window into a saved list, then restore them later. It uses WXT for build/dev and stores saved tabs and settings in chrome.storage.local.
- Condense tabs from the current window (optionally excluding pinned tabs).
- List UI grouped by condense action, with per-group restore all/delete all plus per-tab restore/delete.
- Dynamic search in the fixed top bar filters tabs by title/URL.
- Drag-and-drop between groups to move a saved tab.
- Index-first lazy loading: group keys load first, group payloads load on demand.
- Export/import JSON (append or replace), import from file, and OneTab import.
- Restore rules: single restore uses the current window; restore all opens new windows per chunk, reusing the list window only when it is the sole tab.
- Safety guardrails: condense and restore mutate storage only after a successful verification step.
- List tab is pinned and reused if it already exists.
- Settings for “Exclude pinned tabs,” “Tabs per restore window,” duplicate handling, and optional memory-saving restore.
- Optional manual Google Drive backup/restore from the options page.
- Triggered by clicking the extension action icon.
- Reads settings (
excludePinned,restoreBatchSize,discardRestoredTabs,duplicateTabsPolicy) fromchrome.storage.local. - Skips browser-internal URLs (
chrome://,chrome-extension://,chrome-search://,chrome-untrusted://,devtools://,about:) so only user-content tabs are condensed. - Saves eligible tabs (URL + title + timestamp) to a new group under
savedTabs:<groupKey>and updatessavedTabsIndex. - Verifies the saved group can be read back with matching tab entries.
- Closes eligible tabs only after verification succeeds.
- Focuses an existing nufftabs list tab if one exists anywhere (most recently active), or creates a new one if none exist. The list tab is pinned.
- Unlisted page at
/nufftabs.html. - Reads
savedTabsIndexfirst, then loads group payloads on demand. - Renders saved tab groups from
chrome.storage.localand keeps the header count based on total saved tabs. - Filters visible rows and groups from the top app-bar search input.
- Refreshes on storage changes and when the tab becomes visible.
To keep the list UI responsive with large tab counts, the code makes a few deliberate tradeoffs. These are documented in code comments, but summarized here for maintainers:
- Group diff heuristic: group changes are detected by comparing the first, middle, and last tab IDs instead of a full deep comparison. This avoids O(n) checks but can miss reorders or mid-list edits, resulting in a skipped re-render.
- Incremental list rendering: only the first
RENDER_PAGE_SIZEitems render initially. Remaining tabs require a "Load more" click to render additional chunks. This bounds DOM size but means not all rows are immediately visible. - Index-first group loading: the page loads
savedTabsIndexfirst, then fetches eachsavedTabs:<groupKey>payload as needed (viewport/search/expand). This keeps first paint responsive, but initial renders can show loading placeholders for off-screen groups. - Event delegation: a single click handler on the list container routes actions via
data-action. This reduces per-row listeners, but action handling depends on markup and attributes staying in sync. - Concurrency-limited restore: tab creation runs in parallel batches for speed. This can relax strict creation order compared to fully sequential creation.
- Shallow group cloning:
cloneGroups()only shallow-copies the groups map. Callers must replace arrays rather than mutating them in place to avoid accidental shared state.
- Group key = window ID + timestamp + nonce. Each condense creates a fresh group key like
${windowId}-${epochMs}-${uuid}(orunknown-...when window ID is unavailable), so repeated condenses never append to earlier groups. - Created-at ordering. Group "Created" timestamps use the earliest
savedAtin the group. Imports (including OneTab) stampsavedAtwith "now," which can make imported groups appear newest. - Restore order. Concurrency-limited restore favors throughput; tab ordering can differ slightly from list order for large restores.
- Paged rendering. Large groups only render the first
RENDER_PAGE_SIZEitems until the user clicks "Load more," which can look like missing tabs. - Search semantics. Search is case-insensitive substring matching over title + URL. It is intentionally not fuzzy, tokenized, or regex-based.
- Local settings. Settings are stored in
chrome.storage.local, not sync, so they do not follow the user across machines. - List tab reuse. Condense may focus an existing list tab in another window and pins it, which can feel surprising if multiple windows are open.
- Storage schema: saved tabs are stored per group under
savedTabs:<groupKey>with asavedTabsIndexarray listing active group keys. This avoids full-blob rewrites. - Data shapes:
SavedTabrequires a UUIDid, non-emptyurl,title, andsavedAtepoch ms. Settings are{ excludePinned, restoreBatchSize, discardRestoredTabs, duplicateTabsPolicy, theme }. - Restore chunking:
restoreBatchSizecontrols how many tabs open per window during "Restore all" (one window per chunk, after any reused list window). - Permissions:
tabs,storage, andidentityare used. - Host permissions:
https://www.googleapis.com/is used only for optional manual Drive backup/restore. - WXT output: dev builds live in
.output/chrome-mv3-dev/and prod builds in.output/chrome-mv3/.
- Restore single: always opens the tab in the current window (the window that contains the list tab) and keeps the list tab open and pinned.
- Restore all: opens new window(s) per restore chunk. If the list tab is the only tab in its window, the first chunk opens there and remaining chunks open in new windows (list tab remains open and active).
- Save memory on restore: when enabled, restored tabs are discarded after their URLs are set (best-effort) and will load when clicked.
- Node.js
- pnpm
pnpm installpnpm devpnpm buildpnpm packageThe packaged extension zip is generated under .output/.
- Run
pnpm dev(orpnpm build). - Open
chrome://extensions. - Enable Developer mode (top right).
- Click Load unpacked.
- Select the build output directory:
- Dev:
.output/chrome-mv3-dev/ - Build:
.output/chrome-mv3/
- Dev:
- Open several tabs in a window.
- Click the nufftabs action icon.
- Eligible tabs are saved and closed.
- The list tab is focused (existing or newly created).
- On the list page, click the restore icon for a tab.
- The tab opens in the current window.
- nufftabs verifies the restore succeeded.
- The list item is removed from storage only after verification.
- Type in the search input in the fixed top bar.
- Matching is case-insensitive and checks both tab title and URL.
- Group cards with no matches are hidden.
- Matching groups show filtered counts (
x of y tabs) and preserve all row/group actions.
- Group index (
savedTabsIndex) is read first. - Group payloads are loaded when they are near the viewport, expanded, or needed by search.
- Within each visible group, rows are rendered in pages (
RENDER_PAGE_SIZE) with Load more. - Header tab count always reflects total saved tabs, not just loaded groups.
- Click Restore all on a group card.
- If the list tab is not the only tab in the window, a new window opens for each restore chunk.
- If the list tab is the only tab, the first chunk restores in the same window, and the list tab stays open.
- The saved group is removed from storage only after restore verification succeeds.
- Click Delete all on a group card.
- The selected group is removed.
- Click Merge duplicates in the sticky top bar.
- Confirm the prompt to remove duplicates globally across all groups.
- nufftabs keeps the newest saved instance of each URL and removes older duplicates.
- Click the Export/Import control to open the panel.
- Export populates the JSON textarea, copies to clipboard (if allowed), and downloads a backup file.
- Import appends the parsed tabs to existing groups.
- Import (replace) reads the textarea and replaces the saved list if valid.
- Import file reads a JSON file and appends the parsed tabs.
- Existing groups keep their current collapse/expand state after import; newly added groups follow the current global mode (collapsed if Collapse all is active, otherwise expanded).
- In OneTab, open “Export / Import URLs” and copy the text.
- Paste it into the nufftabs Import panel textarea.
- Click Import OneTab to append those tabs to the current list.
- Only
http,https, andfileURLs are imported. Other schemes (for examplechrome://) are skipped.
- Open the options page (Extension details ? “Extension options”).
- Toggle Exclude pinned tabs.
- When enabled, pinned tabs are not saved or closed during condense.
- Open the options page.
- Set Tabs per restore window (leave blank to use the default of 100).
- During Restore all, each window opens up to that many tabs.
- Open the options page.
- Set Save memory when restoring tabs to Enabled.
- Restored tabs are unloaded after their URLs are set and will load when clicked.
- If you turn the setting off, no discard scheduling runs (pending discards are skipped).
- Open the options page.
- Set Duplicates to Allow duplicates or Silently reject duplicates.
- When reject mode is enabled, condense and import flows skip URLs already saved in nufftabs.
- During condense in reject mode, skipped duplicate tabs are left open in the source window.
- If a list tab already exists anywhere, condense focuses the most recently active one.
- If none exists, a new list tab is created and pinned.
- Open the options page.
- Click Connect to Google Drive and approve OAuth access.
- The same button updates in place to show connection progress/state and acts as Disconnect when already connected.
- Set Retention (how many backups to keep).
- Click Backup now to upload a snapshot of saved tabs + settings.
- Use Restore on any listed backup row to overwrite local data from that backup.
If you see bad client id or auth failures in dev, your unpacked extension ID likely does
not match the OAuth client's configured Chrome Extension ID.
- Set
CHROME_EXTENSION_KEY(orEXTENSION_MANIFEST_KEY) to a fixed extension private key. - Set
GOOGLE_OAUTH_CLIENT_IDto the OAuth client tied to that extension ID. - Restart
pnpm dev(or rebuild + reload extension).
wxt.config.ts uses these env vars so your local extension ID remains stable and matches OAuth.
entrypoints/background/index.ts— action handler, condense logic, list tab focus/pin.entrypoints/nufftabs/— list UI (index.html,index.ts,style.css).entrypoints/options/— settings UI for theme, exclude pinned tabs, restore batch size, duplicate handling, memory-saving restore, and Drive backup actions.entrypoints/drive/— Drive auth helpers, REST client, and backup orchestration logic.entrypoints/drive-auth/— standalone auth page logic kept for direct-entry/debug flows.entrypoints/ui/notifications.ts— shared user-notification adapters used by UI pages (snackbar + inline status text).public/icon/— PNG icons (16/19/32/38/48/96/128).wxt.config.ts— manifest config and permissions.
tabs: required to query, create, update, close, and discard tabs/windows.storage: required to persistsavedTabsIndex,savedTabs:<groupKey>, and settings inchrome.storage.local.identity: required to acquire OAuth tokens for optional Google Drive backup actions.https://www.googleapis.com/host permission: required to call Google Drive REST APIs for manual backup/restore.
- List doesn’t update after condense: reload the list tab or check the service worker console for errors.
- Condense closes tabs but list is empty: check
chrome.storage.localin DevTools and ensure the list tab is open. - No action when clicking icon: open
chrome://extensions, click “service worker” for nufftabs, and check logs. - Google Drive auth says
bad client id: ensureGOOGLE_OAUTH_CLIENT_IDis for a Chrome Extension OAuth client whose extension ID matches the one generated fromCHROME_EXTENSION_KEY.
Why does condense focus a list tab in another window?
If any list tab exists, nufftabs reuses the most recently active one instead of creating duplicates.
Where is data stored?
In chrome.storage.local under savedTabsIndex, savedTabs:<groupKey>, and settings.
Why are pinned tabs excluded by default?
It’s a safety default so pinned tabs are not closed unless you turn the setting off.
MIT. See LICENSE.