fix(ui): restore context panel session chats
This commit is contained in:
@@ -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');
|
||||
|
||||
+28
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user