Studio: size the GPU row to its pane and fix the docs link name

- The device row keyed its layout off the viewport, but the Settings
  dialog caps the tab at roughly 650px no matter how wide the window is,
  so on every desktop the row stayed horizontal and squeezed the device
  name down to a fragment. It now uses a container query against the
  pane and only goes to one row when there is genuinely space.
- The docs link carried an aria-label that replaced its visible
  "unsloth start" text as the accessible name, so it could not be
  targeted by the label users can see. The title keeps the description.
- Dropped a stale agent from the swap hint so it matches the picker.
This commit is contained in:
Unsloth 2026-07-29 00:12:37 -07:00
commit ecb80670e5
3 changed files with 92 additions and 85 deletions

View file

@ -1094,12 +1094,13 @@ export function AgentsTab() {
data-settings-label={t("settings.agents.intro")}
className="text-sm text-muted-foreground leading-relaxed"
>
{/* The chip is the docs entry point, so no separate link is needed. */}
{/* The chip is the docs entry point, so no separate link is needed.
No aria-label: it would replace the visible "unsloth start" as the
accessible name, leaving voice control unable to target it. */}
<a
href={DOCS_URL}
target="_blank"
rel="noopener noreferrer"
aria-label={t("settings.agents.readDocs")}
title={t("settings.agents.readDocs")}
className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em] text-foreground underline decoration-border decoration-dotted underline-offset-2 transition-colors hover:decoration-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:bg-white/[0.08]"
>

View file

@ -456,92 +456,99 @@ export function ResourcesTab() {
</span>
</div>
)}
{hasGpu ? (
metrics.devices.map((device, index) => {
const ordinal = deviceOrdinal(device);
// Preserve null (unknown, e.g. Windows ROCm perf counter); coercing
// to 0 would render a fabricated 0 used / full free.
const total = device.memory_total_gb ?? null;
const used = device.vram_used_gb ?? null;
const free =
device.vram_free_gb ??
(isFiniteNumber(total) && isFiniteNumber(used)
? Math.max(0, total - used)
: null);
const percent =
device.vram_utilization_pct ??
(isFiniteNumber(total) && total > 0 && isFiniteNumber(used)
? (used / total) * 100
: null);
const safePercent = clampPercent(percent);
const usedText = isFiniteNumber(used)
? formatGiB(used)
: unknownLabel;
const freeText = isFiniteNumber(free)
? formatGiB(free)
: unknownLabel;
const totalText = isFiniteNumber(total)
? formatGiB(total)
: unknownLabel;
const percentText = isFiniteNumber(percent)
? formatPercent(safePercent)
: unknownLabel;
return (
// One row per device: a full-width bar under a single GPU read
// as a page-wide rule, so the bar is sized to the row instead.
<div
key={`${device.index ?? index}-${device.name ?? "gpu"}`}
className="flex min-w-0 items-center gap-4 py-3 max-lg:flex-col max-lg:items-stretch max-lg:gap-2"
>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-foreground">
{device.name ?? t("settings.resources.gpu.unknownDevice")}
{/* Sized against this pane, not the viewport: the Settings dialog caps
the tab at roughly 650px however wide the window is. */}
<div className="@container">
{hasGpu ? (
metrics.devices.map((device, index) => {
const ordinal = deviceOrdinal(device);
// Preserve null (unknown, e.g. Windows ROCm perf counter); coercing
// to 0 would render a fabricated 0 used / full free.
const total = device.memory_total_gb ?? null;
const used = device.vram_used_gb ?? null;
const free =
device.vram_free_gb ??
(isFiniteNumber(total) && isFiniteNumber(used)
? Math.max(0, total - used)
: null);
const percent =
device.vram_utilization_pct ??
(isFiniteNumber(total) && total > 0 && isFiniteNumber(used)
? (used / total) * 100
: null);
const safePercent = clampPercent(percent);
const usedText = isFiniteNumber(used)
? formatGiB(used)
: unknownLabel;
const freeText = isFiniteNumber(free)
? formatGiB(free)
: unknownLabel;
const totalText = isFiniteNumber(total)
? formatGiB(total)
: unknownLabel;
const percentText = isFiniteNumber(percent)
? formatPercent(safePercent)
: unknownLabel;
return (
// One row per device: a full-width bar under a single GPU read
// as a page-wide rule, so the bar is sized to the row instead.
<div
key={`${device.index ?? index}-${device.name ?? "gpu"}`}
className="flex min-w-0 flex-col items-stretch gap-2 py-3 @[50rem]:flex-row @[50rem]:items-center @[50rem]:gap-4"
>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-foreground">
{device.name ?? t("settings.resources.gpu.unknownDevice")}
</div>
<div className="mt-0.5 truncate text-xs text-muted-foreground">
{ordinal === undefined
? backendLabel
: `${t("settings.resources.gpu.deviceWithIndex", {
index: ordinal,
})}, ${backendLabel}`}
</div>
</div>
<div className="mt-0.5 truncate text-xs text-muted-foreground">
{ordinal === undefined
? backendLabel
: `${t("settings.resources.gpu.deviceWithIndex", {
index: ordinal,
})}, ${backendLabel}`}
</div>
</div>
{/* Narrow screens stack instead of hiding: the used, free and
total figures stay reachable on tablets and small laptops. */}
<div className="flex shrink-0 items-center gap-4 max-lg:flex-col max-lg:items-stretch max-lg:gap-2">
<div className="flex shrink-0 gap-3 font-mono text-xs tabular-nums text-muted-foreground max-lg:justify-between">
<span className="truncate">
{t("settings.resources.gpu.used", { value: usedText })}
</span>
<span className="truncate">
{t("settings.resources.gpu.free", { value: freeText })}
</span>
<span className="truncate">
{t("settings.resources.gpu.total", { value: totalText })}
</span>
</div>
<div className="flex min-w-0 items-center gap-4">
{/* Fills the space left by the percentage rather than
{/* Stacked until the pane is genuinely wide enough: the
figures, meter and percentage need about 50rem before the
name has room left to read. */}
<div className="flex flex-col items-stretch gap-2 @[50rem]:flex-row @[50rem]:shrink-0 @[50rem]:items-center @[50rem]:gap-4">
<div className="flex justify-between gap-3 font-mono text-xs tabular-nums text-muted-foreground @[50rem]:shrink-0 @[50rem]:justify-start">
<span className="truncate">
{t("settings.resources.gpu.used", { value: usedText })}
</span>
<span className="truncate">
{t("settings.resources.gpu.free", { value: freeText })}
</span>
<span className="truncate">
{t("settings.resources.gpu.total", {
value: totalText,
})}
</span>
</div>
<div className="flex min-w-0 items-center gap-4">
{/* Fills the space left by the percentage rather than
claiming the full row, which overflowed once stacked. */}
<Progress
value={safePercent}
aria-label={device.name ?? "GPU"}
className="h-1.5 min-w-0 flex-1 rounded-full bg-muted lg:w-40 lg:flex-none dark:bg-black/40"
indicatorClassName={usageIndicatorClass(safePercent)}
/>
<div className="w-[5.5rem] shrink-0 text-right font-mono text-xs tabular-nums text-muted-foreground">
{percentText}{" "}
{t("settings.resources.gpu.vramUtilization")}
<Progress
value={safePercent}
aria-label={device.name ?? "GPU"}
className="h-1.5 min-w-0 flex-1 rounded-full bg-muted @[50rem]:w-40 @[50rem]:flex-none dark:bg-black/40"
indicatorClassName={usageIndicatorClass(safePercent)}
/>
<div className="w-[5.5rem] shrink-0 text-right font-mono text-xs tabular-nums text-muted-foreground">
{percentText}{" "}
{t("settings.resources.gpu.vramUtilization")}
</div>
</div>
</div>
</div>
</div>
);
})
) : (
<div className="py-3 text-sm text-muted-foreground">
{t("settings.resources.gpu.noGpu")}
</div>
)}
);
})
) : (
<div className="py-3 text-sm text-muted-foreground">
{t("settings.resources.gpu.noGpu")}
</div>
)}
</div>
</SettingsSection>
<SettingsSection title={t("settings.resources.storage.title")}>

View file

@ -843,8 +843,7 @@ export const en = {
codingAgents: "Coding agents",
codingAgentsHint:
"Launch a coding agent against this server. It uses the loaded model; a local server mints an API key automatically, a remote one includes it in the command.",
codingAgentsSwap:
"Swap claude for codex, openclaw, opencode, hermes, or pi.",
codingAgentsSwap: "Swap claude for codex, openclaw, opencode, or hermes.",
codingAgentDetected: "Installed on this machine",
codingAgentsDetectedHint: "Detected on this machine: {agents}.",
relativeNever: "never",