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 {...@@ -104,9 +104,8 @@ function maxDrawerHeight(): number {
104 return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO));104 return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO));
105}105}
106106
107function clampDrawerHeight(height: number): number {107function clampDrawerHeight(height: number, maxHeight = maxDrawerHeight()): number {
108 const safeHeight = Number.isFinite(height) ? height : DEFAULT_THREAD_TERMINAL_HEIGHT;108 const safeHeight = Number.isFinite(height) ? height : DEFAULT_THREAD_TERMINAL_HEIGHT;
109 const maxHeight = maxDrawerHeight();
110 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);109 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);
111}110}
112111
...@@ -1181,13 +1180,16 @@ export default function ThreadTerminalDrawer({...@@ -1181,13 +1180,16 @@ export default function ThreadTerminalDrawer({
1181 false,1180 false,
1182 Schema.Boolean,1181 Schema.Boolean,
1183 );1182 );
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);
1185 const [drawerHeightState, setDrawerHeightState] = useState(() => ({1186 const [drawerHeightState, setDrawerHeightState] = useState(() => ({
1186 threadId,1187 threadId,
1187 height: controlledDrawerHeight,1188 height: controlledDrawerHeight,
1188 }));1189 }));
1189 const drawerHeight =1190 const preferredDrawerHeight =
1190 drawerHeightState.threadId === threadId ? drawerHeightState.height : controlledDrawerHeight;1191 drawerHeightState.threadId === threadId ? drawerHeightState.height : controlledDrawerHeight;
1192 const drawerHeight = clampDrawerHeight(preferredDrawerHeight, windowMaxDrawerHeight);
1191 const setDrawerHeight = useCallback(1193 const setDrawerHeight = useCallback(
1192 (update: SetStateAction<number>) => {1194 (update: SetStateAction<number>) => {
1193 setDrawerHeightState((current) => {1195 setDrawerHeightState((current) => {
...@@ -1201,11 +1203,9 @@ export default function ThreadTerminalDrawer({...@@ -1201,11 +1203,9 @@ export default function ThreadTerminalDrawer({
1201 },1203 },
1202 [controlledDrawerHeight, threadId],1204 [controlledDrawerHeight, threadId],
1203 );1205 );
1204 const setDrawerHeightFromWindowResize = useEffectEvent((nextHeight: number) => {
1205 setDrawerHeight(nextHeight);
1206 });
1207 const [resizeEpoch, setResizeEpoch] = useState(0);1206 const [resizeEpoch, setResizeEpoch] = useState(0);
1208 const drawerHeightRef = useRef(drawerHeight);1207 const drawerHeightRef = useRef(drawerHeight);
1208 const preferredDrawerHeightRef = useRef(preferredDrawerHeight);
1209 const lastSyncedHeightRef = useRef(controlledDrawerHeight);1209 const lastSyncedHeightRef = useRef(controlledDrawerHeight);
1210 const onHeightChangeRef = useRef(onHeightChange);1210 const onHeightChangeRef = useRef(onHeightChange);
1211 const resizeStateRef = useRef<{1211 const resizeStateRef = useRef<{
...@@ -1395,15 +1395,24 @@ export default function ThreadTerminalDrawer({...@@ -1395,15 +1395,24 @@ export default function ThreadTerminalDrawer({
1395 onHeightChangeRef.current = onHeightChange;1395 onHeightChangeRef.current = onHeightChange;
1396 }, [onHeightChange]);1396 }, [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 }
1399 drawerHeightRef.current = drawerHeight;1405 drawerHeightRef.current = drawerHeight;
1400 }, [drawerHeight]);1406 }, [drawerHeight]);
14011407
1408 useEffect(() => {
1409 preferredDrawerHeightRef.current = preferredDrawerHeight;
1410 }, [preferredDrawerHeight]);
1411
1402 const syncHeight = useCallback((nextHeight: number) => {1412 const syncHeight = useCallback((nextHeight: number) => {
1403 const clampedHeight = clampDrawerHeight(nextHeight);1413 if (lastSyncedHeightRef.current === nextHeight) return;
1404 if (lastSyncedHeightRef.current === clampedHeight) return;1414 lastSyncedHeightRef.current = nextHeight;
1405 lastSyncedHeightRef.current = clampedHeight;1415 onHeightChangeRef.current(nextHeight);
1406 onHeightChangeRef.current(clampedHeight);
1407 }, []);1416 }, []);
14081417
1409 useEffect(() => {1418 useEffect(() => {
...@@ -1448,7 +1457,14 @@ export default function ThreadTerminalDrawer({...@@ -1448,7 +1457,14 @@ export default function ThreadTerminalDrawer({
1448 if (event.currentTarget.hasPointerCapture(event.pointerId)) {1457 if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1449 event.currentTarget.releasePointerCapture(event.pointerId);1458 event.currentTarget.releasePointerCapture(event.pointerId);
1450 }1459 }
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`);
1452 return;1468 return;
1453 }1469 }
1454 setDrawerHeight(drawerHeightRef.current);1470 setDrawerHeight(drawerHeightRef.current);
...@@ -1464,33 +1480,33 @@ export default function ThreadTerminalDrawer({...@@ -1464,33 +1480,33 @@ export default function ThreadTerminalDrawer({
1464 }1480 }
14651481
1466 const onWindowResize = () => {1482 const onWindowResize = () => {
1467 const clampedHeight = clampDrawerHeight(drawerHeightRef.current);1483 // React only renders the stored preference, so clamp a live drag height here.
1468 const changed = clampedHeight !== drawerHeightRef.current;1484 const resizeState = resizeStateRef.current;
1469 if (changed) {1485 const liveHeight = clampDrawerHeight(drawerHeightRef.current);
1470 setDrawerHeightFromWindowResize(clampedHeight);1486 if (resizeState && liveHeight !== drawerHeightRef.current) {
1471 drawerHeightRef.current = clampedHeight;1487 drawerHeightRef.current = liveHeight;
1472 }1488 resizeState.drawer?.style.setProperty("height", `${liveHeight}px`);
1473 if (!resizeStateRef.current) {
1474 syncHeight(clampedHeight);
1475 }1489 }
1490 setWindowMaxDrawerHeight(maxDrawerHeight());
1476 setResizeEpoch((value) => value + 1);1491 setResizeEpoch((value) => value + 1);
1477 };1492 };
1478 window.addEventListener("resize", onWindowResize);1493 window.addEventListener("resize", onWindowResize);
1479 return () => {1494 return () => {
1480 window.removeEventListener("resize", onWindowResize);1495 window.removeEventListener("resize", onWindowResize);
1481 };1496 };
1482 }, [syncHeight, visible]);1497 }, [visible]);
14831498
1484 useEffect(() => {1499 useEffect(() => {
1485 if (!visible) {1500 if (!visible) {
1486 return;1501 return;
1487 }1502 }
1503 setWindowMaxDrawerHeight(maxDrawerHeight());
1488 setResizeEpoch((value) => value + 1);1504 setResizeEpoch((value) => value + 1);
1489 }, [visible]);1505 }, [visible]);
14901506
1491 useEffect(() => {1507 useEffect(() => {
1492 return () => {1508 return () => {
1493 syncHeight(drawerHeightRef.current);1509 syncHeight(preferredDrawerHeightRef.current);
1494 };1510 };
1495 }, [syncHeight]);1511 }, [syncHeight]);
14961512