Fix wave: scroll-lock without layout shift; every models write mirrors the stored record
Fix 1 (owner: "lock must not shift"): the settings dialog's scroll-lock no longer toggles the content column to overflow-hidden (which hid the scrollbar and shifted everything right). The column KEEPS overflow-y-auto — scrollbar and widths stay exactly as they are — and while any MenuShell is open, non-passive wheel + touchmove listeners on the column preventDefault so it cannot scroll (owner-ruled mechanism: the scrollbar may stay, scrolling must not happen, nothing may move). Native listeners via ref — React attaches synthetic wheel/touch passively at the root. Fix 2 (owner repro: xhigh pin survives a GLM swap on screen until reload): every models-writing surface now mirrors SERVER TRUTH, never the request body. The PUT already returns the full stored record with clamps applied — settings-dialog write() and the composer's save() now setRecord/setModels from the response, so a swap that rides an unsupported level shows the clamped value in the same interaction: the row trigger, the open menu's level list + checkmark, and the composer pill all read the stored record. Live-verified the owner's exact repro: deepseek pinned extra high → GLM swap via the slot menu → trigger/menu/pill showed the stored clamped level immediately; and the lock cycle: scroll 300 → blocked while open → 300 after release, zero positional shift at engage.
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
@@ -118,6 +119,26 @@ export function SettingsDialogBody({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// MenuShell open-count — drives the content column's scroll lock.
|
||||
const [openMenus, setOpenMenus] = useState(0);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
// Scroll-lock (fix wave, owner ruling: the scrollbar MAY stay,
|
||||
// scrolling must NOT happen, nothing may move): the content column
|
||||
// keeps overflow-y-auto — the scrollbar and every width stay exactly
|
||||
// as they are — and while any MenuShell is open, non-passive wheel +
|
||||
// touchmove listeners on the column preventDefault. React attaches
|
||||
// synthetic wheel/touch passively at the root, so this needs the
|
||||
// native-listener ref seam.
|
||||
useEffect(() => {
|
||||
const el = contentRef.current;
|
||||
if (!el || openMenus === 0) return;
|
||||
const block = (e: Event) => e.preventDefault();
|
||||
el.addEventListener("wheel", block, { passive: false });
|
||||
el.addEventListener("touchmove", block, { passive: false });
|
||||
return () => {
|
||||
el.removeEventListener("wheel", block);
|
||||
el.removeEventListener("touchmove", block);
|
||||
};
|
||||
}, [openMenus]);
|
||||
|
||||
useEffect(() => {
|
||||
const onOpen = () => setOpenMenus((n) => n + 1);
|
||||
const onClose = () => setOpenMenus((n) => Math.max(0, n - 1));
|
||||
@@ -176,8 +197,17 @@ export function SettingsDialogBody({
|
||||
setError(`${section}: ${detail.error ?? res.status}`);
|
||||
return;
|
||||
}
|
||||
// Server truth, never the request body (fix wave, owner repro):
|
||||
// the models section clamps pinned thinking levels on write
|
||||
// (bd68486) — a swap that rides an unsupported level lands
|
||||
// CLAMPED in the record, and the writing surface must show the
|
||||
// stored value in this same interaction, not the optimistic one.
|
||||
const stored = (
|
||||
(await res.json().catch(() => null)) as { settings?: UserSettings } | null
|
||||
)?.settings;
|
||||
setRecord(
|
||||
(prev) => ({ ...(prev ?? {}), [section]: body }) as UserSettings,
|
||||
(prev) =>
|
||||
stored ?? ({ ...(prev ?? {}), [section]: body }) as UserSettings,
|
||||
);
|
||||
notifySettingsSaved(section);
|
||||
if (section === "appearance")
|
||||
@@ -246,9 +276,12 @@ export function SettingsDialogBody({
|
||||
{/* Content column — one section at a time, this scrolls — and
|
||||
LOCKS while any MenuShell is open (owner ruling: no
|
||||
scrolling with a dropdown down; MenuShell announces
|
||||
open/close, the count keeps overlap honest). */}
|
||||
open/close, the count keeps overlap honest). The lock
|
||||
intercepts wheel/touch instead of hiding the scrollbar —
|
||||
nothing may shift when the lock engages. */}
|
||||
<div
|
||||
className={`min-w-0 flex-1 ${openMenus > 0 ? "overflow-hidden" : "overflow-y-auto"}`}
|
||||
ref={contentRef}
|
||||
className="min-w-0 flex-1 overflow-y-auto"
|
||||
data-testid="settings-content"
|
||||
>
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user