| ... | @@ -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 | } |
| 106 | | 106 | |
| 107 | function clampDrawerHeight(height: number): number { | 107 | function 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 | } |
| 112 | | 111 | |
| ... | @@ -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]); |
| 1397 | | 1397 | |
| 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]); |
| 1401 | | 1407 | |
| | 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 | }, []); |
| 1408 | | 1417 | |
| 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 | } |
| 1465 | | 1481 | |
| 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]); |
| 1483 | | 1498 | |
| 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]); |
| 1490 | | 1506 | |
| 1491 | useEffect(() => { | 1507 | useEffect(() => { |
| 1492 | return () => { | 1508 | return () => { |
| 1493 | syncHeight(drawerHeightRef.current); | 1509 | syncHeight(preferredDrawerHeightRef.current); |
| 1494 | }; | 1510 | }; |
| 1495 | }, [syncHeight]); | 1511 | }, [syncHeight]); |
| 1496 | | 1512 | |