Studio: tidy up the Agents, System and Connections settings

Agents tab
- Coding agent, model and quantization now sit on one row of three
  columns instead of stacking.
- Smaller agent logos so the picker reads as a list, not a row of tiles.
- Command boxes lose their outer border and carry the copy control
  inside the box, top right.
- The `unsloth start` chip is now the link to the docs.
- Quantization dropdown drops the extra tags that wrapped every row onto
  two lines, and the size sits flush right so the list scans cleanly.
- More breathing room between sections.

System tab
- Rounder metric cards with more padding, and no resting border in dark
  mode.
- Darker meter track in dark mode so the bars stand out again.
- GPU devices collapse to one row per device; a full width bar under a
  single GPU read like a page-wide rule.

Connections
- The add-provider box matches the corner radius used elsewhere.

Appearance
- Colour palette cards drop the resting border in dark mode, matching
  the rest of Settings. Hover and selected states are unchanged.
This commit is contained in:
Unsloth 2026-07-28 22:56:16 -07:00
commit dfed6a6921
9 changed files with 256 additions and 285 deletions

View file

@ -84,9 +84,8 @@ Replace `claude` with any supported agent:
| Hermes Agent | `unsloth start hermes` |
| OpenClaw | `unsloth start openclaw` |
| OpenCode | `unsloth start opencode` |
| Pi Coding Agent | `unsloth start pi` |
Claude Code, Codex, OpenCode and Pi can keep their current model and use Unsloth as a local
Claude Code, Codex and OpenCode can keep their current model and use Unsloth as a local
subagent:
```bash
@ -204,7 +203,7 @@ Read our [guide](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide). Ad
## 🦥 Unsloth News
- **AMD training**: Train, run RL, chat and deploy on AMD GPUs across Windows, WSL and Linux. [Guide](https://unsloth.ai/docs/basics/amd)
- **GGUF hardware controls**: Choose GPU/layer placement, offload MoE experts and use multi-GPU or Tensor Parallelism. [#6414](https://github.com/unslothai/unsloth/pull/6414)
- **Local models for any agent**: Use `unsloth start` with Claude Code, Codex, Hermes, OpenCode, OpenClaw, Pi and more through Unsloth's OpenAI- and Anthropic-compatible APIs. [Guide](https://unsloth.ai/docs/basics/api)
- **Local models for any agent**: Use `unsloth start` with Claude Code, Codex, Hermes, OpenCode, OpenClaw and more through Unsloth's OpenAI- and Anthropic-compatible APIs. [Guide](https://unsloth.ai/docs/basics/api)
- **MCP control endpoint**: Let compatible clients manage models, training, recipes, checkpoints and exports. [#7191](https://github.com/unslothai/unsloth/pull/7191)
- **Local inference reliability**: Resume long chats faster, recover stalled downloads and reuse existing GGUF files. [#7204](https://github.com/unslothai/unsloth/pull/7204) • [#6858](https://github.com/unslothai/unsloth/pull/6858) • [#7209](https://github.com/unslothai/unsloth/pull/7209)
- **New models**: [Qwen-AgentWorld](https://huggingface.co/unsloth/Qwen-AgentWorld-35B-A3B-GGUF), [Ornith](https://huggingface.co/unsloth/models?search=ornith), [Kimi K2.7 Code](https://unsloth.ai/docs/models/kimi-k2.7-code) and [MiniMax M3](https://unsloth.ai/docs/models/minimax-m3)

View file

@ -1,21 +0,0 @@
<!-- Source: https://pi.dev/favicon.svg (official Pi press-kit badge) -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
<rect width="800" height="800" rx="120" fill="#09090b"/>
<path fill="#fff" fill-rule="evenodd" d="
M165.29 165.29
H517.36
V400
H400
V517.36
H282.65
V634.72
H165.29
Z
M282.65 282.65
V400
H400
V282.65
Z
"/>
<path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 475 B

View file

@ -1454,7 +1454,7 @@ export function ChatProvidersSettings({
</div>
<section className="flex max-w-[760px] flex-col gap-2">
<div className="overflow-hidden rounded-[10px] border border-border/70 bg-muted/[0.12]">
<div className="overflow-hidden rounded-[14px] border border-border/70 bg-muted/[0.12]">
<button
type="button"
onClick={openAddProvider}

View file

@ -16,6 +16,10 @@ type ApiCodingAgentsInfo = {
detected: string[];
};
// Agents the CLI still supports but Studio does not list. Filtered here so
// every consumer of this endpoint sees the same set.
const HIDDEN_AGENTS = new Set(["pi"]);
// Which CLIs are on PATH is environment state, not a persisted setting -- it
// can change any time the user installs something new, so this only
// de-duplicates concurrent in-flight calls (e.g. React strict-mode's double
@ -24,7 +28,8 @@ type ApiCodingAgentsInfo = {
let inFlightInfo: Promise<CodingAgentsInfo> | null = null;
function fromApi(info: ApiCodingAgentsInfo): CodingAgentsInfo {
return { agents: info.agents, detected: info.detected };
const visible = (ids: string[]) => ids.filter((id) => !HIDDEN_AGENTS.has(id));
return { agents: visible(info.agents), detected: visible(info.detected) };
}
async function fetchCodingAgents(): Promise<CodingAgentsInfo> {

View file

@ -106,16 +106,15 @@ const DOC_LINKS = [
{ label: "Hermes Agent", href: "https://unsloth.ai/docs/integrations/hermes-agent" },
];
// Falls back to this list until the backend's installed-CLI check resolves;
// kept in sync with the `unsloth start <agent>` subcommands and with
// CODING_AGENTS in studio/backend/utils/coding_agents.py.
// Fallback until the backend's installed-CLI check resolves. Mirrors
// CODING_AGENTS in studio/backend/utils/coding_agents.py, minus HIDDEN_AGENTS
// (see ../api/coding-agents.ts).
const DEFAULT_AGENTS = [
"claude",
"codex",
"openclaw",
"opencode",
"hermes",
"pi",
];
// The agent selection resets to this whenever an auto-pick is no longer
// trustworthy (leaving loopback, or the only compatible detected agent
@ -127,7 +126,6 @@ const AGENT_LABELS: Record<string, string> = {
openclaw: "OpenClaw",
opencode: "OpenCode",
hermes: "Hermes",
pi: "Pi",
};
const j = (s: string): string => JSON.stringify(s);

View file

@ -41,11 +41,7 @@ import { ChevronDownStandardIcon } from "@/lib/chevron-icons";
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { Tick02Icon } from "@/lib/tick-icon";
import { cn } from "@/lib/utils";
import {
ArrowUpRight01Icon,
Book03Icon,
Copy01Icon,
} from "@hugeicons/core-free-icons";
import { ArrowUpRight01Icon, Copy01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ApiProviderLogo } from "../../chat/api-provider-logo";
@ -66,7 +62,7 @@ const STATUS_POLL_MS = 5000;
const HUGGING_FACE_REPO_PATTERN = /^[^/\\:\s]+\/[^/\\:\s]+$/;
const SEARCH_TOKEN_PATTERN = /\s+/;
const SAFE_SHELL_ARG_PATTERN = /^[A-Za-z0-9_./:@%+=,-]+$/;
const SUBAGENT_AGENT_IDS = new Set(["claude", "codex", "opencode", "pi"]);
const SUBAGENT_AGENT_IDS = new Set(["claude", "codex", "opencode"]);
function isLoopbackBase(base: string): boolean {
try {
@ -165,12 +161,6 @@ const SUPPORTED_AGENTS: AgentDetails[] = [
icon: "opencode-light.svg",
darkIcon: "opencode-dark.svg",
},
{
id: "pi",
name: "Pi Coding Agent",
docsUrl: DOCS_URL,
icon: "pi.svg",
},
];
const FALLBACK_AGENT = SUPPORTED_AGENTS[0];
@ -382,8 +372,8 @@ function AgentIcon({
}) {
if (logo) {
return (
<span className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md">
<ApiProviderLogo providerType={logo} className="size-7 rounded-md" />
<span className="flex size-5 shrink-0 items-center justify-center overflow-hidden rounded">
<ApiProviderLogo providerType={logo} className="size-5 rounded" />
</span>
);
}
@ -393,13 +383,13 @@ function AgentIcon({
? `${import.meta.env.BASE_URL}agent-logos/${darkIcon}`
: null;
return (
<span className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md">
<span className="flex size-5 shrink-0 items-center justify-center overflow-hidden rounded">
<img
src={iconSrc}
alt=""
aria-hidden={true}
className={cn(
"size-7 object-contain",
"size-5 object-contain",
darkIconSrc && "dark:hidden",
invertIconInDark && "dark:invert",
)}
@ -409,7 +399,7 @@ function AgentIcon({
src={darkIconSrc}
alt=""
aria-hidden={true}
className="hidden size-7 object-contain dark:block"
className="hidden size-5 object-contain dark:block"
/>
) : null}
</span>
@ -419,7 +409,7 @@ function AgentIcon({
<span
aria-hidden={true}
style={{ backgroundColor: color }}
className="flex size-7 shrink-0 items-center justify-center rounded-md font-heading text-ui-11 font-semibold text-white"
className="flex size-5 shrink-0 items-center justify-center rounded font-heading text-ui-10 font-semibold text-white"
>
{mark}
</span>
@ -473,13 +463,59 @@ const PASSTHROUGH_EXAMPLES = [
const DRY_RUN_FLAGS = "--no-launch";
/** Code box with the copy control inside it, top-right. Presentational: the
* copy state stays with the caller so existing resets still apply. */
function CopyableCode({
value,
copyLabel,
copied,
onCopy,
breakAll = true,
}: {
value: string;
copyLabel: string;
copied: boolean;
onCopy: () => void;
breakAll?: boolean;
}) {
const t = useT();
return (
<div className="relative min-w-0">
<code
className={cn(
"block min-w-0 whitespace-pre-wrap rounded-lg border border-border bg-background/70 py-2.5 pr-9 pl-4 font-mono text-ui-11 leading-relaxed text-foreground dark:border-transparent dark:bg-white/[0.05]",
breakAll ? "break-all" : "break-words",
)}
>
{value}
</code>
<button
type="button"
onClick={onCopy}
aria-label={copyLabel}
className="absolute top-1.5 right-1.5 flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<HugeiconsIcon
icon={copied ? Tick02Icon : Copy01Icon}
className={cn("size-3.5", copied && "text-control-accent")}
strokeWidth={2}
/>
</button>
<output className="sr-only" aria-live="polite">
{copied ? t("settings.agents.copied") : ""}
</output>
</div>
);
}
function CommandBlock({ command }: { command: string }) {
const t = useT();
const { copied, copy } = useCopyButton(command);
return (
<div className="group relative">
<pre className="hover-scrollbar overflow-x-auto rounded-lg border border-border bg-muted/40 py-3 pl-3.5 pr-11 text-xs leading-relaxed text-foreground dark:bg-white/[0.04]">
<div className="group relative overflow-hidden rounded-xl border border-border bg-muted/40 dark:border-transparent dark:bg-white/[0.04]">
<pre className="hover-scrollbar overflow-x-auto py-3 pr-11 pl-4 text-xs leading-relaxed text-foreground">
<code className="font-mono whitespace-pre">{command}</code>
</pre>
<button
@ -535,7 +571,7 @@ function SubagentSection({
}
return (
<div className="flex min-w-0 flex-col gap-3 rounded-lg border border-border bg-muted/10 p-3">
<div className="flex min-w-0 flex-col gap-4">
<div className="flex flex-col gap-1">
<span
data-settings-label={t("settings.agents.subagent.title")}
@ -548,60 +584,29 @@ function SubagentSection({
</p>
</div>
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex items-center justify-between gap-3">
<span className="text-ui-11 font-medium text-foreground">
{t("settings.agents.subagent.setupCommand")}
</span>
<button
type="button"
onClick={commandCopy.copy}
aria-label={t("settings.agents.subagent.copySetupCommand")}
className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-background/70 px-2 text-ui-11 font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<HugeiconsIcon
icon={commandCopy.copied ? Tick02Icon : Copy01Icon}
className={cn(
"size-3.5",
commandCopy.copied && "text-control-accent",
)}
/>
{commandCopy.copied
? t("settings.agents.copied")
: t("settings.agents.copy")}
</button>
</div>
<code className="block min-w-0 whitespace-pre-wrap break-all rounded-md border border-border bg-background/70 px-2.5 py-2 font-mono text-ui-11 leading-relaxed text-foreground">
{command}
</code>
<div className="flex min-w-0 flex-col gap-2">
<span className="text-ui-11 font-medium text-foreground">
{t("settings.agents.subagent.setupCommand")}
</span>
<CopyableCode
value={command}
copyLabel={t("settings.agents.subagent.copySetupCommand")}
copied={commandCopy.copied}
onCopy={commandCopy.copy}
/>
</div>
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex items-center justify-between gap-3">
<span className="text-ui-11 font-medium text-foreground">
{t("settings.agents.subagent.usagePrompt", { agent: agent.name })}
</span>
<button
type="button"
onClick={promptCopy.copy}
aria-label={t("settings.agents.subagent.copyUsagePrompt")}
className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-background/70 px-2 text-ui-11 font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<HugeiconsIcon
icon={promptCopy.copied ? Tick02Icon : Copy01Icon}
className={cn(
"size-3.5",
promptCopy.copied && "text-control-accent",
)}
/>
{promptCopy.copied
? t("settings.agents.copied")
: t("settings.agents.copy")}
</button>
</div>
<code className="block min-w-0 whitespace-pre-wrap break-words rounded-md border border-border bg-background/70 px-2.5 py-2 font-mono text-ui-11 leading-relaxed text-foreground">
{prompt}
</code>
<div className="flex min-w-0 flex-col gap-2">
<span className="text-ui-11 font-medium text-foreground">
{t("settings.agents.subagent.usagePrompt", { agent: agent.name })}
</span>
<CopyableCode
value={prompt}
copyLabel={t("settings.agents.subagent.copyUsagePrompt")}
copied={promptCopy.copied}
onCopy={promptCopy.copy}
breakAll={false}
/>
</div>
</div>
);
@ -665,7 +670,6 @@ export function AgentsTab() {
const [modelSearch, setModelSearch] = useState("");
const [modelPickerOpen, setModelPickerOpen] = useState(false);
const [variants, setVariants] = useState<GgufVariantDetail[]>([]);
const [defaultVariant, setDefaultVariant] = useState<string | null>(null);
const [selectedVariant, setSelectedVariant] = useState<string | null>(
EXAMPLE_MODEL_VARIANT,
);
@ -986,7 +990,6 @@ export function AgentsTab() {
return;
}
setVariants([]);
setDefaultVariant(null);
setSelectedVariant(standaloneFile ? null : preferredVariant);
setVariantsFailed(false);
setVariantsLoading(false);
@ -999,7 +1002,6 @@ export function AgentsTab() {
// A programmatic model change reaches here too, so clear the previous model's
// quants up front rather than leaving them selectable until this resolves.
setVariants([]);
setDefaultVariant(null);
setVariantsLoading(true);
// Offer the quants from the same place the command loads from, not remote-only ones.
listGgufVariants(selectedModel, hfToken || undefined, {
@ -1023,7 +1025,6 @@ export function AgentsTab() {
).values(),
);
setVariants(uniqueVariants);
setDefaultVariant(info.default_variant);
const available = new Set(
uniqueVariants.map((variant) => variant.quant),
);
@ -1045,7 +1046,6 @@ export function AgentsTab() {
}
setVariantsFailed(true);
setVariants([]);
setDefaultVariant(null);
setSelectedVariant(preferredVariant);
if (preferredVariant) {
setVariants([
@ -1073,7 +1073,7 @@ export function AgentsTab() {
// picker only ever offers GGUF models.
return (
<div className="flex min-w-0 max-w-full flex-col gap-6">
<div className="flex min-w-0 max-w-full flex-col gap-8">
{/* data-settings-label lets indexed settings search scroll to these. */}
<header className="flex min-w-0 flex-col gap-1">
<h1
@ -1094,105 +1094,104 @@ export function AgentsTab() {
data-settings-label={t("settings.agents.intro")}
className="text-sm text-muted-foreground leading-relaxed"
>
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em] text-foreground dark:bg-white/[0.08]">
{/* The chip is the docs entry point, so no separate link is needed. */}
<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]"
>
unsloth start
</code>{" "}
</a>{" "}
{t("settings.agents.intro")}
</p>
<a
href={DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
>
<HugeiconsIcon icon={Book03Icon} className="size-3.5" />
{t("settings.agents.readDocs")}
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
</a>
<section
aria-label={t("settings.agents.commandBuilder")}
className="flex w-full flex-col gap-4"
className="flex w-full flex-col gap-6"
>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between gap-3">
<span
data-settings-label={t("settings.agents.agent")}
className="text-xs font-medium text-foreground"
<div className="grid grid-cols-[minmax(0,0.8fr)_minmax(0,1fr)_minmax(9rem,0.5fr)] items-start gap-3 max-md:grid-cols-1">
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex items-center justify-between gap-3">
<span
data-settings-label={t("settings.agents.agent")}
className="text-xs font-medium text-foreground"
>
{t("settings.agents.agent")}
</span>
<a
href={selectedAgentDetails.docsUrl}
target="_blank"
rel="noreferrer"
aria-label={t("settings.agents.agentDocs", {
agent: selectedAgentDetails.name,
})}
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-ui-11 font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{t("settings.agents.docs")}
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
</a>
</div>
<Select
value={selectedAgent}
onValueChange={(agent) => {
agentSelectionChanged.current = true;
setSelectedAgent(agent);
resetCopied();
}}
>
{t("settings.agents.agent")}
</span>
<a
href={selectedAgentDetails.docsUrl}
target="_blank"
rel="noreferrer"
aria-label={t("settings.agents.agentDocs", {
agent: selectedAgentDetails.name,
})}
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-ui-11 font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{t("settings.agents.docs")}
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
</a>
</div>
<Select
value={selectedAgent}
onValueChange={(agent) => {
agentSelectionChanged.current = true;
setSelectedAgent(agent);
resetCopied();
}}
>
<SelectTrigger
aria-label={t("settings.agents.agent")}
className="w-full rounded-lg"
>
<SelectValue>
<span className="flex min-w-0 items-center gap-2">
<AgentIcon
logo={selectedAgentDetails.logo}
icon={selectedAgentDetails.icon}
darkIcon={selectedAgentDetails.darkIcon}
invertIconInDark={selectedAgentDetails.invertIconInDark}
color={selectedAgentDetails.color}
mark={selectedAgentDetails.mark}
/>
<span className="truncate">{selectedAgentDetails.name}</span>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent align="start">
{agents.map((agentId) => {
const agent = detailsFor(agentId);
return (
<SelectItem key={agent.id} value={agent.id}>
<span className="flex min-w-0 items-center gap-2">
<AgentIcon
logo={agent.logo}
icon={agent.icon}
darkIcon={agent.darkIcon}
invertIconInDark={agent.invertIconInDark}
color={agent.color}
mark={agent.mark}
/>
<span className="truncate">{agent.name}</span>
{localDetection &&
loaded &&
detectedAgents.has(agent.id) ? (
<span className="shrink-0 rounded-full bg-control-accent/10 px-2 py-1 text-ui-10 leading-none font-semibold text-control-accent">
{t("settings.agents.quickstart.installed")}
</span>
) : null}
<SelectTrigger
aria-label={t("settings.agents.agent")}
className="w-full rounded-lg"
>
<SelectValue>
<span className="flex min-w-0 items-center gap-2">
<AgentIcon
logo={selectedAgentDetails.logo}
icon={selectedAgentDetails.icon}
darkIcon={selectedAgentDetails.darkIcon}
invertIconInDark={selectedAgentDetails.invertIconInDark}
color={selectedAgentDetails.color}
mark={selectedAgentDetails.mark}
/>
<span className="truncate">
{selectedAgentDetails.name}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent align="start">
{agents.map((agentId) => {
const agent = detailsFor(agentId);
return (
<SelectItem key={agent.id} value={agent.id}>
<span className="flex min-w-0 items-center gap-2">
<AgentIcon
logo={agent.logo}
icon={agent.icon}
darkIcon={agent.darkIcon}
invertIconInDark={agent.invertIconInDark}
color={agent.color}
mark={agent.mark}
/>
<span className="truncate">{agent.name}</span>
{localDetection &&
loaded &&
detectedAgents.has(agent.id) ? (
<span className="shrink-0 rounded-full bg-control-accent/10 px-2 py-1 text-ui-10 leading-none font-semibold text-control-accent">
{t("settings.agents.quickstart.installed")}
</span>
) : null}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(10rem,0.4fr)] items-start gap-3 max-md:grid-cols-1">
<div className="flex min-w-0 flex-col gap-1.5">
<span
data-settings-label={t("settings.agents.model")}
@ -1255,7 +1254,6 @@ export function AgentsTab() {
setSelectedModel(model);
setSelectedVariant(knownVariants[model] ?? null);
setVariants([]);
setDefaultVariant(null);
setVariantsFailed(false);
setVariantsLoading(isHuggingFaceRepo(model));
setModelSearch("");
@ -1313,23 +1311,27 @@ export function AgentsTab() {
{selectedVariant}
</SelectValue>
</SelectTrigger>
<SelectContent align="start">
<SelectContent align="start" className="min-w-[16rem]">
{variants.map((variant) => {
const metadata = [
variant.quant === defaultVariant
? t("settings.agents.recommended")
: null,
variant.downloaded ? t("settings.agents.downloaded") : null,
formatBytes(
variant.download_size_bytes ?? variant.size_bytes,
),
].filter(Boolean);
// Size only: the recommended/downloaded tags wrapped every
// row onto two lines and made the list hard to scan.
const size = formatBytes(
variant.download_size_bytes ?? variant.size_bytes,
);
return (
<SelectItem key={variant.quant} value={variant.quant}>
<span className="font-mono text-xs">{variant.quant}</span>
{metadata.length > 0 ? (
<span className="text-ui-10 text-muted-foreground">
{metadata.join(" · ")}
<SelectItem
key={variant.quant}
value={variant.quant}
// Stretch the item text so the size can sit flush right,
// giving the list a clean two-column read.
className="[&>span:last-child]:w-full [&>span:last-child]:justify-between"
>
<span className="font-mono text-xs whitespace-nowrap">
{variant.quant}
</span>
{size ? (
<span className="text-ui-10 whitespace-nowrap text-muted-foreground">
{size}
</span>
) : null}
</SelectItem>
@ -1346,27 +1348,16 @@ export function AgentsTab() {
</p>
) : null}
<div className="flex min-w-0 flex-col gap-2 rounded-lg border border-border bg-muted/20 p-3">
<div className="flex items-center justify-between gap-3">
<span className="text-xs font-medium text-foreground">
{t("settings.agents.generatedCommand")}
</span>
<button
type="button"
onClick={handleCopy}
aria-label={t("settings.agents.copyGeneratedCommand")}
className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-background/70 px-2 text-ui-11 font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<HugeiconsIcon
icon={copied ? Tick02Icon : Copy01Icon}
className={cn("size-3.5", copied && "text-control-accent")}
/>
{copied ? t("settings.agents.copied") : t("settings.agents.copy")}
</button>
</div>
<code className="block min-w-0 whitespace-pre-wrap break-all rounded-md border border-border bg-background/70 px-2.5 py-2 font-mono text-ui-11 leading-relaxed text-foreground">
{command}
</code>
<div className="flex min-w-0 flex-col gap-2.5">
<span className="text-xs font-medium text-foreground">
{t("settings.agents.generatedCommand")}
</span>
<CopyableCode
value={command}
copyLabel={t("settings.agents.copyGeneratedCommand")}
copied={copied}
onCopy={handleCopy}
/>
</div>
<SubagentSection
@ -1406,7 +1397,7 @@ export function AgentsTab() {
title={t("settings.agents.remote.title")}
description={t("settings.agents.remote.description")}
>
<div className="pt-2">
<div className="pt-3">
<CommandBlock command={remoteCommand} />
</div>
</SettingsSection>
@ -1415,7 +1406,7 @@ export function AgentsTab() {
title={t("settings.agents.passthrough.title")}
description={t("settings.agents.passthrough.description")}
>
<div className="flex flex-col gap-2 pt-2">
<div className="flex flex-col gap-3 pt-3">
{PASSTHROUGH_EXAMPLES.map(({ agent, flags }) => (
<CommandBlock key={flags} command={example(agent, flags)} />
))}
@ -1426,7 +1417,7 @@ export function AgentsTab() {
title={t("settings.agents.dryRun.title")}
description={t("settings.agents.dryRun.description")}
>
<div className="pt-2">
<div className="pt-3">
<CommandBlock command={example("claude", DRY_RUN_FLAGS)} />
</div>
</SettingsSection>

View file

@ -116,7 +116,7 @@ function MetricTile({
const percentKnown = isFiniteNumber(percent);
const safePercent = clampPercent(percent);
return (
<div className="flex min-w-0 flex-col gap-2 rounded-md border border-border/60 bg-muted/20 p-3">
<div className="flex min-w-0 flex-col gap-2.5 rounded-xl border border-border/60 bg-muted/20 p-4 dark:border-transparent dark:bg-white/[0.06]">
<div className="flex items-center justify-between gap-3">
<span className="truncate text-ui-11 font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{label}
@ -141,7 +141,7 @@ function MetricTile({
<Progress
value={percentKnown ? safePercent : 0}
aria-label={label}
className="h-1.5 rounded-full bg-muted"
className="h-1.5 rounded-full bg-muted dark:bg-black/40"
indicatorClassName={usageIndicatorClass(safePercent)}
/>
</div>
@ -483,54 +483,46 @@ export function ResourcesTab() {
? 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 gap-2 py-3"
className="flex min-w-0 items-center gap-4 py-3 max-md:flex-col max-md:items-stretch max-md:gap-2"
>
<div className="flex min-w-0 items-start justify-between 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 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="shrink-0 font-mono text-xs tabular-nums text-muted-foreground">
<span>
{percentText}{" "}
{t("settings.resources.gpu.vramUtilization")}
<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="flex shrink-0 items-center gap-4 max-md:justify-between">
<div className="flex shrink-0 gap-3 font-mono text-xs tabular-nums text-muted-foreground max-lg:hidden">
<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>
<Progress
value={safePercent}
aria-label={device.name ?? "GPU"}
className="h-1.5 w-40 shrink-0 rounded-full bg-muted max-md:w-full 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 className="grid gap-1 text-xs text-muted-foreground sm:grid-cols-3 sm:gap-2">
<span className="min-w-0 truncate font-mono tabular-nums">
{t("settings.resources.gpu.used", {
value: usedText,
})}
</span>
<span className="min-w-0 truncate font-mono tabular-nums sm:text-center">
{t("settings.resources.gpu.free", {
value: freeText,
})}
</span>
<span className="min-w-0 truncate font-mono tabular-nums sm:text-right">
{t("settings.resources.gpu.total", {
value: totalText,
})}
</span>
</div>
<Progress
value={safePercent}
aria-label={device.name ?? "GPU"}
className="h-1.5 rounded-full bg-muted"
indicatorClassName={usageIndicatorClass(safePercent)}
/>
</div>
);
})

View file

@ -589,9 +589,9 @@ export const en = {
agents: {
title: "Agents",
description:
"Connect coding agents like Claude Code and Codex to a model running locally in Unsloth with unsloth start.",
"Connect coding agents like Claude Code and Codex to a local model with unsloth start.",
intro:
"connects Claude Code, Codex, Hermes, OpenClaw, OpenCode, Pi and other agents to a model served locally by Unsloth, fully offline on your own hardware. It runs an OpenAI-compatible server for the agent and never touches your agent's config files.",
"connects Claude Code, Codex, Hermes, OpenClaw, OpenCode and other agents to a model served locally by Unsloth, fully offline. It runs an OpenAI-compatible server and never touches your agent's config files.",
readDocs: "Read the docs",
copy: "Copy",
copied: "Copied",
@ -660,7 +660,7 @@ export const en = {
launch: "Launch the agent, or just print the command and environment.",
persist: "Keep Unsloth-managed agent storage between runs.",
asSubagent:
"Keep the parent on its current model and register Unsloth as a local subagent (Claude Code, Codex, OpenCode, and Pi).",
"Keep the parent on its current model and register Unsloth as a local subagent (Claude Code, Codex, and OpenCode).",
apiKey: "Provide your Unsloth API key (or set UNSLOTH_API_KEY).",
yolo: "Skip approval prompts. Use only in trusted environments.",
},

View file

@ -664,6 +664,13 @@ html[data-chat-font] .aui-root {
in the same style pass that swaps the color tokens. Driving it from
React state paints one frame late while the whole page recalculates,
which briefly leaves the ring on the previous card. */
/* Dark mode drops the resting border. Same specificity as, and before, the
hover/selected rules below so those still paint a visible border. */
.dark .palette-card {
border-color: transparent;
}
.palette-card:hover {
border-color: var(--ring-soft);
}