fix(ui): stop status-bar disk chips from oscillating
Measure spare width including visible disk chips and apply hysteresis so adaptive slot count no longer flicker. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
883b240f73
commit
da95113e6d
1 file changed
+56
-15
@@ -197,6 +197,34 @@ function useRemoteHostMetrics(connectionId: string | undefined, enabled: boolean
|
|||||||
return { metrics, stale }
|
return { metrics, stale }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DISK_SLOT_THRESHOLDS = [
|
||||||
|
{ slots: 5, min: 360 },
|
||||||
|
{ slots: 4, min: 290 },
|
||||||
|
{ slots: 3, min: 220 },
|
||||||
|
{ slots: 2, min: 150 },
|
||||||
|
{ slots: 1, min: 72 },
|
||||||
|
{ slots: 0, min: 0 },
|
||||||
|
] as const
|
||||||
|
/** Deadband so showing/hiding disks cannot oscillate slot count. */
|
||||||
|
const DISK_SLOT_HYSTERESIS_PX = 36
|
||||||
|
|
||||||
|
function diskSlotsForSpare(spare: number, current: number, withHysteresis: boolean): number {
|
||||||
|
let next = 0
|
||||||
|
for (const row of DISK_SLOT_THRESHOLDS) {
|
||||||
|
if (spare >= row.min) {
|
||||||
|
next = row.slots
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!withHysteresis || next === current) return next
|
||||||
|
if (next > current) {
|
||||||
|
const need = DISK_SLOT_THRESHOLDS.find((row) => row.slots === next)?.min ?? 0
|
||||||
|
return spare >= need + DISK_SLOT_HYSTERESIS_PX ? next : current
|
||||||
|
}
|
||||||
|
const keep = DISK_SLOT_THRESHOLDS.find((row) => row.slots === current)?.min ?? 0
|
||||||
|
return spare < keep - DISK_SLOT_HYSTERESIS_PX ? next : current
|
||||||
|
}
|
||||||
|
|
||||||
function RemoteHostMetricsStrip({
|
function RemoteHostMetricsStrip({
|
||||||
metrics,
|
metrics,
|
||||||
stale,
|
stale,
|
||||||
@@ -210,7 +238,9 @@ function RemoteHostMetricsStrip({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
const stripRef = useRef<HTMLDivElement>(null)
|
const stripRef = useRef<HTMLDivElement>(null)
|
||||||
const [diskSlots, setDiskSlots] = useState(1)
|
const disksRef = useRef<HTMLDivElement>(null)
|
||||||
|
const measuredRef = useRef(false)
|
||||||
|
const [diskSlots, setDiskSlots] = useState(0)
|
||||||
|
|
||||||
const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null
|
const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null
|
||||||
const tz = metrics ? timezoneLabel(metrics) : null
|
const tz = metrics ? timezoneLabel(metrics) : null
|
||||||
@@ -248,14 +278,12 @@ function RemoteHostMetricsStrip({
|
|||||||
|
|
||||||
const updateSlots = () => {
|
const updateSlots = () => {
|
||||||
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
|
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
|
||||||
const spare = spacer?.clientWidth ?? 0
|
const disksEl = disksRef.current
|
||||||
// ~70px per chip; fullscreen usually has room for top 5.
|
// spacer shrinks when disks appear — add disks width back so capacity is stable.
|
||||||
if (spare >= 360) setDiskSlots(5)
|
const spare = (spacer?.clientWidth ?? 0) + (disksEl?.offsetWidth ?? 0)
|
||||||
else if (spare >= 290) setDiskSlots(4)
|
const useHysteresis = measuredRef.current
|
||||||
else if (spare >= 220) setDiskSlots(3)
|
measuredRef.current = true
|
||||||
else if (spare >= 150) setDiskSlots(2)
|
setDiskSlots((prev) => diskSlotsForSpare(spare, prev, useHysteresis))
|
||||||
else if (spare >= 72) setDiskSlots(1)
|
|
||||||
else setDiskSlots(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSlots()
|
updateSlots()
|
||||||
@@ -264,7 +292,18 @@ function RemoteHostMetricsStrip({
|
|||||||
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
|
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
|
||||||
if (spacer) observer.observe(spacer)
|
if (spacer) observer.observe(spacer)
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [rankedDisks.length, osLabel, tz, transferLabel, metrics?.cpuPercent, metrics?.memPercent])
|
}, [])
|
||||||
|
|
||||||
|
// Disk chips mount after first metrics poll; re-check once data arrives.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rankedDisks.length) return
|
||||||
|
const parent = stripRef.current?.parentElement
|
||||||
|
if (!parent) return
|
||||||
|
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
|
||||||
|
const spare = (spacer?.clientWidth ?? 0) + (disksRef.current?.offsetWidth ?? 0)
|
||||||
|
setDiskSlots((prev) => diskSlotsForSpare(spare, prev, measuredRef.current))
|
||||||
|
measuredRef.current = true
|
||||||
|
}, [rankedDisks.length])
|
||||||
|
|
||||||
const hasAny =
|
const hasAny =
|
||||||
osLabel ||
|
osLabel ||
|
||||||
@@ -303,11 +342,13 @@ function RemoteHostMetricsStrip({
|
|||||||
{transferLabel}
|
{transferLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{visibleDisks.map((d) => (
|
<div ref={disksRef} className="flex shrink-0 items-center gap-2">
|
||||||
<span key={d.mount} className={`shrink-0 ${pctClass(d.percent)}`}>
|
{visibleDisks.map((d) => (
|
||||||
{shortMount(d.mount)} {d.percent}%
|
<span key={d.mount} className={`shrink-0 ${pctClass(d.percent)}`}>
|
||||||
</span>
|
{shortMount(d.mount)} {d.percent}%
|
||||||
))}
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user