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:
parent
00646632bc
commit
dfed6a6921
9 changed files with 256 additions and 285 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue