fix(ui): restore context panel session chats

This commit is contained in:
Bohdan Triapitsyn
2026-08-14 19:49:17 +03:00
parent 5fbd1c0962
commit 37e12e272b
6 changed files with 103 additions and 23 deletions
+9 -5
View File
@@ -54,7 +54,11 @@ import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import { useI18n } from '@/lib/i18n';
import { applyMobileKeyboardMode } from '@/lib/mobileKeyboardMode';
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
import {
EMBEDDED_VISIBILITY_UPDATE,
isEmbeddedSessionChat,
requestEmbeddedSessionVisibility,
} from '@/components/layout/contextPanelEmbeddedChat';
import { SyncAppEffects } from '@/apps/AppEffects';
import { resetAppForRuntimeEndpointChange } from '@/apps/runtimeEndpointReset';
import { useAppFontEffects } from '@/apps/useAppFontEffects';
@@ -538,17 +542,16 @@ function App({ apis }: AppProps) {
}
const applyVisibility = (payload?: EmbeddedVisibilityPayload) => {
const nextVisible = payload?.visible === true;
setIsEmbeddedVisible(nextVisible);
setIsEmbeddedVisible(payload?.visible === true);
};
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) {
if (event.origin !== window.location.origin || event.source !== window.parent) {
return;
}
const data = event.data as { type?: unknown; payload?: EmbeddedVisibilityPayload };
if (data?.type !== 'openchamber:embedded-visibility') {
if (data?.type !== EMBEDDED_VISIBILITY_UPDATE) {
return;
}
@@ -561,6 +564,7 @@ function App({ apis }: AppProps) {
scopedWindow.__openchamberSetEmbeddedVisibility = applyVisibility;
window.addEventListener('message', handleMessage);
requestEmbeddedSessionVisibility();
return () => {
window.removeEventListener('message', handleMessage);
@@ -38,6 +38,8 @@ import { Icon } from "@/components/icon/Icon";
import {
EMBEDDED_RUNTIME_BOOTSTRAP_REQUEST,
EMBEDDED_RUNTIME_BOOTSTRAP_RESPONSE,
EMBEDDED_VISIBILITY_REQUEST,
EMBEDDED_VISIBILITY_UPDATE,
getActiveEmbeddedSessionChatTab,
getOrCreateEmbeddedSessionChatURL,
type EmbeddedSessionChatURLCacheEntry,
@@ -803,27 +805,34 @@ export const ContextPanel: React.FC = () => {
}
}, [allowPromptingSubagentSessions]);
const postEmbeddedVisibilityToChat = React.useCallback((
tabID: string,
frame: HTMLIFrameElement,
targetOrigin: string,
) => {
const frameWindow = frame.contentWindow;
if (!frameWindow) {
return;
}
frameWindow.postMessage(
{
type: EMBEDDED_VISIBILITY_UPDATE,
payload: { visible: activeChatTabID === tabID },
},
targetOrigin,
);
}, [activeChatTabID]);
const postEmbeddedVisibilityToChats = React.useCallback(() => {
if (typeof window === 'undefined') {
return;
}
for (const [tabID, frame] of chatFrameRefs.current.entries()) {
const frameWindow = frame.contentWindow;
if (!frameWindow) {
continue;
}
const payload = { visible: activeChatTabID === tabID };
frameWindow.postMessage(
{
type: 'openchamber:embedded-visibility',
payload,
},
window.location.origin,
);
postEmbeddedVisibilityToChat(tabID, frame, window.location.origin);
}
}, [activeChatTabID]);
}, [postEmbeddedVisibilityToChat]);
React.useEffect(() => {
if (typeof window === 'undefined') {
@@ -835,13 +844,18 @@ export const ContextPanel: React.FC = () => {
return;
}
const isKnownChatFrame = Array.from(chatFrameRefs.current.values())
.some((frame) => frame.contentWindow === event.source);
if (!isKnownChatFrame) {
const sourceChatFrame = Array.from(chatFrameRefs.current.entries())
.find(([, frame]) => frame.contentWindow === event.source);
if (!sourceChatFrame) {
return;
}
const data = event.data as { type?: unknown; requestId?: unknown };
if (data?.type === EMBEDDED_VISIBILITY_REQUEST) {
const [tabID, frame] = sourceChatFrame;
postEmbeddedVisibilityToChat(tabID, frame, event.origin);
return;
}
if (data?.type === EMBEDDED_RUNTIME_BOOTSTRAP_REQUEST) {
if (typeof data.requestId !== 'string' || !data.requestId) return;
const runtimeKey = getRuntimeKey();
@@ -882,7 +896,7 @@ export const ContextPanel: React.FC = () => {
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [postChatSettingsSyncToEmbeddedChat, postThemeSyncToEmbeddedChat, setThemeMode, themeMode]);
}, [postChatSettingsSyncToEmbeddedChat, postEmbeddedVisibilityToChat, postThemeSyncToEmbeddedChat, setThemeMode, themeMode]);
React.useLayoutEffect(() => {
const hasAnyChatTab = tabs.some((tab) => tab.mode === 'chat');
@@ -19,6 +19,7 @@ import {
} from '../contextPanelEmbeddedChat';
const __dirname = dirname(fileURLToPath(import.meta.url));
const appSource = readFileSync(join(__dirname, '..', '..', '..', 'App.tsx'), 'utf-8');
const contextPanelSource = readFileSync(join(__dirname, '..', 'ContextPanel.tsx'), 'utf-8');
type FixtureTab = {
@@ -117,6 +118,7 @@ describe('issue #2815 active-only chat iframe source guard', () => {
expect(block).toContain('<iframe');
expect(block).toContain('key={activeChatTab.id}');
expect(block).toContain('src={activeChatSrc}');
expect(block).toContain('postEmbeddedVisibilityToChats();');
expect(block).not.toContain("'block' : 'hidden'");
});
@@ -128,6 +130,32 @@ describe('issue #2815 active-only chat iframe source guard', () => {
"const activeChatSessionID = isOpen && activeTab?.mode === 'chat'",
);
});
test('answers the mounted iframe visibility handshake from the active tab', () => {
expect(contextPanelSource).toContain('data?.type === EMBEDDED_VISIBILITY_REQUEST');
expect(contextPanelSource).toContain('frame.contentWindow === event.source');
expect(contextPanelSource).toContain('payload: { visible: activeChatTabID === tabID }');
});
test('requests authoritative visibility after installing the iframe listener', () => {
const effectStart = appSource.indexOf('const applyVisibility = (payload?: EmbeddedVisibilityPayload) => {');
const listenerIndex = appSource.indexOf("window.addEventListener('message', handleMessage);", effectStart);
const requestIndex = appSource.indexOf('requestEmbeddedSessionVisibility();', effectStart);
expect(effectStart).toBeGreaterThan(-1);
expect(listenerIndex).toBeGreaterThan(effectStart);
expect(requestIndex).toBeGreaterThan(listenerIndex);
});
test('gates embedded chat subscriptions and background work on visibility', () => {
expect(appSource).toContain(
'const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;',
);
expect(appSource).toContain('active={embeddedBackgroundWorkEnabled}');
expect(appSource).toContain(
'useWebNotificationStream({ enabled: embeddedBackgroundWorkEnabled });',
);
});
});
describe('issue #2815 persisted scenario', () => {
@@ -5,11 +5,13 @@ import {
buildEmbeddedSessionChatURL,
EMBEDDED_RUNTIME_BOOTSTRAP_REQUEST,
EMBEDDED_RUNTIME_BOOTSTRAP_RESPONSE,
EMBEDDED_VISIBILITY_REQUEST,
getOrCreateEmbeddedSessionChatURL,
getActiveEmbeddedSessionChatTab,
getEmbeddedSessionChatOriginSessionId,
isEmbeddedSessionChat,
requestEmbeddedSessionRuntimeBootstrap,
requestEmbeddedSessionVisibility,
resetEmbeddedSessionChatCache,
type EmbeddedSessionChatURLCacheEntry,
} from './contextPanelEmbeddedChat';
@@ -171,6 +173,22 @@ describe('active embedded session chat', () => {
expect(getActiveEmbeddedSessionChatTab(tabs, null)).toBeNull();
expect(getActiveEmbeddedSessionChatTab(tabs, 'missing-chat')).toBeNull();
});
test('requests authoritative visibility from the same-origin parent', () => {
installWindowLocation('http://127.0.0.1:5173/app?ocPanel=session-chat&sessionId=ses_1');
resetEmbeddedSessionChatCache();
const calls: Array<{ message: unknown; origin: string }> = [];
(window as unknown as { parent: { postMessage: (message: unknown, origin: string) => void } }).parent = {
postMessage: (message, origin) => calls.push({ message, origin }),
};
requestEmbeddedSessionVisibility();
expect(calls).toEqual([{
message: { type: EMBEDDED_VISIBILITY_REQUEST },
origin: 'http://127.0.0.1:5173',
}]);
});
});
describe('isEmbeddedSessionChat', () => {
@@ -28,6 +28,8 @@ export type EmbeddedSessionRuntimeBootstrap = {
export const EMBEDDED_RUNTIME_BOOTSTRAP_REQUEST = 'openchamber:embedded-runtime-bootstrap-request';
export const EMBEDDED_RUNTIME_BOOTSTRAP_RESPONSE = 'openchamber:embedded-runtime-bootstrap-response';
export const EMBEDDED_VISIBILITY_REQUEST = 'openchamber:embedded-visibility-request';
export const EMBEDDED_VISIBILITY_UPDATE = 'openchamber:embedded-visibility';
const EMBEDDED_RUNTIME_BOOTSTRAP_TIMEOUT_MS = 5_000;
const EMBEDDED_RUNTIME_BOOTSTRAP_RETRY_MS = 100;
@@ -104,6 +106,13 @@ export const requestEmbeddedSessionRuntimeBootstrap = (): Promise<EmbeddedSessio
});
};
export const requestEmbeddedSessionVisibility = (): void => {
if (!isEmbeddedSessionChat() || typeof window === 'undefined' || !window.parent || window.parent === window) {
return;
}
window.parent.postMessage({ type: EMBEDDED_VISIBILITY_REQUEST }, window.location.origin);
};
const buildEmbeddedSessionChatURLSignature = (
sessionID: string,
directory: string | null,
+7
View File
@@ -40,6 +40,13 @@ Examples:
These stores coordinate visible app state, navigation, selected tabs, dialogs, and lightweight feature flags.
Context-panel session chats mount only the active chat iframe. After installing
its message listener, the iframe requests its authoritative visibility from the
parent. The parent accepts requests only from a currently mounted chat frame and
answers from the current active tab. Do not rely only on a parent `onLoad`
notification: it can arrive before the iframe listener exists and leave a
visible chat with background work and message subscriptions disabled.
### Session / project coordination stores
Examples: