fix(ui): restore status bar metrics layout and simplify Dynamic list

Drop the tab-kind badge, keep connection/forward labels from wrapping, and adapt disk chips by spare width; Dynamic card is start/stop only with port edited in the form.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 00:36:50 +08:00
1 parent b0d0e3a5f3
commit 883b240f73
2 files changed
+50 -74

No files matched your search

+49 -51
View File
@@ -209,10 +209,22 @@ function RemoteHostMetricsStrip({
transferLabel?: string | null
}) {
const { t } = useTranslation('common')
const stripRef = useRef<HTMLDivElement>(null)
const [diskSlots, setDiskSlots] = useState(1)
const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null
const tz = metrics ? timezoneLabel(metrics) : null
const tzTip = metrics ? timezoneTooltip(metrics) : null
const disks = (metrics?.disks ?? []).slice(0, 3)
const rankedDisks = useMemo(
() =>
(metrics?.disks ?? [])
.slice()
.sort((a, b) => b.percent - a.percent || a.mount.localeCompare(b.mount))
.slice(0, 5),
[metrics?.disks]
)
const visibleDisks = rankedDisks.slice(0, diskSlots)
const memDetail =
metrics?.memUsedKb != null && metrics?.memTotalKb != null
? ` (${formatBytes(metrics.memUsedKb * 1024)}/${formatBytes(metrics.memTotalKb * 1024)})`
@@ -223,30 +235,56 @@ function RemoteHostMetricsStrip({
metrics?.cpuPercent != null ? `CPU ${formatCpuPercent(metrics.cpuPercent)}` : null,
metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null,
transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null,
...disks.map(
...rankedDisks.map(
(d) =>
`${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})`
),
].filter(Boolean)
useEffect(() => {
const el = stripRef.current
const parent = el?.parentElement
if (!el || !parent) return
const updateSlots = () => {
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
const spare = spacer?.clientWidth ?? 0
// ~70px per chip; fullscreen usually has room for top 5.
if (spare >= 360) setDiskSlots(5)
else if (spare >= 290) setDiskSlots(4)
else if (spare >= 220) setDiskSlots(3)
else if (spare >= 150) setDiskSlots(2)
else if (spare >= 72) setDiskSlots(1)
else setDiskSlots(0)
}
updateSlots()
const observer = new ResizeObserver(updateSlots)
observer.observe(parent)
const spacer = parent.querySelector<HTMLElement>('[data-statusbar-spacer]')
if (spacer) observer.observe(spacer)
return () => observer.disconnect()
}, [rankedDisks.length, osLabel, tz, transferLabel, metrics?.cpuPercent, metrics?.memPercent])
const hasAny =
osLabel ||
tz ||
metrics?.cpuPercent != null ||
metrics?.memPercent != null ||
transferLabel ||
disks.length > 0
rankedDisks.length > 0
if (!hasAny) return null
return (
<div
className={`ml-3 flex min-w-0 max-w-[58vw] items-center gap-2 overflow-hidden font-mono tabular-nums ${
ref={stripRef}
className={`ml-3 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap font-mono tabular-nums ${
stale ? 'opacity-60' : 'opacity-100'
}`}
title={tipParts.join(' · ')}
>
{osLabel && (
<span className="text-content-secondary max-w-[11rem] shrink-0 truncate" title={osLabel}>
<span className="text-content-secondary max-w-[10rem] shrink truncate" title={osLabel}>
{osLabel}
</span>
)}
@@ -265,8 +303,8 @@ function RemoteHostMetricsStrip({
{transferLabel}
</span>
)}
{disks.map((d) => (
<span key={d.mount} className={`hidden shrink-0 lg:inline ${pctClass(d.percent)}`}>
{visibleDisks.map((d) => (
<span key={d.mount} className={`shrink-0 ${pctClass(d.percent)}`}>
{shortMount(d.mount)} {d.percent}%
</span>
))}
@@ -296,7 +334,7 @@ export default function StatusBar() {
const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId])
const { connectionState, hostLabel, stats, tabKind, connectionId, isRemoteSsh, sessionOsPretty } =
const { connectionState, hostLabel, stats, connectionId, isRemoteSsh, sessionOsPretty } =
useMemo(() => {
if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') {
return {
@@ -779,18 +817,6 @@ export default function StatusBar() {
toast,
])
const handleDynamicPortChange = useCallback(
(port: number) => {
if (forwardSaving || socksRunning) return
const nextPort = resolveDynamicPort(port)
const next = forwardRules.map((rule) =>
rule.type === 'dynamic' ? { ...rule, localPort: nextPort } : rule
)
void persistForwardRules(next)
},
[forwardRules, forwardSaving, persistForwardRules, socksRunning]
)
const handleCopySocks = useCallback(
async (event: { preventDefault: () => void }) => {
event.preventDefault()
@@ -825,28 +851,6 @@ export default function StatusBar() {
const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected
const statusLabel = t(statusCfg.labelKey)
const tabKindLabel = useMemo(() => {
if (!tabKind || tabKind === 'welcome' || tabKind === 'settings') return null
switch (tabKind) {
case 'terminal':
return t('common:tab.kind.terminal')
case 'sftp':
return t('common:tab.kind.sftp')
case 'portforward':
return t('common:tab.kind.portforward')
case 'serial':
return t('common:tab.kind.serial')
case 'rdp':
return t('common:tab.kind.rdp')
case 'vnc':
return t('common:tab.kind.vnc')
case 'batch':
return t('common:tab.kind.batch')
default:
return tabKind
}
}, [tabKind, t])
const transferLabel =
stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null
@@ -855,7 +859,7 @@ export default function StatusBar() {
<button
type="button"
onClick={togglePanel}
className={`-ml-1 flex cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-0.5 transition-colors
className={`-ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors
hover:bg-dark-700/70 group
${statusCfg.colorClass} ${statusCfg.animateClass}`}
title={t('common:connectionPanel.title')}
@@ -870,11 +874,6 @@ export default function StatusBar() {
</span>
</>
)}
{tabKindLabel && (
<span className="bg-dark-700/80 text-txt-faint ml-1.5 rounded px-1.5 py-px text-[10px] capitalize">
{tabKindLabel}
</span>
)}
{totalConnections > 0 && (
<span className="bg-accent-blue/20 ml-1 rounded-full px-1.5 py-px text-[10px] font-medium tabular-nums text-accent-blue">
{totalConnections}
@@ -892,7 +891,7 @@ export default function StatusBar() {
/>
)}
<div className="flex-1" />
<div data-statusbar-spacer className="min-w-2 flex-1" />
{socksAvailable && (
<button
@@ -907,7 +906,7 @@ export default function StatusBar() {
}
aria-label={t('common:statusBar.forwardOpenIdle', { port: dynamicListenPort })}
aria-pressed={socksRunning}
className={`mr-2 flex items-center gap-1 rounded-md px-1.5 py-0.5 transition-colors disabled:opacity-50
className={`mr-2 flex shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors disabled:opacity-50
${
socksRunning
? 'bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30'
@@ -950,7 +949,6 @@ export default function StatusBar() {
onStartRule={(rule) => void handleStartRule(rule)}
onStopRule={(rule) => void handleStopRule(rule)}
onDeleteRule={(rule) => void handleDeleteRule(rule)}
onDynamicPortChange={handleDynamicPortChange}
/>
</div>
)