diff --git a/webapp/src/utils/tabHandling.ts b/webapp/src/utils/tabHandling.ts index 300de1f2..bfd35ac7 100644 --- a/webapp/src/utils/tabHandling.ts +++ b/webapp/src/utils/tabHandling.ts @@ -61,7 +61,6 @@ function getActiveTab(containerId: string): string | undefined { return tabStates.get(containerId)?.activeTab; } -// Add function to set active tab state export const setActiveTabState = (containerId: string, tabId: string): void => { tabStates.set(containerId, {containerId, activeTab: tabId}); }; @@ -84,12 +83,6 @@ export function saveTabState(containerId: string, activeTab: string) { diagnostics.saveCount++; currentStateVersion++; tabStateVersions.set(containerId, currentStateVersion); - // console.trace(`Saving tab state #${diagnostics.saveCount}:`, { - // containerId, - // activeTab, - // existingStates: tabStates.size, - // version: currentStateVersion - // }); const state = {containerId, activeTab}; tabStates.set(containerId, state); trackTabStateHistory(containerId, activeTab); @@ -152,99 +145,168 @@ export function setActiveTab(button: Element, container: Element) { const previousTab = getActiveTab(container.id); const forTab = button.getAttribute('data-for-tab'); if (!forTab) return; - if (VERBOSE_LOGGING) console.trace(`Changing tab ${previousTab} -> ${forTab}`, { + console.debug('[TabSystem] Tab Change Initiated', { + operation: 'setActiveTab', tab: forTab, previousTab: previousTab, button: button, - containerId: container.id - }) + containerId: container.id, + timestamp: new Date().toISOString(), + stack: new Error().stack, + buttonClasses: button.classList.toString(), + containerChildren: container.children.length, + navigationTiming: performance.getEntriesByType('navigation')[0], + documentReadyState: document.readyState + }); + setActiveTabState(container.id, forTab); saveTabState(container.id, forTab); - // First try to find direct .tabs child, then look for anchor-wrapped tabs - const tabsContainer = container.querySelector(':scope > .tabs') || - container.querySelector(':scope > a > .tabs'); - if (tabsContainer) { - tabsContainer.querySelectorAll(':scope > .tab-button').forEach(btn => { - if (btn.getAttribute('data-for-tab') === forTab) { - btn.classList.add('active'); - } else { - btn.classList.remove('active'); - } + + container.querySelectorAll(':scope > .tabs > .tab-button').forEach(btn => { + const prevState = btn.classList.contains('active'); + if (btn.getAttribute('data-for-tab') === forTab) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + console.debug('[TabSystem] Button State Change', { + operation: 'updateButtonState', + buttonId: btn.id, + forTab: btn.getAttribute('data-for-tab'), + previousState: prevState, + newState: btn.classList.contains('active'), + timestamp: new Date().toISOString() }); - } - // Query both direct children and anchor-wrapped tab content - const tabContents = [ - ...container.querySelectorAll(':scope > .tab-content'), - ...container.querySelectorAll(':scope > a > .tab-content') - ]; - tabContents.forEach(content => { + }); + + container.querySelectorAll(':scope > .tab-content').forEach(content => { + const prevDisplay = (content as HTMLElement).style.display; if (content.getAttribute('data-tab') === forTab) { content.classList.add('active'); (content as HTMLElement).style.display = 'block'; + console.debug('[TabSystem] Tab Content State Change', { + operation: 'activateContent', + tab: forTab, + containerId: container.id, + contentId: content.id, + timestamp: new Date().toISOString(), + previousDisplay: prevDisplay, + newDisplay: 'block', + contentChildren: content.children.length, + contentSize: { + width: (content as HTMLElement).offsetWidth, + height: (content as HTMLElement).offsetHeight + }, + visibilityState: document.visibilityState + }); updateNestedTabs(content as HTMLElement); } else { content.classList.remove('active'); (content as HTMLElement).style.display = 'none'; + console.debug('[TabSystem] Tab Content Deactivated', { + operation: 'deactivateContent', + tab: content.getAttribute('data-tab'), + containerId: container.id, + contentId: content.id, + previousDisplay: prevDisplay, + newDisplay: 'none', + timestamp: new Date().toISOString() + }); if ((content as any)._contentObserver) { + console.debug('[TabSystem] Disconnecting Content Observer', { + operation: 'disconnectObserver', + tab: content.getAttribute('data-tab'), + containerId: container.id, + contentId: content.id, + timestamp: new Date().toISOString(), + observerStatus: 'disconnecting' + }); (content as any)._contentObserver.disconnect(); delete (content as any)._contentObserver; } } }); - if (VERBOSE_LOGGING) console.trace(`${'Updated active tab'}`, { + console.debug('[TabSystem] Tab Change Completed', { + operation: 'setActiveTab', containerId: container.id, - activeTab: forTab - }) + activeTab: forTab, + previousTab: previousTab, + timestamp: new Date().toISOString(), + performance: { + timing: performance.now(), + navigation: performance.getEntriesByType('navigation')[0], + resourceTiming: performance.getEntriesByType('resource'), + }, + documentState: { + readyState: document.readyState, + visibilityState: document.visibilityState, + activeElement: document.activeElement?.tagName + }, + browserInfo: { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language + } + }); } function restoreTabState(container: Element) { try { diagnostics.restoreCount++; const containerId = container.id; - // const storedVersion = tabStateVersions.get(containerId) || 0; - // console.debug(`Attempting to restore tab state #${diagnostics.restoreCount}:`, { - // containerId, - // storedState: tabStates.get(containerId), - // allStates: Array.from(tabStates.entries()), - // version: storedVersion - // }); + console.debug(`[TabSystem] Restoring Tab State`, { + operation: 'restoreTabState', + containerId: containerId, + timestamp: new Date().toISOString(), + diagnostics: { ...diagnostics } + }); + const savedTab = getActiveTab(containerId) || tabStates.get(containerId)?.activeTab; if (savedTab) { - // First try direct child tabs, then anchor-wrapped tabs - const tabsContainer = container.querySelector(':scope > .tabs') || - container.querySelector(':scope > a > .tabs'); + const tabsContainer = container.querySelector(':scope > .tabs'); const button = tabsContainer?.querySelector(`:scope > .tab-button[data-for-tab="${savedTab}"]`) as HTMLElement; if (button) { + console.debug(`[TabSystem] Found Saved Tab`, { + operation: 'restoreTabState', + containerId: containerId, + savedTab: savedTab, + buttonFound: true, + stack: new Error().stack + }); setActiveTab(button, container); diagnostics.restoreSuccess++; - // console.debug(`Successfully restored tab state:`, { - // containerId, - // activeTab: savedTab, - // successCount: diagnostics.restoreSuccess - // }); } else { diagnostics.restoreFail++; - console.warn(`No matching tab button found for tab:`, { + console.warn(`[TabSystem] Tab Restore Failed - No Matching Button`, { + operation: 'restoreTabState', containerId, savedTab, - failCount: diagnostics.restoreFail + failCount: diagnostics.restoreFail, + stack: new Error().stack }); } } else { diagnostics.restoreFail++; + console.debug(`[TabSystem] No Saved Tab Found - Using First Button`, { + operation: 'restoreTabState', + containerId: containerId, + fallback: 'firstButton', + diagnostics: { ...diagnostics } + }); const firstButton = container.querySelector('.tab-button') as HTMLElement; if (firstButton) { - // console.warn(`No saved state found for container:`, { - // containerId, - // failCount: diagnostics.restoreFail, - // firstButton: firstButton - // }); setActiveTab(firstButton, container); } } } catch (error) { - console.warn(`Failed to restore tab state:`, error); + console.error(`[TabSystem] Critical Restore Failure`, { + operation: 'restoreTabState', + error: error, + stack: error instanceof Error ? error.stack : new Error().stack, + diagnostics: { ...diagnostics }, + timestamp: new Date().toISOString() + }); diagnostics.restoreFail++; } } @@ -279,9 +341,9 @@ export const updateTabs = debounce(() => { processed.add(container.id); setupTabContainer(container); - const activeTab = getActiveTab(container.id) || - currentStates.get(container.id)?.activeTab || - container.querySelector(':scope > .tabs > .tab-button.active')?.getAttribute('data-for-tab'); + const activeTab = getActiveTab(container.id) || + currentStates.get(container.id)?.activeTab || + container.querySelector(':scope > .tabs > .tab-button.active')?.getAttribute('data-for-tab'); if (activeTab) { const state: TabState = { containerId: container.id, @@ -290,50 +352,31 @@ export const updateTabs = debounce(() => { tabStates.set(container.id, state); restoreTabState(container); } else { - // Try to activate first tab if none active const firstButton = container.querySelector(':scope > .tabs > .tab-button'); if (firstButton instanceof HTMLElement) { const firstTabId = firstButton.getAttribute('data-for-tab'); if (firstTabId) { setActiveTab(firstButton, container); } + } else { + console.warn(`No active tab found for container`, { + containerId: container.id + }); } - console.warn(`No active tab found for container`, { - containerId: container.id - }); } }); document.querySelectorAll('.tabs-container').forEach((container: Element) => { if (container instanceof HTMLElement) { if (processed.has(container.id)) { - return; + return; } processed.add(container.id); let activeTab: string | undefined = getActiveTab(container.id); if (!activeTab) { - // console.warn(`No active tab found`, { - // containerId: container.id, - // action: 'checking active button' - // }); - // Update active button selector - const tabsContainer = container.querySelector(':scope > .tabs, :scope > a > .tabs'); - const activeButton = tabsContainer?.querySelector(':scope > .tab-button.active'); + const activeButton = container.querySelector(':scope > .tabs > .tab-button.active'); if (activeButton) { activeTab = activeButton.getAttribute('data-for-tab') || ''; - } - } - if (!activeTab) { - // console.warn(`No active button found`, { - // containerId: container.id, - // action: 'defaulting to first tab' - // }); - // Update first button selector - const tabsContainer = container.querySelector(':scope > .tabs, :scope > a > .tabs'); - const firstButton = tabsContainer?.querySelector(':scope > .tab-button') as HTMLElement; - if (firstButton) { - activeTab = firstButton.getAttribute('data-for-tab') || ''; - setActiveTab(firstButton, container); } else { console.warn(`No tab buttons found`, { containerId: container.id, @@ -342,37 +385,32 @@ export const updateTabs = debounce(() => { } } + console.debug(`Updating tabs`, { + containerId: container.id, + activeTab: activeTab + }); + let activeCount = 0; let inactiveCount = 0; // Handle both direct and anchor-wrapped tabs - const tabsContainer = container.querySelector(':scope > .tabs') || - container.querySelector(':scope > a > .tabs'); - if (tabsContainer) { - tabsContainer.querySelectorAll(':scope > .tab-button').forEach(button => { - if (button.getAttribute('data-for-tab') === activeTab) { - button.classList.add('active'); - activeCount++; - } else { - button.classList.remove('active'); - inactiveCount++; - } - }); - // Also update tab content visibility - // Query both direct children and anchor-wrapped tab content - const tabContents = [ - ...container.querySelectorAll(':scope > .tab-content'), - ...container.querySelectorAll(':scope > a > .tab-content') - ]; - tabContents.forEach(content => { - if (content.getAttribute('data-tab') === activeTab) { - content.classList.add('active'); - (content as HTMLElement).style.display = 'block'; - } else { - content.classList.remove('active'); - (content as HTMLElement).style.display = 'none'; - } - }); - } + container.querySelectorAll(':scope > .tabs > .tab-button').forEach(button => { + if (button.getAttribute('data-for-tab') === activeTab) { + button.classList.add('active'); + activeCount++; + } else { + button.classList.remove('active'); + inactiveCount++; + } + }); + container.querySelectorAll(':scope > .tab-content').forEach(content => { + if (content.getAttribute('data-tab') === activeTab) { + content.classList.add('active'); + (content as HTMLElement).style.display = 'block'; + } else { + content.classList.remove('active'); + (content as HTMLElement).style.display = 'none'; + } + }); if (VERBOSE_LOGGING) console.debug(`${`Synchronized ${activeCount + inactiveCount} buttons`}`, { activeTab, activeCount, @@ -410,7 +448,7 @@ function setupTabContainer(container: Element) { }) container.addEventListener('click', (event: Event) => { const button = (event.target as HTMLElement).closest('.tab-button'); - if (button && (container.contains(button) || container.querySelector('a')?.contains(button))) { + if (button && (container.contains(button))) { setActiveTab(button, container); event.stopPropagation(); event.preventDefault(); // Prevent anchor tag navigation diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt index 2a67eaf9..7726d18f 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt @@ -6,6 +6,7 @@ import java.util.* open class TabbedDisplay( val task: SessionTask, val tabs: MutableList> = mutableListOf(), + val additionalClasses: String = "" ) { var selectedTab: Int = 0 @@ -17,7 +18,7 @@ open class TabbedDisplay( val tabId = UUID.randomUUID() private fun render() = if (tabs.isEmpty()) "
" else { """ -
+
${renderTabButtons()} ${ tabs.toTypedArray().withIndex().joinToString("\n") @@ -32,8 +33,7 @@ open class TabbedDisplay( task.add(render())!! } - protected open fun renderTabButtons() = """ -
${ + protected open fun renderTabButtons() = """
${ tabs.toTypedArray().withIndex().joinToString("\n") { (idx, pair) -> if (idx == selectedTab) { """""" @@ -41,15 +41,13 @@ open class TabbedDisplay( """""" } } - }
-""" + }
""" - protected open fun renderContentTab(t: Pair, idx: Int) = """ -
"" - } + protected open fun renderContentTab(t: Pair, idx: Int) = """
setOf("active") + else -> emptySet() + }).filter { it.isNotEmpty() }.joinToString(" ") }" data-tab="$idx">${t.second}
""" diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/CodingAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/CodingAgent.kt index e400e97e..e7336721 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/CodingAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/CodingAgent.kt @@ -171,7 +171,7 @@ open class CodingAgent( |${if (!canPlay) "" else playButton(task, request, response, formText) { formHandle!! }} |
|${reviseMsg(task, request, response, formText) { formHandle!! }} - """.trimMargin(), className = "reply-message" + """.trimMargin(), additionalClasses = "reply-message" ) formText.append(formHandle.toString()) formHandle.toString() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/ShellToolAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/ShellToolAgent.kt index 11ac73fe..8802fb5a 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/ShellToolAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/code/ShellToolAgent.kt @@ -89,7 +89,7 @@ abstract class ShellToolAgent( |${createToolButton(task, request, response, formText) { formHandle!! }} |
|${super.reviseMsg(task, request, response, formText) { formHandle!! }} - """.trimMargin(), className = "reply-message" + """.trimMargin(), additionalClasses = "reply-message" ) formText.append(formHandle.toString()) formHandle.toString() @@ -460,7 +460,7 @@ abstract class ShellToolAgent( } } } - """.trimMargin(), className = "reply-message" + """.trimMargin(), additionalClasses = "reply-message" ) formText.append(formHandle.toString()) formHandle.toString() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/AutoPlanChatApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/AutoPlanChatApp.kt index 35e9da03..a10067ee 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/AutoPlanChatApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/AutoPlanChatApp.kt @@ -233,7 +233,7 @@ open class AutoPlanChatApp( task.verbose("API log: $this") } } - val tabbedDisplay = TabbedDisplay(task) + val tabbedDisplay = TabbedDisplay(task, additionalClasses = "iteration") ui.newTask(false).apply { tabbedDisplay["Inputs"] = placeholder header("Project Info") diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt index 09f01af4..c8141b35 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt @@ -102,7 +102,7 @@ Note: This task is for running simple and safe commands. Avoid executing command ${acceptButton(response)}
${super.reviseMsg(task, request, response, formText) { formHandle!! }} - """.trimMargin(), className = "reply-message" + """.trimMargin(), additionalClasses = "reply-message" ) formText.append(formHandle.toString()) formHandle.toString() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt index 603883ef..a3e2dede 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt @@ -56,9 +56,9 @@ abstract class SessionTask( showSpinner: Boolean = true, @Description("The html tag to wrap the message in (default: div)") tag: String = "div", - @Description("The css class to apply to the message (default: response-message)") - className: String = "response-message" - ) = append("""<$tag class="$className">$message""", showSpinner) + @Description("Additional css class(es) to apply to the message") + additionalClasses: String = "" + ) = append("""<$tag class="${(setOf(additionalClasses.split(" ")) + setOf("response-message")).joinToString(" ")}">$message""", showSpinner) @Description("Adds a hideable message to the task output.") fun hideable( @@ -69,8 +69,8 @@ abstract class SessionTask( showSpinner: Boolean = true, @Description("The html tag to wrap the message in (default: div)") tag: String = "div", - @Description("The css class to apply to the message (default: response-message)") - className: String = "response-message" + @Description("Additional css class(es) to apply to the message") + additionalClasses: String = "" ): StringBuilder? { var windowBuffer: StringBuilder? = null val closeButton = """${ @@ -79,7 +79,7 @@ abstract class SessionTask( send() } }""" - windowBuffer = append("""<$tag class="$className">$closeButton$message""", showSpinner) + windowBuffer = append("""<$tag class="${(additionalClasses.split(" ").toSet() + setOf("response-message")).joinToString(" ")}">$closeButton$message""", showSpinner) return windowBuffer } @@ -101,8 +101,8 @@ abstract class SessionTask( showSpinner: Boolean = true, @Description("The html tag to wrap the message in (default: div)") tag: String = "div", - classname: String = "response-header" - ) = add(message, showSpinner, tag, classname) + additionalClasses: String = "" + ) = add(message, showSpinner, tag, additionalClasses) @Description("Adds a verbose message to the task output; verbose messages are hidden by default.") fun verbose( @@ -177,9 +177,9 @@ abstract class SessionTask( message: String = "", @Description("The html tag to wrap the message in (default: div)") tag: String = "div", - @Description("The css class to apply to the message (default: response-message)") - className: String = "response-message" - ) = append(if (message.isNotBlank()) """<$tag class="$className">$message""" else "", false) + @Description("Additional css class(es) to apply to the message") + additionalClasses: String = "" + ) = append(if (message.isNotBlank()) """<$tag class="${(additionalClasses.split(" ").toSet() + setOf("response-message")).joinToString(" ")}">$message""" else "", false) @Description("Displays an image to the task output.") fun image(