fix(chat): enable in-place subtask navigation in embedded session-chat iframe (#2138)

This commit is contained in:
Andrey Meshkov
2026-07-13 08:32:18 +03:00
committed by GitHub
parent 3362ebbfa3
commit 7888dd65b3
10 changed files with 316 additions and 26 deletions
+20 -10
View File
@@ -5,6 +5,7 @@ import { parseRoute, updateBrowserURL, hasRouteParams } from '@/lib/router';
import type { RouteState, AppRouteState } from '@/lib/router';
import type { MainTab } from '@/stores/useUIStore';
import { resolveSettingsSlug } from '@/lib/settings/metadata';
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
/**
* Check if running in VS Code webview context.
@@ -29,9 +30,18 @@ function isVSCodeContext(): boolean {
* - Web: Full bidirectional sync
* - Desktop: Full bidirectional sync
* - VS Code: State-only (no URL updates, reads initial params)
* - Embedded session-chat iframe (`?ocPanel=session-chat`): No URL updates.
* The iframe's session identity is fixed at mount (the parent builds the
* src with `sessionId`); in-place subtask navigation must NOT rewrite the
* URL, otherwise `ocPanel` (and `directory`/`readOnly`) get stripped and
* `isEmbeddedSessionChat()` starts returning false, breaking subsequent
* "Open subtask" clicks.
*/
export function useRouter(): void {
const isVSCode = React.useMemo(() => isVSCodeContext(), []);
// Captured once at mount: the iframe's embedded-ness never changes during
// its lifetime (a parent src swap is a full reload).
const isEmbeddedChat = React.useMemo(() => isEmbeddedSessionChat(), []);
// Track initialization to avoid duplicate applies
const initializedRef = React.useRef(false);
@@ -115,14 +125,14 @@ export function useRouter(): void {
*/
const syncURLFromState = React.useCallback(
(options: { replace?: boolean } = {}) => {
if (isVSCode || isApplyingRouteRef.current) {
if (isVSCode || isEmbeddedChat || isApplyingRouteRef.current) {
return;
}
const state = getCurrentAppState();
updateBrowserURL(state, options);
},
[isVSCode, getCurrentAppState]
[isVSCode, isEmbeddedChat, getCurrentAppState]
);
// Initialize: parse URL and apply route on mount
@@ -148,7 +158,7 @@ export function useRouter(): void {
// Use the parsed route values instead of an immediate store snapshot so
// deep links do not briefly normalize `?session=...` back to `/` while
// the session's directory/message bootstrap is still catching up.
if (!isVSCode) {
if (!isVSCode && !isEmbeddedChat) {
updateBrowserURL({
...getCurrentAppState(),
sessionId: route.sessionId ?? useSessionUIStore.getState().currentSessionId,
@@ -160,11 +170,11 @@ export function useRouter(): void {
};
void initializeRoute();
}, [applyRoute, getCurrentAppState, isVSCode]);
}, [applyRoute, getCurrentAppState, isVSCode, isEmbeddedChat]);
// Subscribe to session changes
React.useEffect(() => {
if (isVSCode) {
if (isVSCode || isEmbeddedChat) {
return;
}
@@ -183,11 +193,11 @@ export function useRouter(): void {
});
return unsubscribe;
}, [isVSCode, syncURLFromState]);
}, [isVSCode, isEmbeddedChat, syncURLFromState]);
// Subscribe to UI store changes (tab, settings)
React.useEffect(() => {
if (isVSCode) {
if (isVSCode || isEmbeddedChat) {
return;
}
@@ -220,11 +230,11 @@ export function useRouter(): void {
});
return unsubscribe;
}, [isVSCode, syncURLFromState]);
}, [isVSCode, isEmbeddedChat, syncURLFromState]);
// Listen for browser back/forward navigation
React.useEffect(() => {
if (typeof window === 'undefined' || isVSCode) {
if (typeof window === 'undefined' || isVSCode || isEmbeddedChat) {
return;
}
@@ -254,5 +264,5 @@ export function useRouter(): void {
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [applyRoute, isVSCode, setActiveMainTab, setSettingsDialogOpen]);
}, [applyRoute, isVSCode, isEmbeddedChat, setActiveMainTab, setSettingsDialogOpen]);
}