From 2bc1b78f3080cb8cfd689c47c54381fde6d82dcb Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 4 Mar 2026 17:49:40 -0800 Subject: [PATCH] fix: make active tab local-only, stop Automerge from overriding it The active tab was stored in Automerge's shared doc (activeLayerId), causing different browser windows/sessions to fight over which tab is highlighted. When one window switched to rInbox, another window showing rFlows would have its tab bar update to highlight rInbox while still displaying rFlows content. Changes: - Active tab is now always determined locally by the URL/currentModuleId - Automerge sync only manages the tab LIST (which modules are open) and flows, not which tab is active - Removed sync.setActiveLayer() calls on tab switch - Removed activeLayerId reads from Automerge on connect and on remote change events - TabCache continues to manage active state correctly when switching tabs within the same page load Co-Authored-By: Claude Opus 4.6 --- server/shell.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/server/shell.ts b/server/shell.ts index 6913892..d75b91a 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -419,10 +419,15 @@ export function renderShell(opts: ShellOptions): string { window.__rspaceTabBar = tabBar; // ── CommunitySync: merge with Automerge once connected ── + // NOTE: The TAB LIST is shared via Automerge (which modules are open), + // but the ACTIVE TAB is always local (determined by the URL / currentModuleId). + // This prevents windows from fighting over which tab is highlighted. document.addEventListener('community-sync-ready', (e) => { const sync = e.detail?.sync; if (!sync) return; + const localActiveId = 'layer-' + currentModuleId; + // Merge: Automerge layers win if they exist, otherwise seed from localStorage const remoteLayers = sync.getLayers(); if (remoteLayers.length > 0) { @@ -433,24 +438,21 @@ export function renderShell(opts: ShellOptions): string { } layers = sync.getLayers(); tabBar.setLayers(layers); - const activeId = sync.doc.activeLayerId; - if (activeId) tabBar.setAttribute('active', activeId); tabBar.setFlows(sync.getFlows()); } else { // First connection: push all localStorage tabs into Automerge for (const l of layers) { sync.addLayer(l); } - sync.setActiveLayer('layer-' + currentModuleId); } + // Active tab stays local — always matches the URL + tabBar.setAttribute('active', localActiveId); + // Keep localStorage in sync saveTabs(); - // Sync layer changes back to Automerge - tabBar.addEventListener('layer-switch', (e) => { - sync.setActiveLayer(e.detail.layerId); - }); + // Sync layer list changes to Automerge (not active tab) tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; const newLayer = makeLayer(moduleId, sync.getLayers().length); @@ -469,13 +471,13 @@ export function renderShell(opts: ShellOptions): string { tabBar.addEventListener('flow-remove', (e) => { sync.removeFlow(e.detail.flowId); }); tabBar.addEventListener('view-toggle', (e) => { sync.setLayerViewMode(e.detail.mode); }); - // Listen for remote changes + // Listen for remote changes — sync tab list and flows only. + // Never touch the active tab: it's managed locally by TabCache + // and the tab-bar component via layer-switch events. sync.addEventListener('change', () => { layers = sync.getLayers(); tabBar.setLayers(layers); tabBar.setFlows(sync.getFlows()); - const activeId = sync.doc.activeLayerId; - if (activeId) tabBar.setAttribute('active', activeId); const viewMode = sync.doc.layerViewMode; if (viewMode) tabBar.setAttribute('view-mode', viewMode); saveTabs(); // keep localStorage in sync