Skip to content

refactor(tab-manager): make Chrome the sole authority for live browser state#1543

Merged
braden-w merged 13 commits intomainfrom
opencode/glowing-pixel
Mar 20, 2026
Merged

refactor(tab-manager): make Chrome the sole authority for live browser state#1543
braden-w merged 13 commits intomainfrom
opencode/glowing-pixel

Conversation

@braden-w
Copy link
Member

This rewrites the tab manager's live browser state around Chrome as the source of truth and removes the old Y.Doc mirror for tabs, windows, and tab groups.

The old design kept two models of the same ephemeral state alive at once: a reactive SvelteMap for the UI and Y.Doc tables for sync. That made every browser event do double work, forced observers to materialize remote changes back into Chrome, and needed echo-detection heuristics to avoid loops. We don't need that machinery for live browser state. Chrome already owns it.

We now seed from browser.windows.getAll({ populate: true }), listen to Chrome events, and keep the sidepanel's live state in a single SvelteMap<number, WindowState>. Y.Doc still backs the data users actually create and expect to persist—saved tabs, bookmarks, chat, tool trust, and device metadata.

The shape change is straightforward:

// before
import type { Tab, Window, WindowCompositeId } from '$lib/workspace';

browserState.tabsByWindow(windowId: WindowCompositeId): Tab[];
// after
import type { BrowserTab, BrowserWindow } from '$lib/state/browser-state.svelte';

browserState.tabsByWindow(windowId: number): BrowserTab[];

That shift let this PR remove the composite browser-state IDs and the conversion layer that only existed to shuttle Chrome data into Y.Doc.

Before
Chrome events ──► SvelteMap ──► UI
      │
      └──────► Y.Doc tables ──► observers ──► Chrome API calls

After
Chrome events ──► SvelteMap ──► UI

Concretely, this PR:

  • rewrites browser-state.svelte.ts to use Chrome-native tabs/windows and event-driven updates only
  • removes live browser-state tables, composite ID helpers, and browser-state queries from workspace.ts
  • deletes row-converters.ts
  • updates sidepanel consumers, tab helpers, and save/bookmark flows to use native numeric tab and window IDs
  • moves device registration into a shared registerDevice() init path
  • adds specs documenting the Chrome-authority design and the follow-up iteration

A couple of trade-offs are worth calling out. Cross-device live tab mirroring goes away with this design, and any future AI queries over live tabs will need to read from browser state directly instead of workspace tables. That's intentional here: the branch favors a simpler, more honest state model over pretending ephemeral browser state is durable shared data.

braden-w added 13 commits March 19, 2026 10:43
…components

Use Empty.Root for both loading and error states in the whenReady gate,
and add {:catch} block so failed seed shows an actionable error instead
of an infinite spinner.
Hoist transaction.origin and deviceId guards to observer callback level
since both are consistent across all changed IDs in a single firing.
Capture narrowed deviceId as const for async closures.
Remove all Y.Doc/CRDT usage for live tabs and windows. Chrome is now the
single source of truth for ephemeral browser data—SvelteMap only, no
dual-writes, no observers, no echo detection.

- Rewrite browser-state.svelte.ts (690 → 406 lines)
- Remove tabsTable, windowsTable, tabGroupsTable from workspace
- Remove composite ID types/helpers (TabCompositeId, WindowCompositeId, etc.)
- Remove row-converters.ts entirely
- Remove 7 Y.Doc-dependent query actions, simplify 8 mutation actions
- Move device registration to standalone registerDevice() in workspace.ts
- Update all consumers for BrowserTab/BrowserWindow types

Saved tabs, bookmarks, chat, and device identity remain Y.Doc-backed.
…lpers JSDoc

- Move windows computation from getter body to $derived for caching
- Update tab-helpers JSDoc to reflect BrowserTab (tabId: number), remove
  stale Y.Doc references and string-ID examples
Replace 30-line manual type declarations with two ReturnType inferences.
The converter functions are now the single source of truth for both the
runtime conversion and the exported type shape.
BrowserTab: remove groupId, openerTabId, status (zero consumers).
BrowserWindow: remove incognito, type (zero consumers).
tab-helpers: unexport normalizeUrl (internal only), replace trySync
with plain try/catch (drops wellcrafted/result import).
# Conflicts:
#	apps/tab-manager/src/entrypoints/sidepanel/App.svelte
#	apps/tab-manager/src/lib/state/bookmark-state.svelte.ts
#	apps/tab-manager/src/lib/state/browser-state.svelte.ts
#	apps/tab-manager/src/lib/state/saved-tab-state.svelte.ts
#	apps/tab-manager/src/lib/workspace.ts
Remove all Y.Doc/CRDT dual-writes, observers, composite IDs, row-converters,
echo detection, and authState.status checks. Chrome is now the sole authority
for live browser tab state—894 lines reduced to 361.

- Add BrowserTab/BrowserWindow intersection types (Browser.tabs.Tab & { id: number })
- Add narrowTab/narrowWindow type guards (no object creation)
- Use $derived for windows getter (cached, not recomputed per access)
- Use raw promise for whenReady (no $state wrapper)
- Single set of event handlers (removed merge-artifact duplicates)

Spec: 20260319T170000-browser-state-chrome-authority-v2.md (Phase 1)
Switch all consumer files from workspace Tab/Window types to Chrome-native
BrowserTab/BrowserWindow from browser-state. Field renames: .tabId→.id,
.windowId→.id (Chrome's native field names).

- saved-tab-state: Tab→BrowserTab, .tabId→.id
- bookmark-state: Tab→BrowserTab
- unified-view-state: .windowId→.id on BrowserWindow accesses
- UnifiedTabList: .windowId→.id, .tabId→.id in getKey + template
- TabItem: .tabId→.id in derived
- items.ts: .tabId→.id, .windowId→.id, title sort null-safety
- tab-helpers: TabLike.tabId→TabLike.id
- App.svelte: add registerDevice() call in onMount
- workspace.ts: add registerDevice() export + device-id imports

Spec: 20260319T170000-browser-state-chrome-authority-v2.md (Phase 4)
Remove tabsTable, windowsTable, tabGroupsTable and all associated
infrastructure: composite ID types/functions, sentinel constants,
tab group color type, internal helpers (nativeTabId, toNativeIds).

Remove query actions that read from deleted tables: tabs.search,
tabs.list, tabs.findDuplicates, tabs.dedup, tabs.groupByDomain,
windows.list, domains.count.

Update remaining mutation actions (close, open, activate, save, group,
pin, mute, reload) to accept native number IDs instead of composite
strings. 1123 lines reduced to 600.

Spec: 20260319T170000-browser-state-chrome-authority-v2.md (Phase 2-3, complete)
- TabItem: remove unnecessary $derived(tab.id) wrapper, use tab.id
  directly; merge duplicate browser-state imports into one
- tab-helpers: update JSDoc examples from tabId to id
- saved-tab-state, bookmark-state: fix indentation on import closing
  brace and method declarations (sub-agent artifacts)
- items.ts: fix indentation in dedup and group-by-domain handlers
…owWindow to BrowserTab/BrowserWindow constructors

Replace plain `{ id: number }` intersections with branded `TabId` and
`WindowId` types (via wellcrafted/brand) to prevent accidental mixing of
tab and window IDs. Brand constructors reject undefined and TAB_ID_NONE
at the ingestion boundary. Rename narrowTab/narrowWindow to match the
PascalCase dual-namespace convention (type + function share the same name).
@github-actions
Copy link
Contributor

Thank you for your contribution! Before we can merge this PR, we need you to sign our Contributor License Agreement. This is a one-time requirement—it lets us offer commercial licenses for the sync server while you retain full copyright on your code.

To sign, please comment on this PR with:

I have read the CLA Document and I hereby sign the CLA


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@braden-w braden-w merged commit d47371d into main Mar 20, 2026
1 of 9 checks passed
@braden-w braden-w deleted the opencode/glowing-pixel branch March 20, 2026 01:05
@github-actions github-actions bot locked and limited conversation to collaborators Mar 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant