From 454119ac25d41c420adaf53c30609c6d675750d4 Mon Sep 17 00:00:00 2001 From: Andrea V <1577639+karimodm@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:11:25 +0200 Subject: [PATCH] fix(ui): mount only the active session chat iframe (#2816) * fix(ui): mount only the active session chat iframe * repro(ui): mount-all-persisted session-chat iframes (#2815) Adds a regression-style reproduction for issue #2815: ContextPanel renders one full-application iframe for every chat tab (inactive tabs are only hidden via the Tailwind 'hidden' class, never unmounted), so a reload restores all persisted session-chat tabs from the ui-store and mounts N embedded OpenChamber apps in one browser tab. The test reads the real ContextPanel.tsx render block, drives the real useUIStore with the issue's persisted scenario (11 tabs, 8 read-only session-chat tabs), and models the render block with the real buildEmbeddedSessionChatURL helper, showing 8 live src iframes (7 hidden but loaded). * test(ui): adapt issue 2815 reproduction for active chat * fix(ui): unmount session chat when panel closes --------- Co-authored-by: ChangeHow <23733347+ChangeHow@users.noreply.github.com> --- .../ui/src/components/layout/ContextPanel.tsx | 73 +++---- ...815-sessionChatIframesMountAllTabs.test.ts | 183 ++++++++++++++++++ .../layout/contextPanelEmbeddedChat.test.ts | 17 ++ .../layout/contextPanelEmbeddedChat.ts | 11 ++ packages/ui/src/lib/surfaces/DOCUMENTATION.md | 12 +- 5 files changed, 250 insertions(+), 46 deletions(-) create mode 100644 packages/ui/src/components/layout/__tests__/issue-2815-sessionChatIframesMountAllTabs.test.ts diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index a8d5b159..6b173209 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -45,6 +45,7 @@ import { invokeDesktopCommand } from '@/lib/desktopNative'; import { EMBEDDED_RUNTIME_BOOTSTRAP_REQUEST, EMBEDDED_RUNTIME_BOOTSTRAP_RESPONSE, + getActiveEmbeddedSessionChatTab, getOrCreateEmbeddedSessionChatURL, type EmbeddedSessionChatURLCacheEntry, type EmbeddedSessionRuntimeBootstrap, @@ -2483,8 +2484,13 @@ export const ContextPanel: React.FC = () => { }, [activeTab, directoryKey, setSelectedFilePath]); - const activeChatTabID = activeTab?.mode === 'chat' ? activeTab.id : null; - const activeChatSessionID = activeTab?.mode === 'chat' ? getSessionIDFromDedupeKey(activeTab.dedupeKey) : null; + const chatTabs = React.useMemo( + () => tabs.filter((tab) => tab.mode === 'chat'), + [tabs], + ); + const activeChatTabID = isOpen && activeTab?.mode === 'chat' ? activeTab.id : null; + const activeChatSessionID = isOpen && activeTab?.mode === 'chat' ? getSessionIDFromDedupeKey(activeTab.dedupeKey) : null; + const activeChatTab = getActiveEmbeddedSessionChatTab(chatTabs, activeChatTabID); React.useEffect(() => { if (!isOpen || !directoryKey || !activeChatSessionID || typeof window === 'undefined') { @@ -2525,6 +2531,10 @@ export const ContextPanel: React.FC = () => { }); }, [currentTheme, darkThemeId, directoryKey, lightThemeId, themeMode]); + const activeChatSrc = activeChatTab && activeChatSessionID + ? getEmbeddedChatSrc(activeChatTab.id, activeChatSessionID, activeChatTab.readOnly) + : null; + React.useEffect(() => { const liveTabIDs = new Set(tabs.map((tab) => tab.id)); for (const tabID of chatFrameSrcByTabIDRef.current.keys()) { @@ -2721,10 +2731,6 @@ export const ContextPanel: React.FC = () => { ); - const chatTabs = React.useMemo( - () => tabs.filter((tab) => tab.mode === 'chat'), - [tabs], - ); const browserTabs = React.useMemo( () => tabs.filter((tab) => tab.mode === 'browser'), [tabs], @@ -2934,41 +2940,26 @@ export const ContextPanel: React.FC = () => { ) : null} - {chatTabs.map((tab) => { - const sessionID = getSessionIDFromDedupeKey(tab.dedupeKey); - if (!sessionID) { - return null; - } - - const src = getEmbeddedChatSrc(tab.id, sessionID, tab.readOnly); - if (!src) { - return null; - } - - return ( -