fix: keep browser tab session navigation stable
Keep the browser pane loaded URL separate from the in-frame current URL so SPA navigation updates the address bar and history without remounting the iframe or resetting the Electron webview src. Preserve parsed ?session= route params during initial URL normalization and pass a directory hint when applying deep links, preventing embedded OpenChamber sessions from collapsing back to / while bootstrap catches up.
This commit is contained in:
@@ -1306,6 +1306,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
const startUrl = normalized !== 'about:blank' ? normalized : '';
|
||||
const [urlInput, setUrlInput] = React.useState(startUrl);
|
||||
const [currentUrl, setCurrentUrl] = React.useState(startUrl);
|
||||
const [loadedUrl, setLoadedUrl] = React.useState(startUrl);
|
||||
const [history, setHistory] = React.useState<string[]>(() => startUrl ? [startUrl] : []);
|
||||
const [historyIndex, setHistoryIndex] = React.useState(() => startUrl ? 0 : -1);
|
||||
const [reloadNonce, bumpReload] = React.useReducer((value: number) => value + 1, 0);
|
||||
@@ -1324,12 +1325,17 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
setContextPanelTabTargetPath(directory, tabID, url);
|
||||
}, [directory, tabID, setContextPanelTabTargetPath]);
|
||||
|
||||
const applyUrl = React.useCallback((url: string, options?: { replaceHistory?: boolean }) => {
|
||||
const applyUrl = React.useCallback((url: string, options?: { replaceHistory?: boolean; inFrame?: boolean }) => {
|
||||
const normalizedUrl = normalizeBrowserUrl(url);
|
||||
const nextUrl = normalizedUrl !== 'about:blank' ? normalizedUrl : '';
|
||||
setCurrentUrl(nextUrl);
|
||||
setUrlInput(nextUrl);
|
||||
setIsLoading(Boolean(nextUrl));
|
||||
if (!options?.inFrame) {
|
||||
setLoadedUrl(nextUrl);
|
||||
setIsLoading(Boolean(nextUrl));
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
persistUrl(nextUrl);
|
||||
|
||||
setHistory((current) => {
|
||||
@@ -1360,6 +1366,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
if (!nextUrl) return;
|
||||
setHistoryIndex(nextIndex);
|
||||
setCurrentUrl(nextUrl);
|
||||
setLoadedUrl(nextUrl);
|
||||
setUrlInput(nextUrl);
|
||||
setIsLoading(true);
|
||||
persistUrl(nextUrl);
|
||||
@@ -1376,12 +1383,12 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
}, [currentUrl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentUrl) {
|
||||
if (!loadedUrl) {
|
||||
setProxyState({ status: 'idle' });
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyTargetKey = getBrowserProxyTargetKey(currentUrl);
|
||||
const proxyTargetKey = getBrowserProxyTargetKey(loadedUrl);
|
||||
const cached = getCachedProxyTarget(proxyTargetKey);
|
||||
if (cached?.previewToken) {
|
||||
setProxyState({ status: 'ready', proxyBasePath: cached.proxyBasePath, previewToken: cached.previewToken, expiresAt: cached.expiresAt });
|
||||
@@ -1401,7 +1408,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ url: currentUrl, allowExternal: true }),
|
||||
body: JSON.stringify({ url: loadedUrl, allowExternal: true }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1441,9 +1448,9 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [currentUrl, t]);
|
||||
}, [loadedUrl, t]);
|
||||
|
||||
const proxyUrlAuthKey = currentUrl && proxyState.status === 'ready'
|
||||
const proxyUrlAuthKey = loadedUrl && proxyState.status === 'ready'
|
||||
? `${proxyState.proxyBasePath}|${proxyState.previewToken || ''}|${reloadNonce}`
|
||||
: '';
|
||||
|
||||
@@ -1468,9 +1475,9 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
|
||||
const proxySrc = React.useMemo(() => {
|
||||
if (urlAuthReadyKey !== proxyUrlAuthKey) return '';
|
||||
if (!currentUrl || proxyState.status !== 'ready') return '';
|
||||
if (!loadedUrl || proxyState.status !== 'ready') return '';
|
||||
try {
|
||||
const parsed = new URL(currentUrl);
|
||||
const parsed = new URL(loadedUrl);
|
||||
const path = parsed.pathname || '/';
|
||||
const searchParams = new URLSearchParams(parsed.search);
|
||||
searchParams.delete('oc_url_token');
|
||||
@@ -1482,12 +1489,12 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [currentUrl, proxyState, proxyUrlAuthKey, reloadNonce, urlAuthReadyKey]);
|
||||
}, [loadedUrl, proxyState, proxyUrlAuthKey, reloadNonce, urlAuthReadyKey]);
|
||||
|
||||
const iframeSrc = proxySrc || (proxyState.status === 'error' ? currentUrl : '');
|
||||
const iframeSrc = proxySrc || (proxyState.status === 'error' ? loadedUrl : '');
|
||||
|
||||
const getCurrentUrlFromFrameUrl = React.useCallback((frameUrl: string): string => {
|
||||
if (!frameUrl || !currentUrl || proxyState.status !== 'ready') return '';
|
||||
if (!frameUrl || !loadedUrl || proxyState.status !== 'ready') return '';
|
||||
try {
|
||||
const parsedFrameUrl = new URL(frameUrl, window.location.origin);
|
||||
const proxyBasePath = proxyState.proxyBasePath.endsWith('/')
|
||||
@@ -1498,18 +1505,18 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
}
|
||||
|
||||
const rest = parsedFrameUrl.pathname.slice(proxyBasePath.length) || '/';
|
||||
const upstreamOrigin = new URL(currentUrl).origin;
|
||||
const upstreamOrigin = new URL(loadedUrl).origin;
|
||||
return stripPreviewQueryParams(new URL(`${rest}${parsedFrameUrl.search}${parsedFrameUrl.hash}`, upstreamOrigin).toString());
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [currentUrl, proxyState]);
|
||||
}, [loadedUrl, proxyState]);
|
||||
|
||||
const getUpstreamUrlFromLocalFrameUrl = React.useCallback((frameUrl: string): string => {
|
||||
if (!frameUrl || !currentUrl || proxyState.status !== 'ready') return '';
|
||||
if (!frameUrl || !loadedUrl || proxyState.status !== 'ready') return '';
|
||||
try {
|
||||
const parsedFrameUrl = new URL(frameUrl, window.location.origin);
|
||||
const upstreamOrigin = new URL(currentUrl).origin;
|
||||
const upstreamOrigin = new URL(loadedUrl).origin;
|
||||
if (parsedFrameUrl.origin !== window.location.origin || upstreamOrigin === window.location.origin) {
|
||||
return '';
|
||||
}
|
||||
@@ -1525,7 +1532,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [currentUrl, proxyState]);
|
||||
}, [loadedUrl, proxyState]);
|
||||
|
||||
const postInspectMode = React.useCallback((enabled: boolean) => {
|
||||
const frameWindow = iframeRef.current?.contentWindow;
|
||||
@@ -1614,7 +1621,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
const frameUrl = typeof data.url === 'string' ? data.url : '';
|
||||
const nextUrl = getCurrentUrlFromFrameUrl(frameUrl);
|
||||
if (nextUrl && nextUrl !== currentUrl) {
|
||||
applyUrl(nextUrl);
|
||||
applyUrl(nextUrl, { inFrame: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1685,8 +1692,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
const frameUrl = iframeRef.current?.contentWindow?.location.href || '';
|
||||
const upstreamUrl = getUpstreamUrlFromLocalFrameUrl(frameUrl);
|
||||
if (upstreamUrl) {
|
||||
setIsLoading(true);
|
||||
applyUrl(upstreamUrl);
|
||||
applyUrl(upstreamUrl, { inFrame: true });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -1739,7 +1745,7 @@ const IframeBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dire
|
||||
{iframeSrc ? (
|
||||
<div className="absolute inset-0">
|
||||
<iframe
|
||||
key={`${iframeSrc}:${reloadNonce}`}
|
||||
key={`${tabID}:${reloadNonce}`}
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
title={t('contextPanel.browser.empty')}
|
||||
@@ -1788,6 +1794,7 @@ const DesktopBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dir
|
||||
const setContextPanelTabTargetPath = useUIStore((state) => state.setContextPanelTabTargetPath);
|
||||
const normalized = normalizeBrowserUrl(initialUrl);
|
||||
const startUrl = normalized !== 'about:blank' ? normalized : '';
|
||||
const initialWebviewSrcRef = React.useRef(normalizeBrowserUrl(initialUrl));
|
||||
const [urlInput, setUrlInput] = React.useState(startUrl);
|
||||
const [currentUrl, setCurrentUrl] = React.useState(startUrl);
|
||||
const [isInspecting, setIsInspecting] = React.useState(false);
|
||||
@@ -2027,7 +2034,7 @@ const DesktopBrowserPane: React.FC<DesktopBrowserPaneProps> = ({ initialUrl, dir
|
||||
<div className="relative min-h-0 flex-1 bg-background">
|
||||
<webview
|
||||
ref={webviewRef}
|
||||
src={normalizeBrowserUrl(initialUrl)}
|
||||
src={initialWebviewSrcRef.current}
|
||||
partition="persist:openchamber-browser"
|
||||
allowpopups
|
||||
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||
|
||||
@@ -60,7 +60,8 @@ export function useRouter(): void {
|
||||
if (route.sessionId) {
|
||||
const currentSessionId = useSessionUIStore.getState().currentSessionId;
|
||||
if (route.sessionId !== currentSessionId) {
|
||||
await setCurrentSession(route.sessionId);
|
||||
const directoryHint = useSessionUIStore.getState().getDirectoryForSession(route.sessionId);
|
||||
setCurrentSession(route.sessionId, directoryHint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,14 +144,23 @@ export function useRouter(): void {
|
||||
const initializeRoute = async () => {
|
||||
await applyRoute(route);
|
||||
|
||||
// After applying, update URL to normalized form (use replaceState)
|
||||
// After applying, update URL to normalized form (use replaceState).
|
||||
// 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) {
|
||||
syncURLFromState({ replace: true });
|
||||
updateBrowserURL({
|
||||
...getCurrentAppState(),
|
||||
sessionId: route.sessionId ?? useSessionUIStore.getState().currentSessionId,
|
||||
tab: route.tab ?? useUIStore.getState().activeMainTab,
|
||||
settingsPath: route.settingsPath ?? useUIStore.getState().settingsPage,
|
||||
diffFile: route.diffFile ?? useUIStore.getState().pendingDiffFile,
|
||||
}, { replace: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
void initializeRoute();
|
||||
}, [applyRoute, isVSCode, syncURLFromState]);
|
||||
}, [applyRoute, getCurrentAppState, isVSCode]);
|
||||
|
||||
// Subscribe to session changes
|
||||
React.useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user