fix(shell): eliminate phantom tab persistence — validate + dedup on restore
Filter restored tabs against valid moduleIds from moduleList, deduplicate on every restore path (localStorage, server sync, BroadcastChannel). Add closed-module tracking to renderExternalAppShell to prevent server sync from resurrecting tabs closed in the current session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cb521cad12
commit
932e550c66
|
|
@ -871,6 +871,9 @@ export function renderShell(opts: ShellOptions): string {
|
|||
};
|
||||
}
|
||||
|
||||
// Valid module IDs for this space (used to filter out stale/phantom tabs)
|
||||
const validModuleIds = new Set(moduleList.map(m => m.id));
|
||||
|
||||
// ── Restore tabs from localStorage ──
|
||||
let layers;
|
||||
try {
|
||||
|
|
@ -879,12 +882,15 @@ export function renderShell(opts: ShellOptions): string {
|
|||
if (!Array.isArray(layers)) layers = [];
|
||||
} catch(e) { layers = []; }
|
||||
|
||||
// Filter out stale tabs whose moduleId no longer exists
|
||||
layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
|
||||
|
||||
// Ensure the current module is in the tab list
|
||||
if (!layers.find(l => l.moduleId === currentModuleId)) {
|
||||
layers.push(makeLayer(currentModuleId, layers.length));
|
||||
}
|
||||
|
||||
// Persist immediately (includes the newly-added tab)
|
||||
// Persist immediately (cleaned list)
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
|
||||
// Render all tabs with the current one active
|
||||
|
|
@ -954,7 +960,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
if (!next.has(mid) && tabCache) tabCache.removePane(mid);
|
||||
}
|
||||
|
||||
layers = deduplicateLayers(remoteLayers);
|
||||
layers = deduplicateLayers(remoteLayers.filter(l => validModuleIds.has(l.moduleId)));
|
||||
|
||||
// Always ensure the current module stays in the tab list
|
||||
if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) {
|
||||
|
|
@ -1001,7 +1007,8 @@ export function renderShell(opts: ShellOptions): string {
|
|||
// Server-authoritative: adopt server tabs exactly.
|
||||
// Only skip tabs the user closed in THIS session (prevents
|
||||
// close-then-immediate-resurrect before save propagates).
|
||||
var serverTabs = data.tabs.filter(t => !_closedModuleIds.has(t.moduleId));
|
||||
var serverTabs = data.tabs.filter(t => validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId));
|
||||
serverTabs = deduplicateLayers(serverTabs);
|
||||
serverTabs.forEach((l, i) => { l.order = i; });
|
||||
layers = serverTabs;
|
||||
// Ensure the currently navigated module is present
|
||||
|
|
@ -1711,6 +1718,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
|
||||
if (tabBar) {
|
||||
tabBar.setModules(moduleList);
|
||||
const validModuleIds = new Set(moduleList.map(m => m.id));
|
||||
function getModuleLabel(id) {
|
||||
const m = moduleList.find(mod => mod.id === id);
|
||||
return m ? m.name : id;
|
||||
|
|
@ -1718,13 +1726,20 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
function makeLayer(id, order) {
|
||||
return { id: 'layer-' + id, moduleId: id, label: getModuleLabel(id), order, color: '', visible: true, createdAt: Date.now() };
|
||||
}
|
||||
function deduplicateLayers(list) {
|
||||
const seen = new Set();
|
||||
return list.filter(l => { if (seen.has(l.moduleId)) return false; seen.add(l.moduleId); return true; });
|
||||
}
|
||||
let layers;
|
||||
try { const saved = localStorage.getItem(TABS_KEY); layers = saved ? JSON.parse(saved) : []; if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; }
|
||||
// Filter out stale/phantom tabs and deduplicate
|
||||
layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
|
||||
if (!layers.find(l => l.moduleId === currentModuleId)) layers.push(makeLayer(currentModuleId, layers.length));
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
tabBar.setLayers(layers);
|
||||
tabBar.setAttribute('active', 'layer-' + currentModuleId);
|
||||
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
|
||||
const _closedModuleIds = new Set();
|
||||
function saveTabs() {
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
try {
|
||||
|
|
@ -1754,7 +1769,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.then(function(data) {
|
||||
if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; }
|
||||
layers = data.tabs;
|
||||
// Filter stale moduleIds and tabs closed this session
|
||||
layers = deduplicateLayers(data.tabs.filter(function(t) { return validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId); }));
|
||||
layers.forEach(function(l, i) { l.order = i; });
|
||||
if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length));
|
||||
tabBar.setLayers(layers);
|
||||
|
|
@ -1765,8 +1781,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
})();
|
||||
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
|
||||
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
|
||||
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
||||
tabBar.addEventListener('layers-close-all', () => { layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
||||
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; const closedMid = layerId.replace('layer-', ''); _closedModuleIds.add(closedMid); tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
||||
tabBar.addEventListener('layers-close-all', () => { layers.forEach(l => _closedModuleIds.add(l.moduleId)); layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
||||
tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); });
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue