Skip to content

Repository files navigation

Spaces

Chrome Web Store

Arc-style sidebar for Chrome. Vertical tab list, nested folders per Space, and GitHub PR / Issue Live folders that auto-populate from a query.

This extension runs entirely on its own model — Chrome Tab Groups are not used at all. Switching a Space hides every other window-tab via chrome.tabs.hide and shows the target Space's tabs, so the strip mirrors the Side Panel's view.

image

Install

From the Chrome Web Store (recommended)

Add Spaces to Chrome

After installing, click the Spaces icon in the toolbar to open the Side Panel. (Recommended) Hide Chrome's tab strip with chrome://flags/#vertical-tabs → Enabled, then restart.

Build from source

git clone git@github.com:matuyuhi/chrome-spaces.git
cd chrome-spaces
bun install     # installs deps (Bun ≥ 1.0)
bun run build   # → dist/

In Chrome:

  1. Open chrome://extensions
  2. Toggle Developer mode
  3. Load unpacked → select dist/
  4. Click the Spaces icon in the toolbar — the Side Panel opens

Concepts

  • Space: a named container, scoped to one window. Has color, optional emoji, and a tree of folders / tabs.
  • Folder: an ordered list of items (tabs or sub-folders) inside a Space. Foldable. Optional Live config (see below).
  • Live folder: a folder whose tabs are managed by a LiveSource (a GitHub PR or Issue search query). Add/remove tabs follow the search result on each sync.
  • Pinned tab (snap-back): any tab can be assigned a base URL via right-click → "Pin to current URL". Right-click → "Reset to base URL" navigates the tab back even after you've drilled deeper.

Side Panel

  • + in the header creates a new empty Space.
  • ⇩N appears when N Chrome Tab Groups exist in this window; clicking imports them as Spaces.
  • opens settings (GitHub PAT).
  • Space pills at the top: click to switch (other Spaces' tabs hide in the strip), drag to reorder, drag a tab onto a pill to move it into that Space.
  • Space ⋯ menu: rename, color, emoji, delete (with or without closing tabs).
  • Folder ⋯ menu: rename, emoji, delete, edit Live config.
  • + Folder / + Live folder at the bottom of any non-Live folder.
  • Drag-and-drop: tabs and folders. Live folders refuse drops (their contents are owned by the sync engine).
  • Tab right-click: pin / reset / close.

GitHub Live folders

  1. Generate a Personal Access Token. Fine-grained PAT with "Repository → Pull requests: read" + "Metadata: read" works for PRs; add "Issues: read" if you want Issue folders. Classic repo / public_repo also works.
  2. Side Panel → → paste the token. Stored only in chrome.storage.local on this device.
  3. Inside any Space, click + Live folder at the bottom of the root folder (or a sub-folder). Pick:
    • Source: PR or Issue + preset (review-requested / assigned / authored / mentioned / custom search)
    • User: defaults to @me
    • Filter: empty / * = all repos. A bare acme becomes org:acme. org:foo, user:bar, repo:a/b are taken verbatim. !sb (or -org:sb) excludes that org.
    • Refresh interval: minutes; 0 = manual only

A Live folder shows to sync now, and a badge if the last sync errored (hover for the message). Right-click any tab inside a Live folder's pages and pick "Sync this Live folder" to refresh from anywhere on the web.

Keyboard shortcuts

Chrome only auto-binds 4 shortcuts at install time, and the obvious choices (⌘1–9) collide with Chrome's own tab navigation. So this extension ships with no defaults. Open chrome://extensions/shortcuts and bind whatever you want, e.g.:

Command Suggested
switch-space-1..9 ⌃⌥1–9
new-space ⌃⌥N
reset-current-tab ⌃⌥⇧R
sync-current-live ⌃⌥⇧S

Architecture

src/
├── background/
│   ├── index.ts            ─ SW: bootstrap + message router
│   ├── space-manager.ts    ─ all CRUD on Space / Folder / TabRecord
│   ├── handlers.ts         ─ chrome.tabs / windows event sinks
│   ├── reconcile.ts        ─ prune dead tab refs at startup
│   ├── storage.ts          ─ chrome.storage.local + v1 → v2 migration
│   ├── secret-storage.ts   ─ separate key for the GitHub PAT
│   ├── commands.ts         ─ chrome.commands dispatch
│   ├── context-menus.ts    ─ chrome.contextMenus + handler
│   └── live/
│       ├── alarms.ts       ─ per-folder sync schedule
│       ├── sync-engine.ts  ─ fetch + diff + apply
│       ├── diff.ts         ─ pure list-diff
│       └── sources/
│           └── github.ts   ─ Search API client
├── shared/
│   ├── types.ts            ─ schema v2: Space + Folder + ItemRef + …
│   └── messaging.ts        ─ typed sendMessage RPC
└── sidepanel/
    ├── index.html
    ├── main.tsx
    ├── App.tsx             ─ tree, DnD, menus, settings
    ├── LiveFolderForm.tsx
    └── sidepanel.css

Switching a Space

switchTo(spaceId, windowId):

  1. Persist activeSpaceByWindow[windowId] = spaceId first so any tab event during the switch sees the new state.
  2. Activate one of the target Space's tabs (lastActiveTabId, or the first known tab, or a freshly-created starter tab if the Space is empty). Chrome refuses to hide the active tab.
  3. chrome.tabs.show(targetTabIds) — idempotent for tabs already visible.
  4. chrome.tabs.hide(everyOtherTabInWindow).

Live folder ownership

A Live folder's items is rewritten by the sync engine on every run. After applying the diff, the engine strips its newly-claimed tabIds from any other folder that may have grabbed them via chrome.tabs.onCreated → registerTab while the network call was in flight. The moveItem operation also refuses to drop user-owned items into a Live folder.

Migration

Schema v1 (Tab Group based) is rewritten to v2 once on first run after upgrade. Each old Static Space becomes a Space with the group's tabs in its root folder; each old Live Space becomes a Space with one Live folder in its root, carrying over the source query, managed tabs, and sync history. The underlying Chrome Tab Groups are ungrouped.

Development

bun run test      # vitest run (use `bun run test`, NOT `bun test`,
                  # so the package.json script dispatches into vitest
                  # rather than Bun's built-in test runner)
bun run test:watch
bun run build     # tsc + vite build → dist/
bun run dev       # vite dev (HMR for side panel; SW reloads via crxjs)

Tests run in the node environment with a fake globalThis.chrome installed by setupChromeMock() (see src/background/test-utils.ts).

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages