fix: preserve per-session scroll position on session switch (#1083)

* fix: restore scroll position when switching chat sessions

When switching between chat sessions, scroll position now restores to
where the user left off instead of always jumping to the bottom.

- Save pixel-level scrollPosition (scrollTop/scrollHeight/clientHeight)
  in viewport store on every scroll event
- Add restoreSavedScrollPosition to timeline controller for ratio-based
  restoration (handles content size changes between visits)
- Suppress intermediate scroll events during session transition with an
  explicit flag, cleared deterministically after restore completes
- Cancel in-flight animations/follow-loops on session switch
- Preserve scrollPosition when session-ui-store rebuilds SessionMemoryState

* fix: keep streaming sessions pinned on restore

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
jwcrystal
2026-05-01 12:39:04 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent a23f5e7545
commit 03c9065c90
7 changed files with 164 additions and 44 deletions
@@ -25,7 +25,6 @@ const STATUS_CHECK_ENDPOINT = '/auth/session';
const TRUST_DEVICE_STORAGE_KEY = 'openchamber.uiAuth.trustDevice';
const fetchSessionStatus = async (): Promise<Response> => {
console.log('[Frontend Auth] Checking session status...');
const response = await fetch(STATUS_CHECK_ENDPOINT, {
method: 'GET',
credentials: 'include',
@@ -33,7 +32,6 @@ const fetchSessionStatus = async (): Promise<Response> => {
Accept: 'application/json',
},
});
console.log('[Frontend Auth] Session status response:', response.status, response.statusText);
return response;
};
@@ -45,7 +43,6 @@ const readStoredTrustDevice = (): boolean => {
};
const submitPassword = async (password: string, trustDevice: boolean): Promise<Response> => {
console.log('[Frontend Auth] Submitting password...');
const response = await fetch(STATUS_CHECK_ENDPOINT, {
method: 'POST',
credentials: 'include',
@@ -55,7 +52,6 @@ const submitPassword = async (password: string, trustDevice: boolean): Promise<R
},
body: JSON.stringify({ password, trustDevice }),
});
console.log('[Frontend Auth] Password submit response:', response.status, response.statusText);
return response;
};
@@ -202,7 +198,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
const checkStatus = React.useCallback(async () => {
if (skipAuth) {
console.log('[Frontend Auth] VSCode runtime, skipping auth');
setState('authenticated');
return;
}
@@ -214,10 +209,8 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
refreshPasskeyStatus(),
]);
const responseText = await response.text();
console.log('[Frontend Auth] Raw response:', response.status, responseText);
if (response.ok) {
console.log('[Frontend Auth] Session is authenticated');
setState('authenticated');
setIsTunnelLocked(false);
setErrorMessage('');
@@ -231,10 +224,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
} catch {
data = {};
}
console.warn('[Frontend Auth] Session is locked (401)', data);
if (data.debug) {
console.warn('[Frontend Auth] Debug info:', data.debug);
}
setIsTunnelLocked(data.tunnelLocked === true);
setPasskeyStatus(latestPasskeyStatus);
setState('locked');
@@ -253,7 +242,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
setState('rate-limited');
return;
}
console.error('[Frontend Auth] Unexpected response status:', response.status);
setState('error');
setIsTunnelLocked(false);
} catch (error) {
@@ -338,7 +326,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
try {
const response = await submitPassword(password, trustDevice);
if (response.ok) {
console.log('[Frontend Auth] Login successful');
setPassword('');
setIsTunnelLocked(false);
if (enrollPasskey && supportsPasskeys) {
@@ -363,7 +350,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
}
if (response.status === 401) {
console.warn('[Frontend Auth] Login failed: Invalid password');
setErrorMessage(t('sessionAuth.error.incorrectPassword'));
setIsTunnelLocked(false);
setState('locked');
@@ -371,7 +357,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
}
if (response.status === 429) {
console.warn('[Frontend Auth] Login failed: Rate limited');
const data = await response.json().catch(() => ({}));
setRetryAfter(data.retryAfter);
setIsTunnelLocked(false);
@@ -379,7 +364,6 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
return;
}
console.error('[Frontend Auth] Login failed: Unexpected response', response.status);
setErrorMessage(t('sessionAuth.error.unexpectedResponse'));
setIsTunnelLocked(false);
setState('error');