No description
  • Python 57%
  • JavaScript 43%
Find a file
2026-10-22 12:16:43 +02:00
bin tg main / open-main: links from outside go into one main window (0.4.0) 2026-10-22 12:16:43 +02:00
extension tg main / open-main: links from outside go into one main window (0.4.0) 2026-10-22 12:16:43 +02:00
tests tg route: CI runs and commits take over the repo's PR tab; browser-open hook (0.3.0) 2026-10-12 18:01:26 +02:00
.gitignore tg pack/policy: packed, force-installed extension; new signing key 2026-10-12 16:40:27 +02:00
README.md tg main / open-main: links from outside go into one main window (0.4.0) 2026-10-22 12:16:43 +02:00

tg — one Chrome tab group per project

tg makes Chrome follow your project the way an editor or terminal does: every project (a wd workdir, say LPS-123) gets a tab group of that name, and switching projects expands that group and collapses the others. All tabs of a ticket live inside its group; the top level stays small.

tg focus [LPS-123]               bring the group to the front with its last used tab
                                 (created if missing), collapse the rest; NAME defaults
                                 to the project of $PWD
tg open -a LPS-123 URL...        open urls in the group, reusing tabs that show them
tg adopt LPS-123                 move the active tab into the group
tg close LPS-123                 close the group's tabs
tg route URL                     show / take over the tab the url belongs to (browser-open
                                 calls this first); exit 3: no such tab
tg main [--show|--unset]         make the last focused window the main one
tg open-main URL                 new tab in the main window, shown (browser-open uses
                                 this when no tab has the url yet)
tg tabs [NAME]                   the group's tabs (NAME defaults to the project of $PWD)
tg go [-n NAME] [QUERY...]       select the tab matching the words (title or url);
                                 several matches: pick with fzf
tg groups | tg list [--json]     what is where
tg status                        is the extension connected?

Under niri focus and open -a also raise the Chrome window (--no-raise to switch quietly, e.g. while another window should keep focus).

Why an extension

Tab groups are only reachable through the extension APIs (chrome.tabGroups); Chrome ignores --remote-debugging-port on the default profile, and CDP has no tab groups anyway. So three small parts, all in this repo:

tg  --unix socket-->  tg-host  --native messaging-->  extension/
  • extension/ — MV3 extension doing the work (tabs, tabGroups, nativeMessaging). Its manifest.json pins a key, so its id is stable: odkcmdcpmkiifoakjbjdaomboncmhgga.
  • bin/tg-host — native messaging host, started by Chrome; relays between the socket $XDG_RUNTIME_DIR/tg.sock and the extension.
  • bin/tg — the command line tool. Python 3 stdlib only; no dependencies.

open reuses a tab that shows the same page anywhere, or a subpage of it inside the group (/pull/1/files for /pull/1, ?focusedCommentId=… for a ticket) rather than opening a duplicate; reload the extension after updating it.

Which tab a url belongs to is decided in extension/match.js (tests: node --test tests/match.test.mjs): the same page or a subpage of it (GitHub PR/issue/run/commit by repo and number, Jira by ticket key) is reused; a CI run or commit URL navigates the most recently used PR/run tab of the same repo (history keeps the PR one Back away); other PRs and issues are peers and open their own tab.

A group is identified by its title; renaming one by hand detaches it from its project. Chrome syncs tab groups, so ticket names show up on your other devices too.

Install

Chrome 138+ disables unpacked extensions whenever developer mode is off, so the lasting install is a packed, force-installed one (works on a managed profile as long as no org policy blocks it):

  1. tg install writes the native messaging manifest (~/.config/google-chrome/NativeMessagingHosts/tg_host.json, pointing at bin/tg-host).
  2. tg pack builds dist/tg.crx and dist/update.xml, signed with the private key from gopass (tg/extension-key; its public half is the key in manifest.json, so the extension id odkcmdcpmkiifoakjbjdaomboncmhgga is fixed).
  3. tg policy > /tmp/tg.json; sudo install -Dm644 /tmp/tg.json /etc/opt/chrome/policies/managed/tg.json force-installs it and keeps it updated from that update manifest (a file:// URL, so it must be the machine's final clone path). It uses ExtensionSettings: the simpler ExtensionInstallForcelist only uses its update URL for the first install. Restart Chrome; tg status should say ok.

Updating (verified: 0.2.2 → 0.3.0): bump version in extension/manifest.json, commit, push, then on the machine git -C ~/.local/share/tg pull && tg pack. Chrome fetches the new build from dist/update.xml on its own schedule (hours); to get it now, open chrome://extensions and press Update (developer mode on), no restart needed. This works because tg pack writes an update_url into the packed manifest: the update URL in the policy only serves the first install, and a build without one is never updated (it needs a one-time remove-and-reinstall via the policy file). For development, Load unpacked extension/ with developer mode on; remove it again before the packed one runs (same id).

With chezmoi, .chezmoiexternal.toml keeps the clone at ~/.local/share/tg and a run_onchange_after_ script calls tg install; pack and policy stay manual.

The main window

Links from outside (Slack, mail: browser-open) that no tab shows yet go into one main window, so the other windows stay whatever you made them. tg main makes the last focused normal window the main one. Chrome gives its windows new ids on every start, so the choice lasts until Chrome quits (or that window closes); until it is made again, links go to the last focused normal window, as Chrome itself would do, minus the new window Chrome opens when the last focused one is a PWA.

Limits

  • Chrome only (google-chrome); Chromium, Vivaldi etc. would need their own manifest dir.
  • One Chrome window per project set is assumed: groups are looked up in the last focused normal window first, otherwise anywhere.
  • The policy file makes Chrome report "managed by your organization" at browser level.

Tests

python3 -I -m unittest discover -s tests

Covers the relay (CLI ⇄ host); the extension itself has to be tried in Chrome.