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

@@ -2,7 +2,7 @@
* Right drawer for SSH port forwarding (SessionDialog-style). * Right drawer for SSH port forwarding (SessionDialog-style).
* Portaled to document.body so TabContent overflow-hidden cannot clip it. * Portaled to document.body so TabContent overflow-hidden cannot clip it.
* *
* Dynamic is a single SOCKS5 on 127.0.0.1 (default port 1080, editable when stopped). * Dynamic is a single SOCKS5 on 127.0.0.1 (default port 1080; port set in the form below).
* Local / Remote rules appear in the list and can be added repeatedly. * Local / Remote rules appear in the list and can be added repeatedly.
*/ */
@@ -49,8 +49,6 @@ interface ForwardRuleDrawerProps {
onStartRule?: (rule: ForwardRule) => void onStartRule?: (rule: ForwardRule) => void
onStopRule?: (rule: ForwardRule) => void onStopRule?: (rule: ForwardRule) => void
onDeleteRule?: (rule: ForwardRule) => void onDeleteRule?: (rule: ForwardRule) => void
/** Persist dynamic listen port while stopped (list mode). */
onDynamicPortChange?: (port: number) => void
} }
const EMPTY_DRAFT: ForwardRuleDraft = { const EMPTY_DRAFT: ForwardRuleDraft = {
@@ -103,7 +101,6 @@ export default function ForwardRuleDrawer({
onStartRule, onStartRule,
onStopRule, onStopRule,
onDeleteRule, onDeleteRule,
onDynamicPortChange,
}: ForwardRuleDrawerProps) { }: ForwardRuleDrawerProps) {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const type = (draft.type || 'local') as ForwardRuleType const type = (draft.type || 'local') as ForwardRuleType
@@ -244,25 +241,6 @@ export default function ForwardRuleDrawer({
<p className="mt-0.5 font-mono text-[11px] text-txt-muted"> <p className="mt-0.5 font-mono text-[11px] text-txt-muted">
127.0.0.1:{dynamicPort} 127.0.0.1:{dynamicPort}
</p> </p>
{!dynamicRunning && dynamicRule && onDynamicPortChange && (
<div className="mt-2 flex items-center gap-2">
<label className="text-[10px] text-txt-faint">{t('portForward.localListenPort')}</label>
<input
type="number"
min={1}
max={65535}
disabled={busy}
value={resolveDynamicPort(dynamicRule.localPort)}
onChange={(e) => onDynamicPortChange(Number(e.target.value))}
className="ui-input h-7 w-24 py-0 font-mono text-[11px]"
inputMode="numeric"
title={t('portForward.dynamicPortHint')}
/>
</div>
)}
<p className="mt-1 text-[10px] leading-relaxed text-txt-faint">
{t('portForward.dynamicSingleHint')}
</p>
</div> </div>
<div className="flex shrink-0 gap-1"> <div className="flex shrink-0 gap-1">
{dynamicRule && dynamicRunning && onStopRule && ( {dynamicRule && dynamicRunning && onStopRule && (
+49 -51
View File
@@ -209,10 +209,22 @@ function RemoteHostMetricsStrip({
transferLabel?: string | null transferLabel?: string | null
}) { }) {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const stripRef = useRef<HTMLDivElement>(null)
const [diskSlots, setDiskSlots] = useState(1)
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
const tzTip = metrics ? timezoneTooltip(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 = const memDetail =
metrics?.memUsedKb != null && metrics?.memTotalKb != null metrics?.memUsedKb != null && metrics?.memTotalKb != null
? ` (${formatBytes(metrics.memUsedKb * 1024)}/${formatBytes(metrics.memTotalKb * 1024)})` ? ` (${formatBytes(metrics.memUsedKb * 1024)}/${formatBytes(metrics.memTotalKb * 1024)})`
@@ -223,30 +235,56 @@ function RemoteHostMetricsStrip({
metrics?.cpuPercent != null ? `CPU ${formatCpuPercent(metrics.cpuPercent)}` : null, metrics?.cpuPercent != null ? `CPU ${formatCpuPercent(metrics.cpuPercent)}` : null,
metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null, metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null,
transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null, transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null,
...disks.map( ...rankedDisks.map(
(d) => (d) =>
`${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})` `${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})`
), ),
].filter(Boolean) ].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 = const hasAny =
osLabel || osLabel ||
tz || tz ||
metrics?.cpuPercent != null || metrics?.cpuPercent != null ||
metrics?.memPercent != null || metrics?.memPercent != null ||
transferLabel || transferLabel ||
disks.length > 0 rankedDisks.length > 0
if (!hasAny) return null if (!hasAny) return null
return ( return (
<div <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' stale ? 'opacity-60' : 'opacity-100'
}`} }`}
title={tipParts.join(' · ')} title={tipParts.join(' · ')}
> >
{osLabel && ( {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} {osLabel}
</span> </span>
)} )}
@@ -265,8 +303,8 @@ function RemoteHostMetricsStrip({
{transferLabel} {transferLabel}
</span> </span>
)} )}
{disks.map((d) => ( {visibleDisks.map((d) => (
<span key={d.mount} className={`hidden shrink-0 lg:inline ${pctClass(d.percent)}`}> <span key={d.mount} className={`shrink-0 ${pctClass(d.percent)}`}>
{shortMount(d.mount)} {d.percent}% {shortMount(d.mount)} {d.percent}%
</span> </span>
))} ))}
@@ -296,7 +334,7 @@ export default function StatusBar() {
const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId]) 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(() => { useMemo(() => {
if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') { if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') {
return { return {
@@ -779,18 +817,6 @@ export default function StatusBar() {
toast, 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( const handleCopySocks = useCallback(
async (event: { preventDefault: () => void }) => { async (event: { preventDefault: () => void }) => {
event.preventDefault() event.preventDefault()
@@ -825,28 +851,6 @@ export default function StatusBar() {
const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected
const statusLabel = t(statusCfg.labelKey) 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 = const transferLabel =
stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null
@@ -855,7 +859,7 @@ export default function StatusBar() {
<button <button
type="button" type="button"
onClick={togglePanel} 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 hover:bg-dark-700/70 group
${statusCfg.colorClass} ${statusCfg.animateClass}`} ${statusCfg.colorClass} ${statusCfg.animateClass}`}
title={t('common:connectionPanel.title')} title={t('common:connectionPanel.title')}
@@ -870,11 +874,6 @@ export default function StatusBar() {
</span> </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 && ( {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"> <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} {totalConnections}
@@ -892,7 +891,7 @@ export default function StatusBar() {
/> />
)} )}
<div className="flex-1" /> <div data-statusbar-spacer className="min-w-2 flex-1" />
{socksAvailable && ( {socksAvailable && (
<button <button
@@ -907,7 +906,7 @@ export default function StatusBar() {
} }
aria-label={t('common:statusBar.forwardOpenIdle', { port: dynamicListenPort })} aria-label={t('common:statusBar.forwardOpenIdle', { port: dynamicListenPort })}
aria-pressed={socksRunning} 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 socksRunning
? 'bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30' ? '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)} onStartRule={(rule) => void handleStartRule(rule)}
onStopRule={(rule) => void handleStopRule(rule)} onStopRule={(rule) => void handleStopRule(rule)}
onDeleteRule={(rule) => void handleDeleteRule(rule)} onDeleteRule={(rule) => void handleDeleteRule(rule)}
onDynamicPortChange={handleDynamicPortChange}
/> />
</div> </div>
) )