This commit is contained in:
oobabooga 2026-07-29 09:17:32 +00:00 committed by GitHub
commit 4ebd079682
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 633 additions and 120 deletions

View file

@ -4,18 +4,285 @@
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { useMonitorOverlayStore } from "@/features/settings";
import {
aggregateGpuMemoryTotalGb,
useSystemInfo,
} from "@/hooks/use-system";
import { aggregateGpuMemoryTotalGb, useSystemInfo } from "@/hooks/use-system";
import { useT } from "@/i18n";
import { cn } from "@/lib/utils";
import { CpuIcon, GripVerticalIcon, XIcon } from "lucide-react";
import { AnimatePresence, motion, useDragControls } from "motion/react";
import { type PointerEvent, useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import {
type PointerEvent,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
interface MonitorLayout {
left: number;
top: number;
minWidth: number;
minHeight: number;
maxWidth: number;
maxHeight: number;
}
interface DragSession {
pointerId: number;
startX: number;
startY: number;
left: number;
top: number;
maxLeft: number;
maxTop: number;
constraintsWidth: number;
constraintsHeight: number;
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
// Height the panel wants. Reading the rendered box instead hides growth once
// maxHeight caps it, so the observer never fires and the cap is never lifted.
function desiredPanelHeight(
renderedHeight: number,
scroll: HTMLDivElement | null,
content: HTMLDivElement | null,
): number {
if (!(scroll && content)) {
return renderedHeight;
}
// The scroll region is the only flexible child, so the rest is fixed chrome.
const chrome = renderedHeight - scroll.getBoundingClientRect().height;
return chrome + content.getBoundingClientRect().height;
}
// Width the panel wants. While anchored, maxWidth equals the current width, so
// the cap is also a floor: a monitor opened in a narrow window never widens
// again. Lift the cap for one measurement to break that.
function naturalWidth(monitor: HTMLDivElement): number {
const capped = monitor.style.maxWidth;
// "none", not "", so the class-level max-w-full lifts too.
monitor.style.maxWidth = "none";
const width = monitor.getBoundingClientRect().width;
monitor.style.maxWidth = capped;
return width;
}
function useMonitorLayout(constraintsElement: HTMLDivElement | null) {
const monitorRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const dragSessionRef = useRef<DragSession | null>(null);
const hasDraggedRef = useRef(false);
const preferredWidthRef = useRef<number | null>(null);
const preferredHeightRef = useRef<number | null>(null);
const surfaceWidthRef = useRef(0);
const remeasureRef = useRef(0);
const [layout, setLayout] = useState<MonitorLayout | null>(null);
useLayoutEffect(() => {
const monitor = monitorRef.current;
const constraints = constraintsElement;
if (!(monitor && constraints)) {
return;
}
// Deferred to the next frame: writing a style while ResizeObserver entries
// are delivered makes Firefox report an observer loop. A hand-resized panel
// keeps the user's width instead of re-measuring.
const scheduleWidthRemeasure = (surfaceWidth: number) => {
if (surfaceWidth === surfaceWidthRef.current) {
return;
}
surfaceWidthRef.current = surfaceWidth;
if (monitor.style.width || remeasureRef.current) {
return;
}
remeasureRef.current = requestAnimationFrame(() => {
remeasureRef.current = 0;
preferredWidthRef.current = naturalWidth(monitor);
reconcileGeometry();
});
};
const reconcileGeometry = () => {
const constraintsBox = constraints.getBoundingClientRect();
const monitorBox = monitor.getBoundingClientRect();
const desiredHeight = desiredPanelHeight(
monitorBox.height,
scrollRef.current,
contentRef.current,
);
scheduleWidthRemeasure(constraintsBox.width);
const desiredWidth = Math.max(
monitorBox.width,
preferredWidthRef.current ?? monitorBox.width,
);
// Content height is the floor, as a resolved number: intrinsic
// min-content outranks max-height, a number clamped to it cannot.
if (!monitor.style.height) {
preferredHeightRef.current = desiredHeight;
}
const width = Math.min(desiredWidth, constraintsBox.width);
const height = Math.min(desiredHeight, constraintsBox.height);
const maxLeft = Math.max(0, constraintsBox.width - width);
const maxTop = Math.max(0, constraintsBox.height - height);
const currentLeft = monitorBox.left - constraintsBox.left;
const currentTop = monitorBox.top - constraintsBox.top;
const left = hasDraggedRef.current
? clamp(currentLeft, 0, maxLeft)
: maxLeft;
const top = hasDraggedRef.current ? clamp(currentTop, 0, maxTop) : maxTop;
const session = dragSessionRef.current;
if (session) {
session.left = left;
session.top = top;
session.maxLeft = maxLeft;
session.maxTop = maxTop;
session.constraintsWidth = constraintsBox.width;
session.constraintsHeight = constraintsBox.height;
}
setLayout((current) => {
const next = {
left,
top,
minWidth: preferredWidthRef.current ?? monitorBox.width,
minHeight: preferredHeightRef.current ?? monitorBox.height,
maxWidth: constraintsBox.width - left,
maxHeight: constraintsBox.height - top,
};
return current &&
current.left === next.left &&
current.top === next.top &&
current.minWidth === next.minWidth &&
current.minHeight === next.minHeight &&
current.maxWidth === next.maxWidth &&
current.maxHeight === next.maxHeight
? current
: next;
});
};
reconcileGeometry();
const observer = new ResizeObserver(reconcileGeometry);
observer.observe(constraints);
observer.observe(monitor);
// The unclamped content wrapper is what makes late GPU rows reposition the
// panel instead of being cut off.
if (contentRef.current) {
observer.observe(contentRef.current);
}
return () => {
observer.disconnect();
if (remeasureRef.current) {
cancelAnimationFrame(remeasureRef.current);
remeasureRef.current = 0;
}
};
}, [constraintsElement]);
function startDrag(event: PointerEvent<HTMLDivElement>) {
const monitor = monitorRef.current;
if (event.button !== 0 || !(monitor && constraintsElement)) {
return;
}
event.preventDefault();
const constraintsBox = constraintsElement.getBoundingClientRect();
const monitorBox = monitor.getBoundingClientRect();
const left = monitorBox.left - constraintsBox.left;
const top = monitorBox.top - constraintsBox.top;
hasDraggedRef.current = true;
// Native resize records attempted inline dimensions even when max-width
// or max-height hides them. Normalize only hidden dimensions so an
// auto-sized monitor can still grow when system rows arrive later.
const inlineWidth = Number.parseFloat(monitor.style.width);
const inlineHeight = Number.parseFloat(monitor.style.height);
if (
Number.isFinite(inlineWidth) &&
Math.abs(inlineWidth - monitorBox.width) > 0.5
) {
monitor.style.width = `${monitorBox.width}px`;
}
if (
Number.isFinite(inlineHeight) &&
Math.abs(inlineHeight - monitorBox.height) > 0.5
) {
monitor.style.height = `${monitorBox.height}px`;
}
dragSessionRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
left,
top,
maxLeft: Math.max(0, constraintsBox.width - monitorBox.width),
maxTop: Math.max(0, constraintsBox.height - monitorBox.height),
constraintsWidth: constraintsBox.width,
constraintsHeight: constraintsBox.height,
};
event.currentTarget.setPointerCapture(event.pointerId);
}
function updateDrag(event: PointerEvent<HTMLDivElement>) {
const session = dragSessionRef.current;
if (!session || session.pointerId !== event.pointerId) {
return;
}
const left = clamp(
session.left + event.clientX - session.startX,
0,
session.maxLeft,
);
const top = clamp(
session.top + event.clientY - session.startY,
0,
session.maxTop,
);
session.startX = event.clientX;
session.startY = event.clientY;
session.left = left;
session.top = top;
setLayout((current) =>
!current || (current.left === left && current.top === top)
? current
: {
...current,
left,
top,
maxWidth: session.constraintsWidth - left,
maxHeight: session.constraintsHeight - top,
},
);
}
function finishDrag(event: PointerEvent<HTMLDivElement>) {
if (dragSessionRef.current?.pointerId === event.pointerId) {
dragSessionRef.current = null;
}
}
return {
monitorRef,
scrollRef,
contentRef,
layout,
startDrag,
updateDrag,
finishDrag,
};
}
function clampPercent(value: number): number {
return Math.max(0, Math.min(100, value));
return clamp(value, 0, 100);
}
function usageIndicatorClass(percent: number): string {
@ -45,23 +312,27 @@ function formatGiB(value: number): string {
return `${value.toFixed(digits)} GiB`;
}
export function FloatingMonitor() {
const t = useT();
const { isOpen, setIsOpen } = useMonitorOverlayStore();
const systemInfo = useSystemInfo({ enabled: isOpen, pollMs: 5000 });
interface FloatingMonitorPanelProps {
onClose: () => void;
systemInfo: ReturnType<typeof useSystemInfo>;
}
function FloatingMonitorPanel({
onClose,
systemInfo,
}: FloatingMonitorPanelProps) {
const t = useT();
const [constraintsElement, setConstraintsElement] =
useState<HTMLDivElement | null>(null);
const constraintsRef = useMemo(
() => ({ current: constraintsElement }),
[constraintsElement],
);
const dragControls = useDragControls();
function startDrag(event: PointerEvent<HTMLDivElement>) {
event.preventDefault();
dragControls.start(event);
}
const {
monitorRef,
scrollRef,
contentRef,
layout,
startDrag,
updateDrag,
finishDrag,
} = useMonitorLayout(constraintsElement);
const ramTotal = systemInfo.memory?.total_gb ?? 0;
const ramAvailable = systemInfo.memory?.available_gb ?? 0;
@ -98,124 +369,165 @@ export function FloatingMonitor() {
const hasGpu = (displayedGpu?.available ?? false) && devices.length > 0;
return (
<AnimatePresence>
{isOpen && (
<div
ref={setConstraintsElement}
className="fixed inset-0 z-50 pointer-events-none"
>
<motion.div
drag={true}
dragControls={dragControls}
dragListener={false}
dragConstraints={constraintsRef}
dragElastic={0}
dragMomentum={false}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="settings-surface fixed bottom-4 right-4 w-64 max-w-[calc(100vw-2rem)] resize overflow-hidden rounded-xl border border-border/70 p-3 shadow-border ring-0 backdrop-blur-sm pointer-events-auto cursor-default select-none"
>
<div className="mb-2 flex items-center justify-between gap-2 border-b border-border/60 pb-2">
<div className="flex min-w-0 flex-1 items-center gap-1.5 truncate text-xs font-semibold text-foreground">
<CpuIcon className="size-3.5 shrink-0 text-primary" />
<span className="truncate">
{t("settings.resources.liveMonitor.title")}
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
<div
onPointerDown={startDrag}
className="touch-none cursor-grab rounded-md px-1 text-muted-foreground/60 transition-colors hover:bg-muted/60 hover:text-muted-foreground active:cursor-grabbing"
>
<GripVerticalIcon className="size-3.5" />
</div>
<Button
size="icon-xs"
variant="ghost"
className="text-muted-foreground hover:text-foreground"
onClick={() => setIsOpen(false)}
title={t("common.close")}
aria-label={t("common.close")}
>
<XIcon className="size-3" />
</Button>
</div>
<div
ref={setConstraintsElement}
className="pointer-events-none fixed inset-4 z-50"
>
<motion.div
ref={monitorRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(
"settings-surface pointer-events-auto absolute flex max-h-full w-64 max-w-full cursor-default select-none flex-col overflow-hidden rounded-xl border border-border/70 p-3 shadow-border ring-0 backdrop-blur-sm",
layout ? "top-0 left-0 resize" : "right-0 bottom-0",
)}
data-testid="floating-monitor"
style={
layout
? {
left: layout.left,
top: layout.top,
minWidth: Math.min(layout.minWidth, layout.maxWidth),
minHeight: Math.min(layout.minHeight, layout.maxHeight),
maxWidth: layout.maxWidth,
maxHeight: layout.maxHeight,
}
: undefined
}
>
<div className="mb-2 flex items-center justify-between gap-2 border-b border-border/60 pb-2">
<div className="flex min-w-0 flex-1 items-center gap-1.5 truncate text-xs font-semibold text-foreground">
<CpuIcon className="size-3.5 shrink-0 text-primary" />
<span className="truncate">
{t("settings.resources.liveMonitor.title")}
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
<div
data-testid="floating-monitor-drag-handle"
onPointerDown={startDrag}
onPointerMove={updateDrag}
onPointerUp={finishDrag}
onPointerCancel={finishDrag}
onLostPointerCapture={finishDrag}
className="touch-none cursor-grab rounded-md px-1 text-muted-foreground/60 transition-colors hover:bg-muted/60 hover:text-muted-foreground active:cursor-grabbing"
>
<GripVerticalIcon className="size-3.5" />
</div>
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="space-y-3 overflow-hidden"
<Button
size="icon-xs"
variant="ghost"
className="text-muted-foreground hover:text-foreground"
onClick={onClose}
title={t("common.close")}
aria-label={t("common.close")}
>
<XIcon className="size-3" />
</Button>
</div>
</div>
<div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto">
<div
ref={contentRef}
data-testid="floating-monitor-content"
className="space-y-3"
>
<div className="space-y-1">
<div className="flex justify-between text-ui-11 font-medium font-mono">
<span>{t("settings.resources.liveMonitor.ram")}</span>
<span
className={cn("tabular-nums", usageTextClass(ramPercent))}
>
{Math.round(ramPercent)}%
</span>
</div>
<div className="text-xs text-muted-foreground font-mono tabular-nums">
{formatGiB(ramUsed)} / {formatGiB(ramTotal)}
</div>
<Progress
value={ramPercent}
className="mt-1 h-1.5 rounded-full bg-muted"
indicatorClassName={usageIndicatorClass(ramPercent)}
/>
</div>
{hasGpu && (
<div className="space-y-1">
<div className="flex justify-between text-ui-11 font-medium font-mono">
<span>{t("settings.resources.liveMonitor.ram")}</span>
<span className="truncate flex-1 pr-2">
{t("settings.resources.liveMonitor.vram")}{" "}
{devices.length > 1
? `(${devices.length} GPUs)`
: `(${devices[0].name ?? "GPU"})`}
</span>
<span
className={cn("tabular-nums", usageTextClass(ramPercent))}
className={cn(
"shrink-0 tabular-nums",
vramUsageKnown
? usageTextClass(vramPercent)
: "text-muted-foreground",
)}
>
{Math.round(ramPercent)}%
{vramUsageKnown ? `${Math.round(vramPercent)}%` : "--"}
</span>
</div>
<div className="text-xs text-muted-foreground font-mono tabular-nums">
{formatGiB(ramUsed)} / {formatGiB(ramTotal)}
{vramUsageKnown ? formatGiB(vramUsed) : unknownLabel} /{" "}
{formatGiB(vramTotal)}
</div>
<Progress
value={ramPercent}
value={vramUsageKnown ? vramPercent : 0}
className="mt-1 h-1.5 rounded-full bg-muted"
indicatorClassName={usageIndicatorClass(ramPercent)}
indicatorClassName={usageIndicatorClass(vramPercent)}
/>
</div>
{hasGpu && (
<div className="space-y-1">
<div className="flex justify-between text-ui-11 font-medium font-mono">
<span className="truncate flex-1 pr-2">
{t("settings.resources.liveMonitor.vram")}{" "}
{devices.length > 1
? `(${devices.length} GPUs)`
: `(${devices[0].name ?? "GPU"})`}
</span>
<span
className={cn(
"shrink-0 tabular-nums",
vramUsageKnown
? usageTextClass(vramPercent)
: "text-muted-foreground",
)}
>
{vramUsageKnown ? `${Math.round(vramPercent)}%` : "--"}
</span>
</div>
<div className="text-xs text-muted-foreground font-mono tabular-nums">
{vramUsageKnown ? formatGiB(vramUsed) : unknownLabel} /{" "}
{formatGiB(vramTotal)}
</div>
<Progress
value={vramUsageKnown ? vramPercent : 0}
className="mt-1 h-1.5 rounded-full bg-muted"
indicatorClassName={usageIndicatorClass(vramPercent)}
/>
</div>
)}
{separateInferenceGpu && (
<div className="flex justify-between gap-2 text-ui-11 font-mono">
<span className="text-muted-foreground">GGUF inference</span>
<span className="uppercase text-foreground">
{separateInferenceGpu.backend ?? "GPU"}
{separateInferenceGpu.available
? inferenceVramTotal
? ` · ${formatGiB(inferenceVramTotal)}`
: ""
: " · unavailable"}
</span>
</div>
)}
</motion.div>
</motion.div>
)}
{separateInferenceGpu && (
<div className="flex justify-between gap-2 text-ui-11 font-mono">
<span className="text-muted-foreground">GGUF inference</span>
<span className="uppercase text-foreground">
{separateInferenceGpu.backend ?? "GPU"}
{separateInferenceGpu.available
? inferenceVramTotal
? ` · ${formatGiB(inferenceVramTotal)}`
: ""
: " · unavailable"}
</span>
</div>
)}
</div>
</div>
</motion.div>
</div>
);
}
export function FloatingMonitor() {
const { isOpen, setIsOpen } = useMonitorOverlayStore();
const systemInfo = useSystemInfo({ enabled: isOpen, pollMs: 5000 });
const [panelKey, setPanelKey] = useState(0);
const wasOpenRef = useRef(isOpen);
// Each visible panel owns native inline resize state. Advance the key on
// close so reopening during the exit animation still mounts fresh geometry.
useEffect(() => {
if (wasOpenRef.current && !isOpen) {
setPanelKey((current) => current + 1);
}
wasOpenRef.current = isOpen;
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<FloatingMonitorPanel
key={panelKey}
systemInfo={systemInfo}
onClose={() => setIsOpen(false)}
/>
)}
</AnimatePresence>
);

View file

@ -263,6 +263,204 @@ def parse_rgb(s):
return tuple(int(x) for x in m.groups()) if m else None
def exercise_floating_monitor_geometry(page):
"""Exercise content, drag, native resize, and viewport geometry."""
monitor = page.get_by_test_id("floating-monitor")
monitor.wait_for(state = "visible", timeout = 10_000)
monitor_handle = page.get_by_test_id("floating-monitor-drag-handle")
viewport = page.viewport_size
if viewport is None:
fail("Playwright viewport unavailable for floating monitor check")
inset = 16
tolerance = 1
def monitor_box(label):
box = monitor.bounding_box()
if box is None:
fail(f"floating monitor has no bounding box during {label}")
return box
def wait_for_box(label, predicate):
deadline = time.time() + 5
box = monitor_box(label)
while not predicate(box) and time.time() < deadline:
page.wait_for_timeout(50)
box = monitor_box(label)
if not predicate(box):
fail(f"floating monitor did not settle during {label}: {box!r}")
return box
def pointer_drag(start_x, start_y, end_x, end_y):
page.mouse.move(start_x, start_y)
page.mouse.down()
page.mouse.move(end_x, end_y, steps = 10)
page.mouse.up()
page.wait_for_timeout(100)
def drag_monitor_to(x, y):
box = monitor_handle.bounding_box()
if box is None:
fail("floating monitor handle has no bounding box")
pointer_drag(
box["x"] + box["width"] / 2,
box["y"] + box["height"] / 2,
x,
y,
)
return monitor_box("drag")
def resize_monitor_to(
x,
y,
grip_inset = 8,
):
before = monitor_box("resize")
pointer_drag(
before["x"] + before["width"] - grip_inset,
before["y"] + before["height"] - grip_inset,
x,
y,
)
return before, monitor_box("resize")
def expect_close(actual, expected, label):
if abs(actual - expected) > tolerance:
fail(f"{label}: expected {expected!r}, got {actual!r}")
def is_inside(box, surface):
return (
box["x"] >= inset - tolerance
and box["y"] >= inset - tolerance
and box["x"] + box["width"] <= surface["width"] - inset + tolerance
and box["y"] + box["height"] <= surface["height"] - inset + tolerance
)
initial_box = monitor_box("initial placement")
expect_close(
initial_box["x"] + initial_box["width"],
viewport["width"] - inset,
"initial right inset",
)
expect_close(
initial_box["y"] + initial_box["height"],
viewport["height"] - inset,
"initial bottom inset",
)
# Delayed GPU rows must expand upward and retain the initial bottom anchor.
# The probe goes in the content region, where a real row is rendered.
monitor.get_by_test_id("floating-monitor-content").evaluate(
"""node => {
const probe = document.createElement("div");
probe.dataset.testid = "floating-monitor-growth-probe";
probe.style.height = "48px";
node.appendChild(probe);
}"""
)
grown_box = wait_for_box(
"content growth",
lambda box: box["height"] >= initial_box["height"] + 47,
)
expect_close(
grown_box["y"] + grown_box["height"],
viewport["height"] - inset,
"content growth bottom inset",
)
monitor.get_by_test_id("floating-monitor-growth-probe").evaluate("node => node.remove()")
initial_box = wait_for_box(
"content shrink",
lambda box: (
abs(box["height"] - initial_box["height"]) <= tolerance
and abs(box["y"] + box["height"] - viewport["height"] + inset) <= tolerance
),
)
# Chromium retains a blocked inline resize request. A subsequent drag must
# not reveal that hidden size.
_, blocked_box = resize_monitor_to(
viewport["width"] - 2,
viewport["height"] - 2,
)
expect_close(blocked_box["width"], initial_box["width"], "blocked width")
expect_close(blocked_box["height"], initial_box["height"], "blocked height")
left_box = drag_monitor_to(0, viewport["height"] / 2)
expect_close(left_box["x"], inset, "left inset")
expect_close(left_box["width"], initial_box["width"], "post-drag width")
expect_close(left_box["height"], initial_box["height"], "post-drag height")
right_box = drag_monitor_to(viewport["width"], viewport["height"] / 2)
expect_close(
right_box["x"] + right_box["width"],
viewport["width"] - inset,
"right inset",
)
# Constraint changes during pointer capture must rebase the active drag.
handle_box = monitor_handle.bounding_box()
if handle_box is None:
fail("floating monitor handle has no active-drag bounding box")
page.mouse.move(
handle_box["x"] + handle_box["width"] / 2,
handle_box["y"] + handle_box["height"] / 2,
)
page.mouse.down()
reduced_viewport = {"width": 500, "height": 400}
page.set_viewport_size(reduced_viewport)
page.mouse.move(498, 398, steps = 10)
page.mouse.up()
wait_for_box(
"active viewport shrink",
lambda box: is_inside(box, reduced_viewport),
)
narrow_viewport = {"width": 260, "height": 400}
page.set_viewport_size(narrow_viewport)
wait_for_box("narrow viewport", lambda box: is_inside(box, narrow_viewport))
page.set_viewport_size(viewport)
wait_for_box("viewport restore", lambda box: is_inside(box, viewport))
resize_start = drag_monitor_to(0, 0)
_, resized_box = resize_monitor_to(
resize_start["x"] + resize_start["width"] - 8 + 40,
resize_start["y"] + resize_start["height"] - 8 + 30,
)
expect_close(resized_box["width"], resize_start["width"] + 40, "resize width")
expect_close(resized_box["height"], resize_start["height"] + 30, "resize height")
expect_close(resized_box["x"], resize_start["x"], "resize left edge")
expect_close(resized_box["y"], resize_start["y"], "resize top edge")
_, minimum_box = resize_monitor_to(
resized_box["x"] + resized_box["width"] - 102,
resized_box["y"] + resized_box["height"] - 102,
grip_inset = 2,
)
expect_close(minimum_box["width"], resize_start["width"], "minimum width")
expect_close(minimum_box["height"], resize_start["height"], "minimum height")
drag_monitor_to(0, 0)
_, maximum_box = resize_monitor_to(
viewport["width"] - 2,
viewport["height"] - 2,
)
expect_close(
maximum_box["x"] + maximum_box["width"],
viewport["width"] - inset,
"maximum resize right inset",
)
expect_close(
maximum_box["y"] + maximum_box["height"],
viewport["height"] - inset,
"maximum resize bottom inset",
)
# Do not leave the maximum-size overlay above the shutdown controls.
monitor.get_by_role("button", name = "Close").click()
monitor.wait_for(state = "hidden")
info(
"OK floating monitor preserves native resize and stays stable across "
"content, drag, and viewport changes"
)
with sync_playwright() as p:
_watchdog = install_wall_clock_watchdog(
WALL_TIMEOUT_S,
@ -1647,6 +1845,9 @@ with sync_playwright() as p:
if page.get_by_role("dialog", name = re.compile(r"^Settings$")).count() != 0:
fail("settings shortcut on /login left the dialog open after authentication")
info("OK persisted monitor stayed dormant on /login and resumed after authentication")
exercise_floating_monitor_geometry(page)
shoot("18-relogin-with-NEW2")
step("Shutdown via account menu")