authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-09 22:51:36-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 22:51:36-03:00
log42e1a0fba2938bd56761db63f466962342d7b779
treec11e1b620e4eb84fc4dd4f03248be5c07ecb41cd
parentca40de081c3ab087cd6458ce9bef12e43c5a7233
signature Signed by PGP key B5690EEEBB952194

fix(web): terminal drawer keeps its height after the window shrinks (#17658)


1 files changed, 39 insertions(+), 23 deletions(-)

apps/web/src/components/ThreadTerminalDrawer.tsx+39-23
......@@ -104,9 +104,8 @@ function maxDrawerHeight(): number {
104104 return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO));
105105}
106106
107function clampDrawerHeight(height: number): number {
107function clampDrawerHeight(height: number, maxHeight = maxDrawerHeight()): number {
108108 const safeHeight = Number.isFinite(height) ? height : DEFAULT_THREAD_TERMINAL_HEIGHT;
109 const maxHeight = maxDrawerHeight();
110109 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);
111110}
112111
......@@ -1181,13 +1180,16 @@ export default function ThreadTerminalDrawer({
11811180 false,
11821181 Schema.Boolean,
11831182 );
1184 const controlledDrawerHeight = clampDrawerHeight(height);
1183 // The stored height is the user's preference; the window only clamps what renders.
1184 const controlledDrawerHeight = height;
1185 const [windowMaxDrawerHeight, setWindowMaxDrawerHeight] = useState(maxDrawerHeight);
11851186 const [drawerHeightState, setDrawerHeightState] = useState(() => ({
11861187 threadId,
11871188 height: controlledDrawerHeight,
11881189 }));
1189 const drawerHeight =
1190 const preferredDrawerHeight =
11901191 drawerHeightState.threadId === threadId ? drawerHeightState.height : controlledDrawerHeight;
1192 const drawerHeight = clampDrawerHeight(preferredDrawerHeight, windowMaxDrawerHeight);
11911193 const setDrawerHeight = useCallback(
11921194 (update: SetStateAction<number>) => {
11931195 setDrawerHeightState((current) => {
......@@ -1201,11 +1203,9 @@ export default function ThreadTerminalDrawer({
12011203 },
12021204 [controlledDrawerHeight, threadId],
12031205 );
1204 const setDrawerHeightFromWindowResize = useEffectEvent((nextHeight: number) => {
1205 setDrawerHeight(nextHeight);
1206 });
12071206 const [resizeEpoch, setResizeEpoch] = useState(0);
12081207 const drawerHeightRef = useRef(drawerHeight);
1208 const preferredDrawerHeightRef = useRef(preferredDrawerHeight);
12091209 const lastSyncedHeightRef = useRef(controlledDrawerHeight);
12101210 const onHeightChangeRef = useRef(onHeightChange);
12111211 const resizeStateRef = useRef<{
......@@ -1395,15 +1395,24 @@ export default function ThreadTerminalDrawer({
13951395 onHeightChangeRef.current = onHeightChange;
13961396 }, [onHeightChange]);
13971397
1398 useEffect(() => {
1398 useLayoutEffect(() => {
1399 // A render mid-drag (the window cap moved) must not replace the live height.
1400 const resizeState = resizeStateRef.current;
1401 if (resizeState) {
1402 resizeState.drawer?.style.setProperty("height", `${drawerHeightRef.current}px`);
1403 return;
1404 }
13991405 drawerHeightRef.current = drawerHeight;
14001406 }, [drawerHeight]);
14011407
1408 useEffect(() => {
1409 preferredDrawerHeightRef.current = preferredDrawerHeight;
1410 }, [preferredDrawerHeight]);
1411
14021412 const syncHeight = useCallback((nextHeight: number) => {
1403 const clampedHeight = clampDrawerHeight(nextHeight);
1404 if (lastSyncedHeightRef.current === clampedHeight) return;
1405 lastSyncedHeightRef.current = clampedHeight;
1406 onHeightChangeRef.current(clampedHeight);
1413 if (lastSyncedHeightRef.current === nextHeight) return;
1414 lastSyncedHeightRef.current = nextHeight;
1415 onHeightChangeRef.current(nextHeight);
14071416 }, []);
14081417
14091418 useEffect(() => {
......@@ -1448,7 +1457,14 @@ export default function ThreadTerminalDrawer({
14481457 if (event.currentTarget.hasPointerCapture(event.pointerId)) {
14491458 event.currentTarget.releasePointerCapture(event.pointerId);
14501459 }
1451 if (!didResizeDuringDragRef.current) {
1460 // A drag that ends where it started keeps the stored preference, which may
1461 // be taller than the window-capped height the drag started from.
1462 if (!didResizeDuringDragRef.current || drawerHeightRef.current === resizeState.startHeight) {
1463 // React skips the write when the rendered height did not change, so put
1464 // back the preference as the current window caps it.
1465 const renderedHeight = clampDrawerHeight(preferredDrawerHeightRef.current);
1466 drawerHeightRef.current = renderedHeight;
1467 resizeState.drawer?.style.setProperty("height", `${renderedHeight}px`);
14521468 return;
14531469 }
14541470 setDrawerHeight(drawerHeightRef.current);
......@@ -1464,33 +1480,33 @@ export default function ThreadTerminalDrawer({
14641480 }
14651481
14661482 const onWindowResize = () => {
1467 const clampedHeight = clampDrawerHeight(drawerHeightRef.current);
1468 const changed = clampedHeight !== drawerHeightRef.current;
1469 if (changed) {
1470 setDrawerHeightFromWindowResize(clampedHeight);
1471 drawerHeightRef.current = clampedHeight;
1472 }
1473 if (!resizeStateRef.current) {
1474 syncHeight(clampedHeight);
1483 // React only renders the stored preference, so clamp a live drag height here.
1484 const resizeState = resizeStateRef.current;
1485 const liveHeight = clampDrawerHeight(drawerHeightRef.current);
1486 if (resizeState && liveHeight !== drawerHeightRef.current) {
1487 drawerHeightRef.current = liveHeight;
1488 resizeState.drawer?.style.setProperty("height", `${liveHeight}px`);
14751489 }
1490 setWindowMaxDrawerHeight(maxDrawerHeight());
14761491 setResizeEpoch((value) => value + 1);
14771492 };
14781493 window.addEventListener("resize", onWindowResize);
14791494 return () => {
14801495 window.removeEventListener("resize", onWindowResize);
14811496 };
1482 }, [syncHeight, visible]);
1497 }, [visible]);
14831498
14841499 useEffect(() => {
14851500 if (!visible) {
14861501 return;
14871502 }
1503 setWindowMaxDrawerHeight(maxDrawerHeight());
14881504 setResizeEpoch((value) => value + 1);
14891505 }, [visible]);
14901506
14911507 useEffect(() => {
14921508 return () => {
1493 syncHeight(drawerHeightRef.current);
1509 syncHeight(preferredDrawerHeightRef.current);
14941510 };
14951511 }, [syncHeight]);
14961512