debug(shell): add console logging to trace tab-switch failure for rspace

Adds diagnostic console.log to layer-switch handler, TabCache.switchTo,
fetchAndInject, and reconcileRemoteLayers to identify why clicking the
rspace tab closes all tabs and shows the dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-24 16:32:57 -07:00
parent 433833da0c
commit 677a69645e
2 changed files with 27 additions and 6 deletions

View File

@ -844,10 +844,12 @@ export function renderShell(opts: ShellOptions): string {
// Reconcile remote layer changes (shared by BroadcastChannel + Automerge)
function reconcileRemoteLayers(remoteLayers) {
console.log('[shell] reconcileRemoteLayers: remote=' + remoteLayers.length + ', local=' + layers.length + ', current=' + currentModuleId);
// Guard: never let remote sync wipe all tabs when we have an active module.
// Empty remote layers indicate a CRDT initial state or sync race, not
// an intentional "close everything" action.
if (remoteLayers.length === 0 && currentModuleId) {
console.log('[shell] BLOCKED empty remote layers (keeping local)');
// Keep local layers intact — remote has nothing useful
tabBar.setLayers(layers);
return;
@ -936,6 +938,7 @@ export function renderShell(opts: ShellOptions): string {
// This prevents the visual desync where the tab highlights before content loads.
tabBar.addEventListener('layer-switch', (e) => {
const { layerId, moduleId } = e.detail;
console.log('[shell] layer-switch:', moduleId, 'layerId:', layerId, 'tabCache:', !!tabCache, 'layers:', layers.length);
currentModuleId = moduleId;
saveTabs();
// Update settings panel to show config for the newly active module
@ -943,15 +946,20 @@ export function renderShell(opts: ShellOptions): string {
if (sp) sp.setAttribute('module-id', moduleId);
if (tabCache) {
tabCache.switchTo(moduleId).then(ok => {
console.log('[shell] switchTo result:', ok, 'for', moduleId);
if (ok) {
tabBar.setAttribute('active', layerId);
} else {
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
const url = window.__rspaceNavUrl(spaceSlug, moduleId);
console.log('[shell] switchTo failed, navigating to:', url);
window.location.href = url;
}
}).catch(() => {
}).catch((err) => {
console.error('[shell] switchTo error:', err);
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
});
} else {
console.log('[shell] no tabCache, navigating to:', window.__rspaceNavUrl(spaceSlug, moduleId));
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
}
});

View File

@ -101,14 +101,19 @@ export class TabCache {
/** Switch to a module tab within the current space. Returns true if handled client-side. */
async switchTo(moduleId: string): Promise<boolean> {
const key = this.paneKey(this.spaceSlug, moduleId);
if (moduleId === this.currentModuleId && this.panes.has(key)) return true;
if (moduleId === this.currentModuleId && this.panes.has(key)) {
console.log("[TabCache] switchTo", moduleId, "→ already current + cached");
return true;
}
if (this.panes.has(key)) {
console.log("[TabCache] switchTo", moduleId, "→ cached, showing pane");
this.showPane(this.spaceSlug, moduleId);
this.updateUrl(this.spaceSlug, moduleId);
return true;
}
console.log("[TabCache] switchTo", moduleId, "→ NOT cached, fetching…");
return this.fetchAndInject(this.spaceSlug, moduleId);
}
@ -165,7 +170,7 @@ export class TabCache {
/** Fetch a module page, extract content, and inject into a new pane */
private async fetchAndInject(space: string, moduleId: string): Promise<boolean> {
const navUrl = (window as any).__rspaceNavUrl;
if (!navUrl) return false;
if (!navUrl) { console.warn("[TabCache] fetchAndInject: no __rspaceNavUrl"); return false; }
const url: string = navUrl(space, moduleId);
// For cross-space fetches, use the path-based API to stay same-origin
@ -177,11 +182,14 @@ export class TabCache {
fetchUrl = `/${space}/${moduleId}`;
}
} catch {
console.warn("[TabCache] fetchAndInject: URL resolve failed for", url);
return false;
}
const app = document.getElementById("app");
if (!app) return false;
if (!app) { console.warn("[TabCache] fetchAndInject: no #app"); return false; }
console.log("[TabCache] fetchAndInject:", fetchUrl, "for", moduleId);
// Show loading spinner
const loadingPane = document.createElement("div");
@ -197,13 +205,16 @@ export class TabCache {
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) {
console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fetchUrl);
loadingPane.remove();
return false;
}
const html = await resp.text();
console.log("[TabCache] fetchAndInject: got", html.length, "bytes for", moduleId);
const content = this.extractContent(html);
if (!content) {
console.warn("[TabCache] fetchAndInject: extractContent returned null for", moduleId, "— HTML has #app:", html.includes('id="app"'));
loadingPane.remove();
return false;
}
@ -233,8 +244,10 @@ export class TabCache {
this.updateUrl(space, moduleId);
this.updateCanvasLayout(moduleId);
console.log("[TabCache] fetchAndInject: SUCCESS for", moduleId);
return true;
} catch {
} catch (err) {
console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err);
loadingPane.remove();
return false;
}