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:
parent
937919b17f
commit
ecb80670e5
3 changed files with 92 additions and 85 deletions
|
|
@ -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]"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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")}>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue