Skip to content

zeshhaan/tab-out

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

98 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tab Out

Keep tabs on your tabs.

Tab Out replaces your Chrome new tab page with a dashboard that shows everything you have open -- grouped by domain, with landing pages (Gmail, X, LinkedIn, etc.) pulled into their own group for easy cleanup. Close tabs with a satisfying swoosh + confetti.

Built for people who open too many tabs and never close them.


Elixir port. This fork replaces the original Node.js + Express server with an Elixir application built on Bandit, Plug, and Exqlite. The SQLite schema, JSON API surface, Chrome extension, and dashboard are all unchanged — only the server binary is different. The server ships as a mix release — a self-contained directory that bundles its own Erlang runtime. Prebuilt releases for macOS (arm64) and Linux (x86_64) are attached to every GitHub Release, so installing on a fresh machine does not require Elixir at all. See Tech stack for the rest of the changes.


Install with a coding agent

Send your coding agent (Claude Code, Cursor, Windsurf, etc.) this repo and say "install this":

https://github.com/zeshhaan/tab-out

The agent will explain what Tab Out does and set everything up. Takes about 2 minutes (plus ~30 seconds the very first time for Mix to fetch and compile dependencies).


Features

  • See all your tabs at a glance -- grouped by domain on a clean grid, no more squinting at 30 tiny tab titles
  • Landing pages group -- homepages and feeds (Gmail, X, LinkedIn, GitHub, YouTube) are pulled into one card so you can close them all at once
  • Close tabs with style -- swoosh sound + confetti burst when you clean up a group. Makes tab hygiene feel rewarding
  • Duplicate detection -- flags when you have the same page open twice, with one-click cleanup
  • Click any tab to jump to it -- switches to the existing tab, even across windows
  • Save for later -- bookmark individual tabs to a checklist before closing them
  • Live cross-window sync -- defer a tab in one Chrome window and the dashboard in another window refreshes immediately, no polling (powered by Server-Sent Events over Phoenix.PubSub)
  • Tab Out dupe detection -- notices when you have extra new tab pages open and offers to close them
  • Expandable groups -- large groups show the first 8 tabs with a clickable "+N more" to reveal the rest
  • Auto-updates -- get notified when a new version is available, update with one click
  • 100% local -- your browsing data never leaves your machine. No AI, no external API calls
  • Always on -- starts automatically when you log in, runs silently in the background

Manual Setup

Two modes, pick whichever fits. scripts/install.sh auto-detects which one to use.

Mode A — Prebuilt release (no Elixir required)

Runs entirely from a GitHub Release artifact. Good for installing on a second machine, a fresh laptop, a colleague's box.

curl -sSL https://raw.githubusercontent.com/zeshhaan/tab-out/main/scripts/install.sh | bash

The script downloads the latest prebuilt tarball for your platform (macOS arm64 or Linux x86_64), unpacks it under ~/.local/share/tab-out, downloads and unpacks the Chrome extension into the same directory, writes ~/.mission-control/config.json, installs a Launch Agent / systemd unit, and starts the daemon. No git clone, no Elixir install.

After the script finishes it prints the path to the unpacked extension/ folder — load that in chrome://extensions → Developer mode → Load unpacked.

Mode B — Build from source

Good for hacking on it.

# 1. Install Elixir (build-time only)
brew install elixir            # macOS
sudo apt install elixir        # Debian / Ubuntu

# 2. Clone and run the installer
git clone https://github.com/zeshhaan/tab-out.git
cd tab-out
bash scripts/install.sh

Because the script detects both the source tree and elixir on $PATH, it runs mix deps.get + MIX_ENV=prod mix release locally and points the Launch Agent at _build/prod/rel/tab_out/bin/tab_out. First build takes ~30 seconds while Bandit, Plug, Exqlite, Phoenix.PubSub, Req, and Corsica compile; subsequent builds are a few seconds.

Load the Chrome extension from ./extension/ inside the clone (chrome://extensions → Developer mode → Load unpacked).

Either way

Open a new tab — you'll see Tab Out. The server auto-starts on future logins via the Launch Agent / systemd unit.

Manual daemon control:

tab_out start    # run in the foreground (useful for debugging)
tab_out daemon   # run in the background
tab_out stop     # stop the daemon
tab_out remote   # attach an iex shell to the live daemon

(tab_out here means the binary at _build/prod/rel/tab_out/bin/tab_out if you built from source, or ~/.local/share/tab-out/tab_out/bin/tab_out if you used the prebuilt.)


Configuration

Config lives at ~/.mission-control/config.json:

Field Default What it does
port 3456 Local port for the dashboard

The release reads this file at boot via config/runtime.exs, so you can edit it and restart without rebuilding.


How it works

You open a new tab
  -> Chrome extension loads Tab Out in an iframe
  -> Dashboard fetches /api/missions, /api/deferred, /api/stats
  -> Dashboard opens a live EventSource to /api/events
  -> You close a group, save a tab for later, or archive a mission
  -> Server broadcasts an event; other open dashboards refresh automatically

Inside the BEAM VM, the application supervisor starts five children in order:

  1. Phoenix.PubSub — in-process broadcast registry for the events topic
  2. TabOut.DB.Writer — single GenServer owning the one read/write SQLite connection; creates the schema on boot
  3. TabOut.DB.ReaderPoolNimblePool of 4 read-only Exqlite connections; read queries check one out, run, and return it
  4. TabOut.Updater — polls GitHub for a newer commit every 48h using Req
  5. Bandit — HTTP server hosting TabOut.Router

WAL mode on SQLite lets the writer and the pool's readers run concurrently without blocking each other. Every DB call and every HTTP request is wrapped in a :telemetry span, and TabOut.Telemetry logs a warning if any query exceeds 50ms.

The server runs silently in the background. It starts on login and restarts if any child crashes — BEAM's supervisor handles that without the OS even noticing.


Tech stack

What How
Server Elixir (Mix project under lib/tab_out/, shipped as mix release)
HTTP Bandit + Plug
CORS Corsica
Database Exqlite on local SQLite (same missions.db schema as upstream), with a NimblePool of read connections and a writer GenServer
Live sync Phoenix.PubSub + Server-Sent Events over a chunked Plug.Conn
Outbound HTTP Req
Instrumentation :telemetry + Plug.Telemetry, with runtime_tools bundled so bin/tab_out remote:observer.start() works against a live daemon
Concurrency OTP supervisor keeps the HTTP server, DB writer, reader pool, pubsub, and update-checker alive; restarts cleanly on crash
Extension Chrome Manifest V3
Auto-start macOS Launch Agent / Linux systemd user service
Sound Web Audio API (synthesized, no files)
Animations CSS transitions + JS confetti particles

Why Elixir instead of Node?

  • Ships as a release with its own ERTS. mix release produces a directory under _build/prod/rel/tab_out/ that contains compiled BEAM files and a bundled Erlang runtime. Copy the directory to another machine and it runs — no brew install, no package manager, no runtime compilation. You need Elixir to build the release; you do not need it to run the release.
  • BEAM supervision. A crash in one request handler doesn't tear down the VM. The supervisor restarts the failing child in milliseconds, which is nicer than "launchd notices the process died, restarts Node cold."
  • Cheap in-process pubsub. Phoenix.PubSub lets us broadcast an event on every mutation and serve Server-Sent Events to connected dashboards with zero extra infrastructure. That's what powers the live cross-window sync — no polling, no WebSockets, no Redis.
  • Concurrency that matches SQLite's model. A writer GenServer + a NimblePool of read connections maps one-to-one onto SQLite's "single writer, many readers under WAL" semantics.
  • Compatible on-disk format. Same ~/.mission-control/missions.db schema, same JSON shapes, same config file. You can switch back to the upstream Node version without losing data.

Poking around at runtime

Once the release is running, you can open a remote iex shell against the live daemon:

_build/prod/rel/tab_out/bin/tab_out remote

From there:

# Full BEAM process inspector — every GenServer mailbox, ETS table,
# message queue. Tab Out bundles runtime_tools for exactly this.
:observer.start()

# Peek at telemetry handlers
:telemetry.list_handlers([:tab_out, :db, :query, :stop])

# Hit the DB directly
TabOut.DB.query("SELECT COUNT(*) FROM deferred_tabs WHERE archived = 0")

Exit with Ctrl+G, q — the daemon keeps running.


License

MIT


Built by Zara; Elixir port by zeshhaan.

About

Elixir port (Bandit + Plug + Exqlite). Keep tabs on your tabs — turn your New Tab page into a mission control, so you can close them easily. Built for people who open too many tabs and never close them.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • JavaScript 49.3%
  • Elixir 24.5%
  • CSS 14.9%
  • Shell 5.7%
  • HTML 5.6%