Files
arcum42-ComfyUI_SageUtils/js/sidebar/cacheSidebar.js
T

1016 lines
37 KiB
JavaScript

/**
* SageUtils Sidebar Tab with Multiple Sub-tabs
* Multi-tabbed interface for model browser and file manager
*
* Performance Optimization: Uses hide/show approach instead of recreating tab content
* each time, which maintains state and reduces loading times for subsequent tab switches.
*/
// Import centralized state management
import {
getState,
subscribe,
actions,
selectors
} from "../shared/stateManager.js";
import {
handleError
} from "../shared/errorHandler.js";
// Import decomposed tab modules
// import { createModelsTab } from "./modelsTab.js";
import { createModelsTabV2 } from "./modelsTabV2.js";
import { createFilesTab } from "./filesTab.js";
import { createCivitaiSearchTab } from "./civitaiSearchTab.js";
import { createImageGalleryTab } from "./imageGalleryTab.js";
import { createPromptBuilderTab } from "./promptBuilderTab.js";
import { createLLMTab } from "./llmTab.js";
import { createTestSidebarTab } from "./testSidebarTab.js";
// Import cache API functions
import {
fetchCacheHash,
fetchCacheInfo
} from "../shared/api/cacheApi.js";
// Import global data cache
import { DataCache, CacheKeys } from "../shared/dataCache.js";
// Import gallery API for background preloading
import {
loadImagesFromFolder
} from "../shared/api/galleryApi.js";
// Import shared UI components
import {
createMainContainer
} from "../components/cacheUI.js";
// Import TabManager
import { TabManager } from "../components/tabs.js";
// Import API for settings
import { api } from '../../../../scripts/api.js';
import {
DEFAULT_TAB_VISIBILITY_SETTINGS,
normalizeTabVisibilitySettings,
toTabVisibilityMap
} from '../shared/sidebarVisibility.js';
// Import performance timing utilities for telemetry persistence
import { startTimer, endTimer, javascriptTimer, shouldSendTimingData, shouldLogTimingDetails } from "../shared/performanceTimer.js";
console.log('[SageUtils] cacheSidebar.js imported');
// Always use Models Tab V2
// Track the current sidebar element for reloading
let currentSidebarElement = null;
// Global error handlers are shared across sidebar mounts; keep them attached
// only while at least one sidebar instance is active.
let globalErrorHandlersRefCount = 0;
let globalSidebarErrorHandler = null;
let globalSidebarRejectionHandler = null;
const SIDEBAR_SHELL_STYLE_ID = 'sageutils-sidebar-shell-styles';
function isSidebarDebugEnabled() {
try {
if (shouldLogTimingDetails()) {
return true;
}
if (typeof window === 'undefined') {
return false;
}
const debugFlag = window.localStorage?.getItem('sageutils_sidebar_debug') === 'true';
const queryFlag = new URLSearchParams(window.location.search).get('sage_sidebar_debug') === '1';
return debugFlag || queryFlag;
} catch {
return shouldLogTimingDetails();
}
}
function logSidebarDebug(...args) {
if (isSidebarDebugEnabled()) {
console.debug(...args);
}
}
function logSidebarInfo(...args) {
if (isSidebarDebugEnabled()) {
console.info(...args);
}
}
function ensureSidebarShellStyles() {
if (document.getElementById(SIDEBAR_SHELL_STYLE_ID)) {
return;
}
const style = document.createElement('style');
style.id = SIDEBAR_SHELL_STYLE_ID;
style.textContent = `
.sidebar-settings-button {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: #2a2a2a;
border: 1px solid #444;
border-radius: 4px;
color: #ccc;
padding: 6px 10px;
cursor: pointer;
font-size: 18px;
transition: all 0.2s ease;
z-index: 10;
flex-shrink: 0;
}
.sidebar-settings-button:hover {
background: #4CAF50;
border-color: #4CAF50;
transform: translateY(-50%) scale(1.1);
}
.sidebar-settings-indicator {
position: absolute;
right: 60px;
top: 50%;
transform: translateY(-50%);
background: #2a2a2a;
border: 1px solid #444;
border-radius: 4px;
color: #ddd;
padding: 4px 8px;
font-size: 12px;
opacity: 0.9;
z-index: 9;
}
`;
document.head.appendChild(style);
}
function acquireGlobalErrorHandlers() {
if (!globalSidebarErrorHandler) {
globalSidebarErrorHandler = (event) => {
console.error('Sidebar error caught:', event.error);
if (event.error) {
handleError(event.error, { component: 'Sidebar', operation: 'Global Error Handler' });
}
};
}
if (!globalSidebarRejectionHandler) {
globalSidebarRejectionHandler = (event) => {
console.error('Sidebar promise rejection:', event.reason);
if (event.reason) {
handleError(event.reason, { component: 'Sidebar', operation: 'Promise Rejection' });
}
};
}
if (globalErrorHandlersRefCount === 0) {
window.addEventListener('error', globalSidebarErrorHandler);
window.addEventListener('unhandledrejection', globalSidebarRejectionHandler);
}
globalErrorHandlersRefCount += 1;
return () => {
globalErrorHandlersRefCount = Math.max(0, globalErrorHandlersRefCount - 1);
if (globalErrorHandlersRefCount === 0) {
window.removeEventListener('error', globalSidebarErrorHandler);
window.removeEventListener('unhandledrejection', globalSidebarRejectionHandler);
}
};
}
function createCleanupRegistry() {
const cleanups = new Set();
const add = (cleanup) => {
if (typeof cleanup !== 'function') {
return () => {};
}
cleanups.add(cleanup);
return () => cleanups.delete(cleanup);
};
const addInterval = (intervalId) => add(() => clearInterval(intervalId));
const addTimeout = (timeoutId) => add(() => clearTimeout(timeoutId));
const addObserver = (observer) => {
if (!observer || typeof observer.disconnect !== 'function') {
return () => {};
}
return add(() => observer.disconnect());
};
const runAll = () => {
cleanups.forEach((cleanup) => {
try {
cleanup();
} catch (error) {
console.warn('[Sidebar] Cleanup callback failed:', error);
}
});
cleanups.clear();
};
return {
add,
addInterval,
addTimeout,
addObserver,
runAll,
cleanup: runAll
};
}
/**
* Load tab visibility settings from the backend
* @returns {Promise<Object>} Tab visibility settings
*/
async function loadTabVisibilitySettings() {
try {
startTimer('Sidebar.Settings.Total');
startTimer('Sidebar.Settings.Fetch');
const t0 = performance.now();
const response = await api.fetchApi('/sage_utils/settings');
endTimer('Sidebar.Settings.Fetch');
const t1 = performance.now();
if (response.ok) {
startTimer('Sidebar.Settings.Parse');
const jsonStart = performance.now();
const data = await response.json();
endTimer('Sidebar.Settings.Parse');
const jsonEnd = performance.now();
if (data.success && data.settings) {
const settings = normalizeTabVisibilitySettings(data.settings);
startTimer('Sidebar.Settings.Map');
const mappedStart = performance.now();
const result = {
...settings,
_timing: {
fetch_ms: +(t1 - t0).toFixed(2),
parse_ms: +(jsonEnd - jsonStart).toFixed(2),
map_ms: +(performance.now() - mappedStart).toFixed(2),
total_ms: +(performance.now() - t0).toFixed(2)
}
};
endTimer('Sidebar.Settings.Map');
if (shouldLogTimingDetails()) {
console.info('[Sidebar] Settings loaded', result._timing);
}
endTimer('Sidebar.Settings.Total');
return result;
}
}
} catch (error) {
console.warn('Failed to load tab visibility settings, using defaults:', error);
}
// Return all tabs visible by default
const fallback = {
...DEFAULT_TAB_VISIBILITY_SETTINGS,
_timing: { fallback: true }
};
logSidebarInfo('[Sidebar] Using default visibility settings');
endTimer('Sidebar.Settings.Total');
return fallback;
}
/**
* Creates the tab manager with all tabs
* @param {HTMLElement} container - Container element for tabs
* @param {Object} tabVisibility - Object containing visibility settings for each tab
* @param {Object} tabContentFactories - Object containing content factory functions for each tab
* @returns {TabManager} Configured TabManager instance
*/
function createTabManager(container, tabVisibility = {}, tabContentFactories = {}) {
ensureSidebarShellStyles();
const tabManager = new TabManager({
container: container,
lazyLoad: true,
onTabSwitch: (tabId) => {
logSidebarDebug(`Switched to tab: ${tabId}`);
},
onTabInit: (tabId) => {
logSidebarDebug(`Initialized tab: ${tabId}`);
}
});
// Initialize the tab manager
tabManager.init();
// Add all tabs with their visibility settings and content factories
if (tabVisibility.show_models_tab !== false) {
tabManager.addTab('models', 'Models', tabContentFactories.models, { active: true });
}
if (tabVisibility.show_files_tab !== false) {
tabManager.addTab('notes', 'Files', tabContentFactories.notes);
}
if (tabVisibility.show_search_tab !== false) {
tabManager.addTab('civitai', 'Search', tabContentFactories.civitai);
}
if (tabVisibility.show_gallery_tab !== false) {
tabManager.addTab('gallery', 'Gallery', tabContentFactories.gallery);
}
if (tabVisibility.show_prompts_tab !== false) {
tabManager.addTab('promptBuilder', 'Prompts', tabContentFactories.promptBuilder);
}
if (tabVisibility.show_llm_tab !== false) {
tabManager.addTab('llm', 'LLM', tabContentFactories.llm);
}
tabManager.addTab('testcss', 'Test CSS', tabContentFactories.testcss);
// Add settings button to the tab header
const settingsButton = document.createElement('button');
settingsButton.innerHTML = '⚙️';
settingsButton.title = 'Settings';
settingsButton.className = 'sidebar-settings-button';
// Click handler
settingsButton.addEventListener('click', async () => {
try {
const { showSettingsDialog } = await import('../dialogs/settingsDialog.js');
showSettingsDialog();
} catch (error) {
console.error('Failed to open settings dialog:', error);
handleError(error, 'Settings Dialog Error');
}
});
tabManager.tabHeader.appendChild(settingsButton);
// Check if settings button overlaps with tabs and hide it if necessary
const checkSettingsButtonVisibility = () => {
const header = tabManager.tabHeader;
const tabs = header.querySelectorAll('.tab-button');
if (tabs.length === 0) return;
// Calculate total width needed for tabs
let tabsWidth = 0;
tabs.forEach(tab => {
tabsWidth += tab.offsetWidth;
});
// Add padding and gap spacing
const headerPadding = 20; // 10px on each side
const tabGaps = (tabs.length - 1) * 2; // 2px gap between tabs
const settingsButtonWidth = 50; // Approximate width of settings button
const requiredWidth = tabsWidth + tabGaps + headerPadding + settingsButtonWidth;
// Hide button if there's not enough space
if (header.offsetWidth < requiredWidth) {
settingsButton.style.display = 'none';
} else {
settingsButton.style.display = 'block';
}
};
// Check visibility on load and resize
const settingsVisibilityTimeoutId = setTimeout(checkSettingsButtonVisibility, 100);
tabManager.registerCleanup(() => clearTimeout(settingsVisibilityTimeoutId));
// Use ResizeObserver to dynamically check when header size changes
const resizeObserver = new ResizeObserver(() => {
checkSettingsButtonVisibility();
});
resizeObserver.observe(tabManager.tabHeader);
tabManager.registerCleanup(() => resizeObserver.disconnect());
return tabManager;
}
/**
* Initialize the cache sidebar data and state
* @param {Object} lifecycle - Cleanup lifecycle helpers for mount-scoped resources
*/
async function initializeSidebarData(lifecycle) {
try {
// Set loading state
actions.setModelsLoading(true);
lifecycle.add(acquireGlobalErrorHandlers());
// Set up state subscriptions for debugging (optional)
const unsubscribe = subscribe((state, prevState) => {
// Guard against undefined prevState (initial state)
if (!prevState) return;
// Only log significant state changes
if (state.activeTab !== prevState.activeTab) {
logSidebarDebug('Active tab changed:', prevState.activeTab, '->', state.activeTab);
}
if (state.models?.selectedHash !== prevState.models?.selectedHash) {
logSidebarDebug('Selected model changed:', prevState.models?.selectedHash, '->', state.models?.selectedHash);
}
if (state.models?.isLoading !== prevState.models?.isLoading) {
logSidebarDebug('Models loading state changed:', state.models?.isLoading);
}
if (state.notes?.isLoading !== prevState.notes?.isLoading) {
logSidebarDebug('Notes loading state changed:', state.notes?.isLoading);
}
});
lifecycle.add(unsubscribe);
// Check if data is already cached from preload
const cacheHashReady = DataCache.isReady(CacheKeys.CACHE_HASH);
const cacheInfoReady = DataCache.isReady(CacheKeys.CACHE_INFO);
if (cacheHashReady && cacheInfoReady) {
logSidebarDebug('[Sidebar] Using preloaded cache data');
// Get cached data
const hashData = DataCache.get(CacheKeys.CACHE_HASH);
const infoData = DataCache.get(CacheKeys.CACHE_INFO);
// Store in state management
actions.setCacheData({ hash: hashData, info: infoData });
logSidebarDebug('Cache data loaded from preload', {
hashEntries: Object.keys(hashData).length,
infoEntries: Object.keys(infoData).length
});
} else {
// Fallback: Load cache data if not preloaded
logSidebarDebug('[Sidebar] Cache not preloaded, loading now...');
try {
const [hashData, infoData] = await Promise.all([
fetchCacheHash(),
fetchCacheInfo()
]);
// Store in cache for future use
DataCache.set(CacheKeys.CACHE_HASH, hashData);
DataCache.set(CacheKeys.CACHE_INFO, infoData);
// Store in state management
actions.setCacheData({ hash: hashData, info: infoData });
logSidebarDebug('Cache data loaded', {
hashEntries: Object.keys(hashData).length,
infoEntries: Object.keys(infoData).length
});
} catch (cacheError) {
console.warn('Failed to load cache data:', cacheError);
// Don't throw - let individual tabs handle their own loading
}
}
// Initialize API connection status check
try {
const response = await fetch('/sage_cache/stats');
if (response.ok) {
logSidebarDebug('Cache API connection verified');
} else {
console.warn('Cache API connection issue:', response.status);
}
} catch (apiError) {
console.warn('Failed to verify API connection:', apiError);
}
// Set up periodic cache refresh (every 5 minutes)
const refreshIntervalId = setInterval(async () => {
try {
const currentState = getState();
// Only refresh if not currently loading and a model is selected
if (!currentState.models.isLoading && currentState.models.selectedHash) {
const [hashData, infoData] = await Promise.all([
fetchCacheHash(),
fetchCacheInfo()
]);
// Update cache
DataCache.set(CacheKeys.CACHE_HASH, hashData);
DataCache.set(CacheKeys.CACHE_INFO, infoData);
// Update state
actions.setCacheData({ hash: hashData, info: infoData });
logSidebarDebug('Periodic cache refresh completed');
}
} catch (refreshError) {
console.warn('Periodic cache refresh failed:', refreshError);
}
}, 5 * 60 * 1000); // 5 minutes
lifecycle.addInterval(refreshIntervalId);
} catch (error) {
console.error('Failed to initialize sidebar data:', error);
handleError(error, 'Sidebar Initialization Error');
} finally {
actions.setModelsLoading(false);
}
}
/**
* Main function to create the cache sidebar with tabs
* @param {HTMLElement} el - Element to populate with the sidebar
*/
export function createCacheSidebar(el) {
const sidebarRenderStart = performance.now();
console.log('[Sidebar] createCacheSidebar invoked', { el });
if (!el) {
console.error('[Sidebar] createCacheSidebar called with no container element');
throw new Error('createCacheSidebar requires a valid container element');
}
// Store reference for potential reload
currentSidebarElement = el;
const lifecycle = createCleanupRegistry();
// Start with safe defaults (all tabs visible) to avoid blank UI on slow startup
const defaultVisibility = { ...DEFAULT_TAB_VISIBILITY_SETTINGS };
let tabVisibility = defaultVisibility;
let isDestroyed = false;
const pendingTimeouts = new Set();
let backgroundTabPreload = null;
let galleryPreloadToken = 0;
const cancelBackgroundTabPreload = () => {
if (!backgroundTabPreload || typeof backgroundTabPreload.cancel !== 'function') {
backgroundTabPreload = null;
return;
}
try {
backgroundTabPreload.cancel();
} catch (error) {
console.warn('[Sidebar] Failed to cancel background tab preload:', error);
}
backgroundTabPreload = null;
};
// Track delayed callbacks so cleanup can cancel them safely.
const scheduleDelayed = (callback, delayMs) => {
let timeoutId = setTimeout(() => {
pendingTimeouts.delete(timeoutId);
timeoutId = null;
if (isDestroyed) {
return;
}
callback();
}, delayMs);
pendingTimeouts.add(timeoutId);
lifecycle.addTimeout(timeoutId);
return {
cancel() {
if (timeoutId === null) {
return;
}
clearTimeout(timeoutId);
pendingTimeouts.delete(timeoutId);
timeoutId = null;
}
};
};
// Create main container
const mainContainer = createMainContainer();
// Create tab content factories for lazy loading
const tabContentFactories = {
models: createModelsTabV2,
notes: createFilesTab,
civitai: createCivitaiSearchTab,
gallery: createImageGalleryTab,
promptBuilder: createPromptBuilderTab,
llm: createLLMTab,
testcss: createTestSidebarTab // New test tab for CSS
};
// Create tab manager
const tabManager = createTabManager(mainContainer, tabVisibility, tabContentFactories);
// Add to provided element
el.appendChild(mainContainer);
console.log('[Sidebar] mainContainer appended to sidebar element');
// Add a lightweight settings loading indicator in the tab header
const settingsIndicator = document.createElement('div');
settingsIndicator.textContent = 'Loading settings…';
settingsIndicator.className = 'sidebar-settings-indicator';
// The tabHeader exists post-init inside TabManager
try { tabManager.tabHeader.appendChild(settingsIndicator); } catch { /* no-op */ }
// Initialize sidebar data and state
initializeSidebarData(lifecycle);
// Activate the first visible tab
tabManager.activateFirstTab();
console.log('[Sidebar] initial tab activation completed', {
activeTab: tabManager.getActiveTab(),
visibleTabs: tabManager.getVisibleTabIds()
});
console.log('[Sidebar] createCacheSidebar initial render completed', {
durationMs: +(performance.now() - sidebarRenderStart).toFixed(2)
});
// Load tab visibility settings asynchronously and apply when ready
// This prevents a blank sidebar if the settings endpoint is slow or unavailable at startup
const settingsLoadStart = performance.now();
loadTabVisibilitySettings().then((settings) => {
if (isDestroyed) {
return;
}
try {
tabVisibility = normalizeTabVisibilitySettings(settings || defaultVisibility);
const mapStart = performance.now();
const visibilityMap = toTabVisibilityMap(tabVisibility, {
ensureOneVisible: true,
fallbackTabId: 'models'
});
// Ensure at least one tab remains visible to avoid a blank sidebar
const anyVisible = Object.values(visibilityMap).some(v => v);
if (!anyVisible) {
console.warn('[Sidebar] All tabs were configured hidden; forcing Models tab visible to avoid blank UI.');
visibilityMap.models = true;
}
const mapEnd = performance.now();
const updateStart = performance.now();
startTimer('Sidebar.Visibility.Update');
tabManager.updateVisibility(visibilityMap);
endTimer('Sidebar.Visibility.Update');
const updateEnd = performance.now();
// If current active tab became hidden, activate the first visible tab
const activeId = tabManager.getActiveTab();
const activateStart = performance.now();
startTimer('Sidebar.Visibility.Activate');
if (!activeId || !visibilityMap[activeId]) {
tabManager.activateFirstTab();
}
endTimer('Sidebar.Visibility.Activate');
const activateEnd = performance.now();
// Log a breakdown of where time was spent
const totalMs = +(performance.now() - settingsLoadStart).toFixed(2);
const fetchMs = settings?._timing?.fetch_ms ?? null;
const parseMs = settings?._timing?.parse_ms ?? null;
const mapMs = settings?._timing?.map_ms ?? +(mapEnd - mapStart).toFixed(2);
const updateMs = +(updateEnd - updateStart).toFixed(2);
const activateMs = +(activateEnd - activateStart).toFixed(2);
logSidebarInfo('[Sidebar] Visibility settings applied', {
total_ms: totalMs,
fetch_ms: fetchMs,
parse_ms: parseMs,
map_ms: mapMs,
update_visibility_ms: updateMs,
activate_ms: activateMs
});
// Optionally persist timing immediately if telemetry is enabled
if (shouldSendTimingData()) {
javascriptTimer.sendTimingDataToServer?.().catch(() => {});
}
// Remove or update the indicator
if (settingsIndicator?.parentNode) {
settingsIndicator.textContent = `Settings applied in ${totalMs} ms`;
scheduleDelayed(() => {
try { settingsIndicator.parentNode.removeChild(settingsIndicator); } catch { /* no-op */ }
}, 1200);
}
} catch (e) {
console.warn('[Sidebar] Failed applying visibility settings, continuing with defaults:', e);
if (settingsIndicator?.parentNode) {
try { settingsIndicator.parentNode.removeChild(settingsIndicator); } catch { /* no-op */ }
}
}
}).catch((e) => {
if (isDestroyed) {
return;
}
console.warn('[Sidebar] Visibility settings load failed, using defaults:', e);
if (settingsIndicator?.parentNode) {
try { settingsIndicator.parentNode.removeChild(settingsIndicator); } catch { /* no-op */ }
}
});
// Start tab preloading first so the most likely tab switches are warmed before
// lower-priority data fetches begin competing for the same startup window.
scheduleDelayed(() => {
logSidebarDebug('[Sidebar] Starting background tab preloading...');
backgroundTabPreload = tabManager.preloadTabsDuringIdle({
maxIdleTime: 20, // Lower threshold to ensure we have real idle time
timeout: 5000, // Longer timeout to wait for idle periods
priority: ['llm', 'gallery', 'promptBuilder', 'notes', 'civitai']
});
}, 1500); // Give the initial tab time to settle before idle warmup starts.
// Gallery data preload runs after tab warmup so it can hydrate shared state without
// outranking interactive tab initialization during early sidebar startup.
scheduleDelayed(() => {
const currentPreloadToken = ++galleryPreloadToken;
const defaultFolder = actions ? (typeof actions.selectedFolder === 'function' ? actions.selectedFolder() : 'notes') : 'notes';
const savedFolder = selectors.selectedFolder ? selectors.selectedFolder() : defaultFolder;
const galleryFolder = savedFolder !== 'custom' ? savedFolder : 'notes'; // Don't auto-load custom folders
// Check if already preloaded in cache
const cacheKey = `galleryImages:${galleryFolder}`;
if (DataCache.isReady(cacheKey)) {
logSidebarDebug(`[Sidebar] Gallery images for '${galleryFolder}' already preloaded`);
if (isDestroyed || currentPreloadToken !== galleryPreloadToken) {
return;
}
// Store in state management
const cachedData = DataCache.get(cacheKey);
if (cachedData && cachedData.images) {
actions.setImages(cachedData.images);
actions.setFolders(cachedData.folders || []);
logSidebarDebug(`[Sidebar] Gallery cache hit: ${cachedData.images.length} images, ${cachedData.folders?.length || 0} folders`);
}
return;
}
logSidebarDebug(`[Sidebar] Preloading gallery images from '${galleryFolder}' folder in background...`);
loadImagesFromFolder(galleryFolder, null, (msg) => {
// Only log start and completion messages, skip progress updates to reduce console noise
if (msg.includes('Loading images from') || msg.includes('Error') || msg.includes('Complete')) {
logSidebarDebug(`[Sidebar Gallery Preload] ${msg}`);
}
}).then((result) => {
if (isDestroyed || currentPreloadToken !== galleryPreloadToken) {
return;
}
logSidebarDebug(`[Sidebar] Gallery preload complete for '${galleryFolder}' folder`);
// Store in cache for future use
DataCache.set(cacheKey, result);
// Also store in state management
if (result && result.images) {
actions.setImages(result.images);
actions.setFolders(result.folders || []);
}
}).catch(err => {
if (isDestroyed || currentPreloadToken !== galleryPreloadToken) {
return;
}
console.warn(`[Sidebar] Gallery preload failed (non-critical):`, err);
});
}, 2000); // Delay data preload until after tab warmup has been scheduled.
// Store references for potential external access
const getRegressionSnapshot = () => {
const visibleTabIds = tabManager.getVisibleTabIds();
const activeTabId = tabManager.getActiveTab();
const activeContainers = Array.from(tabManager.tabContainers.entries())
.filter(([, container]) => container.classList.contains('active'))
.map(([tabId, container]) => ({ tabId, childCount: container.children.length }));
return {
isDestroyed,
visibleTabIds,
activeTabId,
activeContainers,
initializedTabIds: Array.from(tabManager.initializedTabs),
pendingInitCount: tabManager.pendingInitTimeouts?.size || 0,
hasScheduledTabPreload: !!backgroundTabPreload,
pendingDelayedCallbacks: pendingTimeouts.size
};
};
const runRegressionChecks = ({ log = true } = {}) => {
const snapshot = getRegressionSnapshot();
const checks = [
{
name: 'single-active-container',
pass: snapshot.activeContainers.length <= 1,
details: `active=${snapshot.activeContainers.length}`
},
{
name: 'active-tab-visible',
pass: !snapshot.activeTabId || snapshot.visibleTabIds.includes(snapshot.activeTabId),
details: `activeTab=${snapshot.activeTabId || 'none'}`
},
{
name: 'active-container-matches-active-tab',
pass: snapshot.activeContainers.length === 0
|| snapshot.activeContainers[0].tabId === snapshot.activeTabId,
details: snapshot.activeContainers.length > 0
? `container=${snapshot.activeContainers[0].tabId}, activeTab=${snapshot.activeTabId}`
: 'no active container'
},
{
name: 'no-pending-init-after-destroy',
pass: !snapshot.isDestroyed || snapshot.pendingInitCount === 0,
details: `destroyed=${snapshot.isDestroyed}, pendingInit=${snapshot.pendingInitCount}`
}
];
const result = {
ok: checks.every((check) => check.pass),
timestamp: new Date().toISOString(),
checks,
snapshot
};
if (log) {
console.groupCollapsed(`[Sidebar Regression] ${result.ok ? 'PASS' : 'FAIL'} @ ${result.timestamp}`);
console.table(checks.map((check) => ({
check: check.name,
pass: check.pass,
details: check.details
})));
console.log('Snapshot:', snapshot);
console.groupEnd();
}
return result;
};
el._sidebarData = {
tabManager,
switchTab: (tabId) => tabManager.switchTab(tabId),
initializedTabs: tabManager.initializedTabs,
getRegressionSnapshot,
runRegressionChecks,
cleanup: () => {
isDestroyed = true;
galleryPreloadToken += 1;
cancelBackgroundTabPreload();
pendingTimeouts.forEach((timeoutId) => {
clearTimeout(timeoutId);
});
pendingTimeouts.clear();
// Cleanup cross-tab subscription
if (crossTabUnsubscribe) {
try {
crossTabUnsubscribe();
crossTabUnsubscribe = null;
} catch (err) {
console.warn('[Sidebar] Error unsubscribing from cross-tab messaging:', err);
}
}
lifecycle.cleanup();
// Destroy tab manager
tabManager.destroy();
}
};
if (typeof window !== 'undefined') {
window.SageUtilsSidebarRegression = {
run: (options = {}) => {
if (!currentSidebarElement?._sidebarData?.runRegressionChecks) {
return {
ok: false,
checks: [{ name: 'sidebar-mounted', pass: false, details: 'No sidebar instance found' }]
};
}
return currentSidebarElement._sidebarData.runRegressionChecks(options);
},
smoke: async (options = {}) => {
const {
log = false,
delayMs = 0,
throwOnFail = true
} = options;
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
const result = window.SageUtilsSidebarRegression.run({ log });
if (result.ok || !throwOnFail) {
return result;
}
const failedChecks = (result.checks || [])
.filter((check) => !check.pass)
.map((check) => check.name)
.join(', ');
throw new Error(`Sidebar regression smoke check failed: ${failedChecks || 'unknown check failure'}`);
},
snapshot: () => {
if (!currentSidebarElement?._sidebarData?.getRegressionSnapshot) {
return null;
}
return currentSidebarElement._sidebarData.getRegressionSnapshot();
}
};
}
// Subscribe to cross-tab messaging for tab switching
// Use a slight delay to ensure all components are fully initialized
let crossTabUnsubscribe = null;
scheduleDelayed(() => {
import('../shared/crossTabMessaging.js').then(({ getEventBus, MessageTypes }) => {
if (isDestroyed) {
return;
}
const bus = getEventBus();
crossTabUnsubscribe = bus.subscribe(MessageTypes.TAB_SWITCH_REQUEST, (message) => {
if (isDestroyed) {
return;
}
const { tabId, source } = message.data;
// Map tab IDs to actual tab keys used in switchTab()
const tabKeyMap = {
'llm': 'llm',
'gallery': 'gallery',
'prompts': 'promptBuilder',
'prompt-builder': 'promptBuilder', // Alias
'files': 'notes',
'notes': 'notes',
'models': 'models',
'civitai': 'civitai'
};
const tabKey = tabKeyMap[tabId] || tabId;
// Validate that tab manager exists
if (!tabManager) {
console.warn('[Sidebar] TabManager not available');
return;
}
logSidebarDebug(`[Sidebar] Received cross-tab switch request for '${tabId}' from ${source}, mapped to '${tabKey}'`);
// Switch to the requested tab using TabManager
try {
tabManager.switchTab(tabKey);
bus.publish(MessageTypes.TAB_SWITCHED, { tabId: tabKey, source: 'sidebar' });
logSidebarDebug(`[Sidebar] Successfully switched to tab: ${tabKey}`);
} catch (error) {
console.error(`[Sidebar] Error switching to tab '${tabKey}':`, error);
}
});
lifecycle.add(() => {
if (crossTabUnsubscribe) {
try {
crossTabUnsubscribe();
} catch (err) {
console.warn('[Sidebar] Error unsubscribing from cross-tab messaging:', err);
}
crossTabUnsubscribe = null;
}
});
logSidebarDebug('[Sidebar] Subscribed to cross-tab switch requests');
}).catch(err => {
console.warn('[Sidebar] Failed to load cross-tab messaging:', err);
});
}, 100);
console.log('[Sidebar] createCacheSidebar completed and sidebar is ready');
}
/**
* Reload the sidebar with updated settings
* This is called when tab visibility settings change
*/
export async function reloadCacheSidebar() {
if (!currentSidebarElement) {
console.warn('No sidebar element reference found for reload');
return;
}
logSidebarDebug('Reloading sidebar with updated settings...');
// Clear the current sidebar content
currentSidebarElement.innerHTML = '';
// Recreate the sidebar
await createCacheSidebar(currentSidebarElement);
logSidebarDebug('Sidebar reloaded successfully');
}