Files
openchamber/packages/ui/src/lib/router/serializeRoute.ts
T
Bohdan Triapitsyn c82f188fc8 refactor(surface): remove the main-area surface concept entirely
activeSurface was permanently 'chat' after the legacy mobile layout
removal, so the whole concept is gone: the store field, surfaceGuard,
setActiveSurface/setSurfaceGuard, the per-runtime surface memory in
prepare/restoreForRuntimeSwitch, and WorkspaceSurface itself. All ~30
setActiveSurface('chat') call sites were no-ops and are deleted;
always-true 'is the chat active' checks in keyboard shortcuts, Header
and ChatContainer are unconditional now. FilesView's dirty-file guard
kept its file-switch and close protection but drops the surface-switch
branch nothing could trigger. TerminalView visibility comes only from
its callers. The router keeps parsing legacy ?tab= links (they open the
matching context-panel surface) via its own RouteTab type and no longer
serializes a tab or diff file into URLs — desktop URLs never carried
them anyway.
2026-08-24 16:36:41 +03:00

136 lines
3.7 KiB
TypeScript

import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
import { ROUTE_PARAMS } from './types';
/**
* Application state relevant for URL serialization.
*/
export interface AppRouteState {
sessionId: string | null;
isSettingsOpen: boolean;
settingsPath: string;
}
/**
* Serialize application state to URL search parameters.
* Only includes parameters that differ from defaults to keep URLs clean.
*/
function serializeRoute(state: AppRouteState): URLSearchParams {
const params = new URLSearchParams();
// Session ID - always include if present
if (state.sessionId && state.sessionId.trim().length > 0) {
params.set(ROUTE_PARAMS.SESSION, state.sessionId);
}
// Settings takes precedence - if open, include settings section
if (state.isSettingsOpen) {
const settingsPath = state.settingsPath.trim().length > 0 ? state.settingsPath : 'home';
params.set(ROUTE_PARAMS.SETTINGS, settingsPath);
// Don't include tab when settings is open (it's a full-screen overlay)
return params;
}
return params;
}
/**
* Convert URLSearchParams to a URL string.
* Returns just the pathname if no params, otherwise pathname + search string.
*/
function buildURL(params: URLSearchParams, pathname?: string): string {
const path = pathname ?? (typeof window !== 'undefined' ? window.location.pathname : '/');
const search = params.toString();
if (!search) {
return path;
}
return `${path}?${search}`;
}
/**
* Check if the current URL matches the given route state.
* Used to avoid unnecessary URL updates.
*/
function routeMatchesURL(state: AppRouteState): boolean {
if (typeof window === 'undefined') {
return true;
}
try {
const currentParams = new URLSearchParams(window.location.search);
const newParams = serializeRoute(state);
// Compare sorted param strings for equality
const currentSorted = [...currentParams.entries()].sort((a, b) => a[0].localeCompare(b[0]));
const newSorted = [...newParams.entries()].sort((a, b) => a[0].localeCompare(b[0]));
if (currentSorted.length !== newSorted.length) {
return false;
}
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i][0] !== newSorted[i][0] || currentSorted[i][1] !== newSorted[i][1]) {
return false;
}
}
return true;
} catch {
return true;
}
}
/**
* Update the browser URL using pushState or replaceState.
* Does nothing if URL already matches, in VS Code context, or in the
* embedded session-chat iframe (whose URL identity is fixed at mount).
*/
export function updateBrowserURL(
state: AppRouteState,
options: { replace?: boolean; force?: boolean } = {}
): void {
if (typeof window === 'undefined') {
return;
}
// Both VS Code webviews and embedded session-chat iframes carry session
// identity outside the route params (`__VSCODE_CONFIG__` / `?ocPanel=…`).
// Rebuilding the URL here would strip those params, so skip entirely.
if (isVSCodeContext() || isEmbeddedSessionChat()) {
return;
}
// Skip if URL already matches (unless forced)
if (!options.force && routeMatchesURL(state)) {
return;
}
try {
const params = serializeRoute(state);
const url = buildURL(params);
if (options.replace) {
window.history.replaceState({ ...window.history.state, route: state }, '', url);
} else {
window.history.pushState({ route: state }, '', url);
}
} catch {
// Silently fail - URL updates are non-critical
}
}
/**
* Check if running in VS Code webview context.
*/
function isVSCodeContext(): boolean {
if (typeof window === 'undefined') {
return false;
}
// Check for VS Code config object
const win = window as { __VSCODE_CONFIG__?: unknown };
return win.__VSCODE_CONFIG__ !== undefined;
}