fix(chat): enable in-place subtask navigation in embedded session-chat iframe (#2138)
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
import { afterAll, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||
import type { Theme } from '@/types/theme';
|
||||
import { buildEmbeddedSessionChatURL, getOrCreateEmbeddedSessionChatURL, type EmbeddedSessionChatURLCacheEntry } from './contextPanelEmbeddedChat';
|
||||
import {
|
||||
buildEmbeddedSessionChatURL,
|
||||
getOrCreateEmbeddedSessionChatURL,
|
||||
getEmbeddedSessionChatOriginSessionId,
|
||||
isEmbeddedSessionChat,
|
||||
resetEmbeddedSessionChatCache,
|
||||
type EmbeddedSessionChatURLCacheEntry,
|
||||
} from './contextPanelEmbeddedChat';
|
||||
|
||||
const originalWindow = globalThis.window;
|
||||
|
||||
@@ -32,6 +39,7 @@ const makeTheme = (id: string, variant: 'light' | 'dark'): Theme => ({
|
||||
|
||||
beforeEach(() => {
|
||||
installWindowLocation();
|
||||
resetEmbeddedSessionChatCache();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -99,3 +107,69 @@ describe('embedded session chat URL', () => {
|
||||
expect(new URL(readOnly).searchParams.get('readOnly')).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmbeddedSessionChat', () => {
|
||||
test('is true only for the session-chat panel search param', () => {
|
||||
installWindowLocation('http://127.0.0.1:5173/app?ocPanel=session-chat&sessionId=ses_1');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(isEmbeddedSessionChat()).toBe(true);
|
||||
|
||||
installWindowLocation('http://127.0.0.1:5173/app?sessionId=ses_1');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(isEmbeddedSessionChat()).toBe(false);
|
||||
|
||||
installWindowLocation('http://127.0.0.1:5173/app');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(isEmbeddedSessionChat()).toBe(false);
|
||||
});
|
||||
|
||||
test('caches the first result so URL rewrites cannot flip it (mirrors VS Code stable global)', () => {
|
||||
// VS Code detects its webview via the stable `window.__VSCODE_CONFIG__`
|
||||
// global — it never changes. The embedded iframe's identity is equally
|
||||
// fixed at mount (the parent builds the src); caching the first read
|
||||
// makes detection just as stable, surviving any URL rewrite.
|
||||
installWindowLocation('http://127.0.0.1:5173/app?ocPanel=session-chat&sessionId=ses_1');
|
||||
resetEmbeddedSessionChatCache();
|
||||
|
||||
// First read: caches true.
|
||||
expect(isEmbeddedSessionChat()).toBe(true);
|
||||
|
||||
// Even if the URL were rewritten, the cached value stays true.
|
||||
installWindowLocation('http://127.0.0.1:5173/app?session=ses_grandchild');
|
||||
expect(isEmbeddedSessionChat()).toBe(true);
|
||||
|
||||
// Still true after another rewrite.
|
||||
installWindowLocation('http://127.0.0.1:5173/app');
|
||||
expect(isEmbeddedSessionChat()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmbeddedSessionChatOriginSessionId', () => {
|
||||
test('returns the URL sessionId when embedded', () => {
|
||||
installWindowLocation('http://127.0.0.1:5173/app?ocPanel=session-chat&sessionId=ses_child&directory=%2Frepo');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(getEmbeddedSessionChatOriginSessionId()).toBe('ses_child');
|
||||
});
|
||||
|
||||
test('returns null outside the embedded iframe', () => {
|
||||
installWindowLocation('http://127.0.0.1:5173/app?session=ses_main');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(getEmbeddedSessionChatOriginSessionId()).toBeNull();
|
||||
|
||||
installWindowLocation('http://127.0.0.1:5173/app? sessionId=ses_orphan');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(getEmbeddedSessionChatOriginSessionId()).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when embedded URL is missing sessionId param', () => {
|
||||
installWindowLocation('http://127.0.0.1:5173/app?ocPanel=session-chat&directory=%2Frepo');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(getEmbeddedSessionChatOriginSessionId()).toBeNull();
|
||||
});
|
||||
|
||||
test('trims whitespace', () => {
|
||||
installWindowLocation('http://127.0.0.1:5173/app?ocPanel=session-chat&sessionId=%20%20ses_child%20%20');
|
||||
resetEmbeddedSessionChatCache();
|
||||
expect(getEmbeddedSessionChatOriginSessionId()).toBe('ses_child');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,3 +70,60 @@ export const getOrCreateEmbeddedSessionChatURL = (
|
||||
cache.set(tabID, { signature, src });
|
||||
return src;
|
||||
};
|
||||
|
||||
/**
|
||||
* True when the current document is the embedded session-chat iframe
|
||||
* (`?ocPanel=session-chat`). Used to distinguish the embedded iframe from
|
||||
* the main app so callers can route behavior accordingly (e.g. in-place
|
||||
* subtask navigation instead of opening a new side-panel tab, or skipping
|
||||
* URL rewrites that would strip the iframe's identity params).
|
||||
*
|
||||
* Cached on first call (per JS realm): an iframe's embedded-ness is fixed
|
||||
* at mount by the parent and cannot change during its lifetime — a parent
|
||||
* src swap is a full browser reload, starting a fresh realm.
|
||||
*/
|
||||
let embeddedSessionChatCached: boolean | null = null;
|
||||
|
||||
export const isEmbeddedSessionChat = (): boolean => {
|
||||
if (embeddedSessionChatCached !== null) {
|
||||
return embeddedSessionChatCached;
|
||||
}
|
||||
if (typeof window === 'undefined') {
|
||||
embeddedSessionChatCached = false;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
embeddedSessionChatCached =
|
||||
new URLSearchParams(window.location.search).get('ocPanel') === 'session-chat';
|
||||
return embeddedSessionChatCached;
|
||||
} catch {
|
||||
embeddedSessionChatCached = false;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the module-level cache. Intended for tests that simulate different
|
||||
* JS realms by swapping `window.location` in the same process.
|
||||
*/
|
||||
export const resetEmbeddedSessionChatCache = (): void => {
|
||||
embeddedSessionChatCached = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The session ID recorded in the embedded iframe's URL
|
||||
* (`?ocPanel=session-chat&sessionId=…`), i.e. the session the panel was
|
||||
* opened to show. Returns `null` outside the embedded iframe or when the
|
||||
* URL is malformed.
|
||||
*/
|
||||
export const getEmbeddedSessionChatOriginSessionId = (): string | null => {
|
||||
if (!isEmbeddedSessionChat()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const sid = new URLSearchParams(window.location.search).get('sessionId');
|
||||
return sid && sid.trim().length > 0 ? sid.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user