Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

jobs:
unit:
name: Unit tests (node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: make check
- run: make test

e2e:
name: Browser e2e (Chrome for Testing)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install Chrome for Testing into the holodeck
id: chrome
run: |
mkdir -p "$HOME/.holodeck/browsers"
out=$(npx --yes @puppeteer/browsers install chrome@stable --path "$HOME/.holodeck/browsers")
echo "$out"
echo "path=${out#* }" >> "$GITHUB_OUTPUT"
- name: Run e2e
env:
BROWSER: ${{ steps.chrome.outputs.path }}
run: make e2e

package:
name: Package zip
runs-on: ubuntu-latest
needs: [unit, e2e]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: make package
- uses: actions/upload-artifact@v4
with:
name: organize-tabs-zip
path: organize_tabs-*.zip
if-no-files-found: error
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
*.zip
node_modules/
60 changes: 60 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Organize Tabs: notes for AI agents and contributors

## What this is

A Chrome extension (Manifest V3) that groups, sorts, deduplicates and tidies tabs. It is the maintainer's daily driver, used across many machines and browser profiles. Keep it that way: vanilla JavaScript, no build step, no runtime dependencies.

## Layout

```
manifest.json
src/background.js service worker: message router, context menus, shortcuts, auto-dedupe, badge
src/lib/url.js pure URL logic: normalization, entity rules, duplicate detection, sort order (tested)
src/lib/settings.js chrome.storage.sync persistence, export/import, remote rules
src/lib/actions.js the action registry and everything that touches the tabs/windows/bookmarks APIs
src/popup.* toolbar popup (sends messages, runs nothing itself)
src/options.* settings page
src/ui.css shared design tokens, light and dark
test/url.test.js node --test suite for src/lib/url.js
scripts/e2e.mjs headless browser test over the DevTools protocol
scripts/dev.mjs launches the holodeck dev profile with sample tabs
scripts/screenshots.mjs renders README images and 1280x800 store screenshots
STORE_LISTING.md Chrome Web Store copy, permission justifications, privacy answers
```

## Rules of the road

- **All tab operations run in the service worker.** The popup only sends `chrome.runtime.sendMessage` and renders results. Chrome closes the popup the moment a window is created or focused, which kills any logic running there.
- **Never unpin or reorder a pinned tab.** Sort, group, split and the cleanup actions skip them entirely. The two actions that move pinned tabs across windows on purpose (Consolidate All, Pull Media) re-pin them at the front of the destination immediately, because Chrome unpins on move.
- **Never break a tab group.** Sort keeps groups contiguous. Cross-window moves go through `moveTabsPreservingGroups` in `src/lib/actions.js`, which moves whole groups, merges into a same-named group in the destination, and rebuilds partial groups.
- **Windows holding a meeting or playing media are primary.** Consolidate and Split go through `withoutProtectedWindows`; new window-rearranging actions should too. `isMediaTab` in `src/lib/url.js` defines media (audible, or a URL matching the user's media patterns).
- **Anything that closes tabs with content goes through `closeTabs`** so it lands in the undo stack, and exposes a `preview` so the popup can show the list first. Blank new-tab pages are the one exception: they carry nothing to restore. Tabs the extension itself opens (Undo, Restore) are registered with `markOwnTab` so auto-dedupe leaves them alone.
- **Pure logic lives in `src/lib/url.js`** and gets a unit test. New duplicate rules go in `DEFAULT_RULES` with a case in `test/url.test.js` showing the URL variants that must collapse and one that must not.
- Keep the existing code style (`.prettierrc`: 4 spaces, single quotes, semicolons).

## Testing

```sh
make test # unit tests, no browser needed
make e2e # every action in a real headless browser
make dev # interactive: the holodeck
```

### The holodeck

Browser testing never touches a real profile. Automated and interactive runs use the **holodeck**, a throwaway sandbox at `~/.holodeck/` (override with `HOLODECK=`):

- `make dev` opens Brave (or `BROWSER=/path/to/chromium make dev`) on `~/.holodeck/brave`, named "🧪 holodeck" in the profile chip, with this folder loaded as an unpacked extension and two windows of sample tabs that hit every rule. The profile persists between runs. `make dev-clean` deletes it.
- `make e2e` creates a fresh profile under `~/.holodeck/tmp/` and removes it when the run ends.
- Google Chrome's branded build ignores `--load-extension`, so the scripts default to Brave. Loading unpacked through `chrome://extensions` works in any browser.

Agents may launch, inspect and destroy anything under `~/.holodeck/` without asking.

CI (`.github/workflows/ci.yml`) runs `make check`, `make test` on Node 22 and 24, and `make e2e` on Ubuntu with Chrome for Testing from `@puppeteer/browsers`. When `CI` is set the e2e script adds `--no-sandbox` and friends for containers.

## Releasing

1. Bump `version` in `manifest.json` and `package.json`. The manifest `description` is the store summary and must stay at or under 132 characters.
2. `make screenshots` if the UI changed, and update `STORE_LISTING.md` if actions or permissions changed.
3. Merge, tag `vX.Y.Z`, create a GitHub Release with the CI zip attached.
4. Upload the zip to the Chrome Web Store dashboard and paste from `STORE_LISTING.md`.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
31 changes: 28 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
VERSION := $(shell jq < manifest.json .version -r)
.PHONY: help test check e2e dev dev-clean screenshots clean package

## help - Display help about make targets for this Makefile
help:
@cat Makefile | grep '^## ' --color=never | cut -c4- | sed -e "`printf 's/ - /\t- /;'`" | column -s "`printf '\t'`" -t

## test - run the unit tests
test:
node --test

## check - syntax-check every script
check:
@for f in src/*.js src/lib/*.js; do node --check $$f && echo "ok $$f"; done

## clean - remove all build artifacts
clean:
rm -f *.zip

## package - creates a new package
package: clean
7z a organize_tabs-${VERSION}.zip `cat FILES`
## package - build organize_tabs-<version>.zip for the Chrome Web Store (or Load unpacked after unzipping)
package: clean test
zip -r -X organize_tabs-${VERSION}.zip `cat FILES` -x '*.DS_Store'

## e2e - run the browser end-to-end test (needs Brave or Chromium)
e2e:
node scripts/e2e.mjs

## dev - launch the holodeck (throwaway browser profile at ~/.holodeck) with the extension and sample tabs
dev:
node scripts/dev.mjs

## dev-clean - end program: delete the holodeck browser profiles
dev-clean:
rm -rf "$${HOLODECK:-$$HOME/.holodeck}"/brave "$${HOLODECK:-$$HOME/.holodeck}"/chromium "$${HOLODECK:-$$HOME/.holodeck}"/tmp

## screenshots - render README and Chrome Web Store screenshots from the holodeck
screenshots:
node scripts/screenshots.mjs
158 changes: 136 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,158 @@
# Organize Tabs Google Chrome Extension
# Organize Tabs

This plugin enables the following operations:
A Chrome extension for people who keep a lot of tabs open. It groups, sorts, deduplicates and tidies tabs across every window, and it understands that a pull request, a ticket, a profile or a document is the same page even when the URL differs.

- **Collate Tabs** - Groups all tabs into separate windows, grouped by hostname, and sorted by URL. Ignores windows with pinned tabs.
- **Consolidate Tabs** - Combines all tabs across all windows into one window, sorted by URL. Ignores windows with pinned tabs.
- **Deduplicate Tabs** - Enumerates through all tabs, and keeps only the first copy of a tab with a unique URL, and closes the remaining tabs with duplicate URLs. Recommend running this *after* running **Collate Tabs**.
- **Sort Tabs in Window** - Sorts tabs in current window by URL.
- **Close All Tabs from this Domain** - Close all tabs opened to the same domain/host as the current tab.
- **Close Blank Tabs** - Closes all blank tabs.
- **Bring All Windows To Front** - Focuses all windows in session and tiles them. Useful for rediscovering misplaced windows.
Vanilla JavaScript, no build step, no dependencies. Works in Chrome, Brave, Edge and other Chromium browsers (Chrome 121 or newer).

Available on the Google Chrome Webstore: <https://chrome.google.com/webstore/detail/organize-tabs/ebnlpacdgjnofakgfgbildmjdhbibnpa>
Chrome Web Store: <https://chrome.google.com/webstore/detail/organize-tabs/ebnlpacdgjnofakgfgbildmjdhbibnpa>

## Screenshots
<p>
<img src="promo/popup.png" width="300" alt="Popup" />
<img src="promo/preview.png" width="300" alt="Preview before closing duplicates" />
<img src="promo/popup-help.png" width="300" alt="Popup with inline help" />
</p>

![image](https://github.com/hacktoolkit/organize-tabs-chrome-extension/raw/master/promo/screenshot1.png)
## Actions

## Development and Testing
Every action is available from the toolbar popup, the right-click context menu, and (optionally) a keyboard shortcut. Actions that close tabs show a preview first, and the last close can always be undone.

1. Clone this repository locally.
1. In Google Chrome, go to `Manage Extensions` (<chrome://extensions/>).
1. Enable `Developer mode`.
1. Press `Load unpacked` and navigate to the folder with `manifest.json`, and `Select`.
1. Voilà!
### Organize

| Action | What it does |
| --- | --- |
| **Group Tabs by Domain** | Puts tabs from the same site into a tab group, in every window. Pinned tabs and existing groups are left alone. |
| **Sort Tabs in Window** | Sorts the current window by site and URL. Pinned tabs stay put, tab groups stay together and are sorted by name. |
| **Ungroup Tabs in Window** | Removes all tab groups in the current window. |
| **Consolidate All Tabs** | Moves every tab, pinned ones included, into the current window and sorts it. Tab groups come along, and a group joins an existing group with the same name. |
| **Consolidate Unpinned Tabs** | Same, but pinned tabs stay in their windows. |
| **Split Windows by Domain** | One window per site, plus one window for the sites with a single tab. Tab groups are kept. The old "Collate" action. |

### Clean up

| Action | What it does |
| --- | --- |
| **Deduplicate Tabs** | Closes tabs that point at the same thing, using the duplicate rules below. Keeps the pinned, active, or most recently viewed copy. |
| **Close Tabs Like This One** | Closes tabs from the same domain, the same section of the site, or the same kind of page (all pull requests, all LinkedIn profiles). |
| **Close Stale Tabs** | Closes tabs you have not looked at for N days. Pinned, active and playing tabs are skipped. |
| **Free Memory** | Unloads stale tabs from memory without closing them. |
| **Close Blank Tabs** | Closes empty new-tab pages everywhere. |
| **Park Window to Bookmarks** | Saves the current window's tabs into a dated bookmark folder and closes them. |
| **Restore Last Parked** | Reopens the most recently parked folder as a tab group. |

### Windows

| Action | What it does |
| --- | --- |
| **Pull Meetings & Media to a Window** | Moves every tab that is playing audio or showing a meeting or video player into its own window and focuses the meeting. |
| **Bring All Windows to Front** | Focuses and cascades every window so misplaced ones reappear. |
| **Undo Last Close** | Reopens whatever the last action closed. |

## Duplicate rules

Two URLs are duplicates when they produce the same **key**. Without a rule, the key is the URL with the scheme, `www.`, fragment, trailing slash, tracking parameters and query-parameter order normalized away. With a rule, many URLs collapse to one key:

```json
{
"name": "GitHub pull request",
"match": "^https?://github\\.com/([^/]+)/([^/]+)/pull/(\\d+)",
"key": "github:$1/$2/pull/$3"
}
```

That rule makes `/pull/123`, `/pull/123/files`, `/pull/123/commits/abc` and `/pull/123#discussion_r1` the same tab. Built-in rules cover GitHub, Bitbucket, GitLab, Jira, Confluence, LinkedIn, YouTube, Google Docs and Drive, Notion, HubSpot, Slack, Figma, Amazon, Stack Overflow, Reddit and X. Add your own in Settings, where a tester shows which rule a URL hits.

The same rules power **Close Tabs Like This One**, so from any pull request you can close every open pull request with one click.

**Auto-deduplicate** (off by default) watches new tabs. When you open a link that is already open, the new tab is closed and the existing one focused.

## Settings, sync and backup

Open Settings from the popup or from `chrome://extensions`.

- Preferences and rules are stored in `chrome.storage.sync`, so they follow a signed-in Chrome or Brave profile across machines.
- **Export** downloads a JSON file with everything. **Import** reads it back, on any profile.
- **Remote rules file**: point at a JSON file you host (a raw file in a dotfiles repo works well). Its rules are merged in after your local ones and refreshed on a schedule.

A remote rules file is either a full export or just `{"rules": [...]}`.

## Keyboard shortcuts

Defaults: `Alt+Shift+O` opens the popup, `Alt+Shift+D` deduplicates, `Alt+Shift+S` sorts the window, `Alt+Shift+G` groups by domain. Every other action can be bound at `chrome://extensions/shortcuts`.

## Meetings and media

Being on a call while tidying up is the common case, so a window holding a meeting or a playing tab is treated as primary: **Consolidate** and **Split** leave it alone (a setting, on by default). A tab counts as media when it is audible or on a meeting or player page: Google Meet, Zoom, Teams, Whereby, Around, Webex, Gather, Discord, Slack huddles, YouTube, YouTube Music, Twitch, Vimeo, Spotify, Netflix and Loom by default, editable in Settings. **Pull Meetings & Media to a Window** gathers all of them into one window, which is a good first click when a call starts.

## Tab groups and pinned tabs

No action ever moves a pinned tab out of its place, and no action breaks a tab group:

- **Sort** keeps groups contiguous and sorts them by name, with members sorted inside.
- **Consolidate** and **Split** move whole groups between windows. When the destination already has a group with the same name, the incoming tabs join it. Untitled groups keep their colour and members.
- **Group by Domain** adds tabs to an existing group with the matching name instead of creating a second one.

## Development

```sh
git clone git@github.com:hacktoolkit/organize-tabs-chrome-extension.git
cd organize-tabs-chrome-extension
make test # unit tests for the URL and rule logic (node --test)
make e2e # headless browser test of every action (needs Brave or Chromium)
make dev # open a throwaway browser profile with the extension and sample tabs
```

CI runs both suites on every push and pull request (`.github/workflows/ci.yml`): unit tests on Node 22 and 24, and the e2e suite on Ubuntu with Chrome for Testing installed via `@puppeteer/browsers`. To use that same browser locally instead of Brave:

```sh
npx --yes @puppeteer/browsers install chrome@stable --path ~/.holodeck/browsers
BROWSER="<path printed above>" make e2e
```

`make dev` launches the **holodeck**: Brave (or `BROWSER=/path/to/chromium make dev`) on a throwaway profile at `~/.holodeck/brave`, named "🧪 holodeck" in the profile chip, with the extension loaded from this folder and two windows of sample tabs that exercise every rule. The profile persists between runs so settings, parked bookmarks and shortcuts survive; `make dev-clean` deletes it. `make e2e` uses a fresh profile under `~/.holodeck/tmp` that is removed when the run ends. Nothing under `~/.holodeck` is real, so humans and AI agents can trash it freely. After editing source, reload the extension at `chrome://extensions`.

To use it in your real profile: `chrome://extensions`, enable Developer mode, **Load unpacked**, pick the folder with `manifest.json`. Google Chrome's branded build no longer accepts `--load-extension` on the command line, which is why the scripts default to Brave; loading unpacked through the UI works in every browser.

Layout:

```
manifest.json
src/background.js service worker: message router, context menus, shortcuts, auto-dedupe, badge
src/lib/url.js pure URL logic: normalization, rules, duplicate detection, sorting (tested)
src/lib/settings.js storage.sync persistence, export/import, remote rules
src/lib/actions.js the action registry and everything that touches the tabs API
src/popup.* toolbar popup
src/options.* settings page
test/ node --test suites
```

All tab operations run in the service worker. The popup only sends messages, because Chrome closes the popup as soon as a new window is created or focused.

## Privacy

Organize Tabs does not collect, store remotely, or transmit any data. Tab URLs and titles are read and processed only inside your browser to find duplicates, sort and group. Preferences and rules are saved in your browser profile's own storage. The only network request the extension can make is to fetch a rules file at a URL you configure yourself, and that is off by default.

## Building and publishing

See: https://developer.chrome.com/webstore/publish
```sh
make package # organize_tabs-<version>.zip
```

`STORE_LISTING.md` has the store description, permission justifications and privacy answers to paste into the dashboard, and `make screenshots` regenerates the README images and the 1280x800 store screenshots in `promo/store/` from the holodeck.

CI also attaches the zip to every run as the `organize-tabs-zip` artifact (Actions tab, pick a run, Artifacts). To try a build on another machine without cloning: download it, unzip, then `chrome://extensions` → Developer mode → **Load unpacked** → pick the unzipped folder.

See <https://developer.chrome.com/webstore/publish>.

## Contributing

Contributions are welcome. Please fork the repository and submit a pull request from your local branch.
Contributions are welcome, especially new duplicate rules with tests in `test/url.test.js`. Fork the repository and open a pull request.

## Support

Check out <a href="https://github.com/hacktoolkit/chrome-extensions">other Chrome extensions</a> made by Hacktoolkit!
Check out <a href="https://github.com/hacktoolkit/chrome-extensions">other Chrome extensions</a> made by Hacktoolkit.

## License

MIT. See LICENSE.md.

Logo: Derived from `window-restore` [icon](https://fontawesome.com/icons/window-restore?style=regular) from Font Awesome (License: <https://fontawesome.com/license>).
Logo: derived from the `window-restore` [icon](https://fontawesome.com/icons/window-restore?style=regular) from Font Awesome (License: <https://fontawesome.com/license>).
Loading
Loading