Compare commits
12 commits
main
...
studio-set
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9a9a3682 | ||
|
|
8e48894fa8 | ||
|
|
3f436eb609 | ||
|
|
d3817021ff | ||
|
|
0f70c81776 | ||
|
|
8f0c0608b6 | ||
|
|
146cf98f23 | ||
|
|
29131cc85e | ||
|
|
ecb80670e5 | ||
|
|
937919b17f | ||
|
|
03082cdb74 | ||
|
|
dfed6a6921 |
19 changed files with 428 additions and 410 deletions
|
|
@ -84,9 +84,8 @@ Replace `claude` with any supported agent:
|
||||||
| Hermes Agent | `unsloth start hermes` |
|
| Hermes Agent | `unsloth start hermes` |
|
||||||
| OpenClaw | `unsloth start openclaw` |
|
| OpenClaw | `unsloth start openclaw` |
|
||||||
| OpenCode | `unsloth start opencode` |
|
| 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:
|
subagent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -204,7 +203,7 @@ Read our [guide](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide). Ad
|
||||||
## 🦥 Unsloth News
|
## 🦥 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)
|
- **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)
|
- **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)
|
- **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)
|
- **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)
|
- **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>
|
</div>
|
||||||
|
|
||||||
<section className="flex max-w-[760px] flex-col gap-2">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openAddProvider}
|
onClick={openAddProvider}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ type ApiCodingAgentsInfo = {
|
||||||
detected: string[];
|
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
|
// 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
|
// 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
|
// 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;
|
let inFlightInfo: Promise<CodingAgentsInfo> | null = null;
|
||||||
|
|
||||||
function fromApi(info: ApiCodingAgentsInfo): CodingAgentsInfo {
|
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> {
|
async function fetchCodingAgents(): Promise<CodingAgentsInfo> {
|
||||||
|
|
|
||||||
|
|
@ -106,16 +106,15 @@ const DOC_LINKS = [
|
||||||
{ label: "Hermes Agent", href: "https://unsloth.ai/docs/integrations/hermes-agent" },
|
{ label: "Hermes Agent", href: "https://unsloth.ai/docs/integrations/hermes-agent" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Falls back to this list until the backend's installed-CLI check resolves;
|
// Fallback until the backend's installed-CLI check resolves. Mirrors
|
||||||
// kept in sync with the `unsloth start <agent>` subcommands and with
|
// CODING_AGENTS in studio/backend/utils/coding_agents.py, minus HIDDEN_AGENTS
|
||||||
// CODING_AGENTS in studio/backend/utils/coding_agents.py.
|
// (see ../api/coding-agents.ts).
|
||||||
const DEFAULT_AGENTS = [
|
const DEFAULT_AGENTS = [
|
||||||
"claude",
|
"claude",
|
||||||
"codex",
|
"codex",
|
||||||
"openclaw",
|
"openclaw",
|
||||||
"opencode",
|
"opencode",
|
||||||
"hermes",
|
"hermes",
|
||||||
"pi",
|
|
||||||
];
|
];
|
||||||
// The agent selection resets to this whenever an auto-pick is no longer
|
// The agent selection resets to this whenever an auto-pick is no longer
|
||||||
// trustworthy (leaving loopback, or the only compatible detected agent
|
// trustworthy (leaving loopback, or the only compatible detected agent
|
||||||
|
|
@ -127,7 +126,6 @@ const AGENT_LABELS: Record<string, string> = {
|
||||||
openclaw: "OpenClaw",
|
openclaw: "OpenClaw",
|
||||||
opencode: "OpenCode",
|
opencode: "OpenCode",
|
||||||
hermes: "Hermes",
|
hermes: "Hermes",
|
||||||
pi: "Pi",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const j = (s: string): string => JSON.stringify(s);
|
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 { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||||
import { Tick02Icon } from "@/lib/tick-icon";
|
import { Tick02Icon } from "@/lib/tick-icon";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import { ArrowUpRight01Icon, Copy01Icon } from "@hugeicons/core-free-icons";
|
||||||
ArrowUpRight01Icon,
|
|
||||||
Book03Icon,
|
|
||||||
Copy01Icon,
|
|
||||||
} from "@hugeicons/core-free-icons";
|
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ApiProviderLogo } from "../../chat/api-provider-logo";
|
import { ApiProviderLogo } from "../../chat/api-provider-logo";
|
||||||
|
|
@ -66,7 +62,7 @@ const STATUS_POLL_MS = 5000;
|
||||||
const HUGGING_FACE_REPO_PATTERN = /^[^/\\:\s]+\/[^/\\:\s]+$/;
|
const HUGGING_FACE_REPO_PATTERN = /^[^/\\:\s]+\/[^/\\:\s]+$/;
|
||||||
const SEARCH_TOKEN_PATTERN = /\s+/;
|
const SEARCH_TOKEN_PATTERN = /\s+/;
|
||||||
const SAFE_SHELL_ARG_PATTERN = /^[A-Za-z0-9_./:@%+=,-]+$/;
|
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 {
|
function isLoopbackBase(base: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
|
@ -165,12 +161,6 @@ const SUPPORTED_AGENTS: AgentDetails[] = [
|
||||||
icon: "opencode-light.svg",
|
icon: "opencode-light.svg",
|
||||||
darkIcon: "opencode-dark.svg",
|
darkIcon: "opencode-dark.svg",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "pi",
|
|
||||||
name: "Pi Coding Agent",
|
|
||||||
docsUrl: DOCS_URL,
|
|
||||||
icon: "pi.svg",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const FALLBACK_AGENT = SUPPORTED_AGENTS[0];
|
const FALLBACK_AGENT = SUPPORTED_AGENTS[0];
|
||||||
|
|
@ -382,8 +372,8 @@ function AgentIcon({
|
||||||
}) {
|
}) {
|
||||||
if (logo) {
|
if (logo) {
|
||||||
return (
|
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">
|
||||||
<ApiProviderLogo providerType={logo} className="size-7 rounded-md" />
|
<ApiProviderLogo providerType={logo} className="size-5 rounded" />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -393,13 +383,13 @@ function AgentIcon({
|
||||||
? `${import.meta.env.BASE_URL}agent-logos/${darkIcon}`
|
? `${import.meta.env.BASE_URL}agent-logos/${darkIcon}`
|
||||||
: null;
|
: null;
|
||||||
return (
|
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
|
<img
|
||||||
src={iconSrc}
|
src={iconSrc}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-7 object-contain",
|
"size-5 object-contain",
|
||||||
darkIconSrc && "dark:hidden",
|
darkIconSrc && "dark:hidden",
|
||||||
invertIconInDark && "dark:invert",
|
invertIconInDark && "dark:invert",
|
||||||
)}
|
)}
|
||||||
|
|
@ -409,7 +399,7 @@ function AgentIcon({
|
||||||
src={darkIconSrc}
|
src={darkIconSrc}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="hidden size-7 object-contain dark:block"
|
className="hidden size-5 object-contain dark:block"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -419,7 +409,7 @@ function AgentIcon({
|
||||||
<span
|
<span
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
style={{ backgroundColor: color }}
|
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}
|
{mark}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -473,13 +463,59 @@ const PASSTHROUGH_EXAMPLES = [
|
||||||
|
|
||||||
const DRY_RUN_FLAGS = "--no-launch";
|
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 }) {
|
function CommandBlock({ command }: { command: string }) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { copied, copy } = useCopyButton(command);
|
const { copied, copy } = useCopyButton(command);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative">
|
<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 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]">
|
<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>
|
<code className="font-mono whitespace-pre">{command}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button
|
<button
|
||||||
|
|
@ -535,7 +571,7 @@ function SubagentSection({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex flex-col gap-1">
|
||||||
<span
|
<span
|
||||||
data-settings-label={t("settings.agents.subagent.title")}
|
data-settings-label={t("settings.agents.subagent.title")}
|
||||||
|
|
@ -548,60 +584,29 @@ function SubagentSection({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-col gap-1.5">
|
<div className="flex min-w-0 flex-col gap-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<span className="text-ui-11 font-medium text-foreground">
|
||||||
<span className="text-ui-11 font-medium text-foreground">
|
{t("settings.agents.subagent.setupCommand")}
|
||||||
{t("settings.agents.subagent.setupCommand")}
|
</span>
|
||||||
</span>
|
<CopyableCode
|
||||||
<button
|
value={command}
|
||||||
type="button"
|
copyLabel={t("settings.agents.subagent.copySetupCommand")}
|
||||||
onClick={commandCopy.copy}
|
copied={commandCopy.copied}
|
||||||
aria-label={t("settings.agents.subagent.copySetupCommand")}
|
onCopy={commandCopy.copy}
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-col gap-1.5">
|
<div className="flex min-w-0 flex-col gap-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<span className="text-ui-11 font-medium text-foreground">
|
||||||
<span className="text-ui-11 font-medium text-foreground">
|
{t("settings.agents.subagent.usagePrompt", { agent: agent.name })}
|
||||||
{t("settings.agents.subagent.usagePrompt", { agent: agent.name })}
|
</span>
|
||||||
</span>
|
<CopyableCode
|
||||||
<button
|
value={prompt}
|
||||||
type="button"
|
copyLabel={t("settings.agents.subagent.copyUsagePrompt")}
|
||||||
onClick={promptCopy.copy}
|
copied={promptCopy.copied}
|
||||||
aria-label={t("settings.agents.subagent.copyUsagePrompt")}
|
onCopy={promptCopy.copy}
|
||||||
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"
|
breakAll={false}
|
||||||
>
|
/>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -665,7 +670,6 @@ export function AgentsTab() {
|
||||||
const [modelSearch, setModelSearch] = useState("");
|
const [modelSearch, setModelSearch] = useState("");
|
||||||
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
||||||
const [variants, setVariants] = useState<GgufVariantDetail[]>([]);
|
const [variants, setVariants] = useState<GgufVariantDetail[]>([]);
|
||||||
const [defaultVariant, setDefaultVariant] = useState<string | null>(null);
|
|
||||||
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
||||||
EXAMPLE_MODEL_VARIANT,
|
EXAMPLE_MODEL_VARIANT,
|
||||||
);
|
);
|
||||||
|
|
@ -986,7 +990,6 @@ export function AgentsTab() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setVariants([]);
|
setVariants([]);
|
||||||
setDefaultVariant(null);
|
|
||||||
setSelectedVariant(standaloneFile ? null : preferredVariant);
|
setSelectedVariant(standaloneFile ? null : preferredVariant);
|
||||||
setVariantsFailed(false);
|
setVariantsFailed(false);
|
||||||
setVariantsLoading(false);
|
setVariantsLoading(false);
|
||||||
|
|
@ -999,7 +1002,6 @@ export function AgentsTab() {
|
||||||
// A programmatic model change reaches here too, so clear the previous model's
|
// A programmatic model change reaches here too, so clear the previous model's
|
||||||
// quants up front rather than leaving them selectable until this resolves.
|
// quants up front rather than leaving them selectable until this resolves.
|
||||||
setVariants([]);
|
setVariants([]);
|
||||||
setDefaultVariant(null);
|
|
||||||
setVariantsLoading(true);
|
setVariantsLoading(true);
|
||||||
// Offer the quants from the same place the command loads from, not remote-only ones.
|
// Offer the quants from the same place the command loads from, not remote-only ones.
|
||||||
listGgufVariants(selectedModel, hfToken || undefined, {
|
listGgufVariants(selectedModel, hfToken || undefined, {
|
||||||
|
|
@ -1023,7 +1025,6 @@ export function AgentsTab() {
|
||||||
).values(),
|
).values(),
|
||||||
);
|
);
|
||||||
setVariants(uniqueVariants);
|
setVariants(uniqueVariants);
|
||||||
setDefaultVariant(info.default_variant);
|
|
||||||
const available = new Set(
|
const available = new Set(
|
||||||
uniqueVariants.map((variant) => variant.quant),
|
uniqueVariants.map((variant) => variant.quant),
|
||||||
);
|
);
|
||||||
|
|
@ -1045,7 +1046,6 @@ export function AgentsTab() {
|
||||||
}
|
}
|
||||||
setVariantsFailed(true);
|
setVariantsFailed(true);
|
||||||
setVariants([]);
|
setVariants([]);
|
||||||
setDefaultVariant(null);
|
|
||||||
setSelectedVariant(preferredVariant);
|
setSelectedVariant(preferredVariant);
|
||||||
if (preferredVariant) {
|
if (preferredVariant) {
|
||||||
setVariants([
|
setVariants([
|
||||||
|
|
@ -1073,7 +1073,7 @@ export function AgentsTab() {
|
||||||
// picker only ever offers GGUF models.
|
// picker only ever offers GGUF models.
|
||||||
|
|
||||||
return (
|
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. */}
|
{/* data-settings-label lets indexed settings search scroll to these. */}
|
||||||
<header className="flex min-w-0 flex-col gap-1">
|
<header className="flex min-w-0 flex-col gap-1">
|
||||||
<h1
|
<h1
|
||||||
|
|
@ -1094,249 +1094,265 @@ export function AgentsTab() {
|
||||||
data-settings-label={t("settings.agents.intro")}
|
data-settings-label={t("settings.agents.intro")}
|
||||||
className="text-sm text-muted-foreground leading-relaxed"
|
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.
|
||||||
|
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"
|
||||||
|
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
|
unsloth start
|
||||||
</code>{" "}
|
</a>{" "}
|
||||||
{t("settings.agents.intro")}
|
{t("settings.agents.intro")}
|
||||||
</p>
|
</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
|
<section
|
||||||
aria-label={t("settings.agents.commandBuilder")}
|
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">
|
{/* Keyed to this pane, not the viewport. The dialog leaves the tab
|
||||||
<div className="flex items-center justify-between gap-3">
|
about 440px at a 768px window, where three columns crush the agent
|
||||||
<span
|
and model controls; 34rem is the point all three stay usable. */}
|
||||||
data-settings-label={t("settings.agents.agent")}
|
<div className="@container">
|
||||||
className="text-xs font-medium text-foreground"
|
<div className="grid grid-cols-1 items-start gap-3 @[34rem]:grid-cols-[minmax(0,0.8fr)_minmax(0,1fr)_minmax(9rem,0.5fr)]">
|
||||||
>
|
<div className="flex min-w-0 flex-col gap-1.5">
|
||||||
{t("settings.agents.agent")}
|
{/* Fixed height on every column header, so the padded docs link
|
||||||
</span>
|
here cannot push this control below the other two. */}
|
||||||
<a
|
<div className="flex h-5 items-center justify-between gap-3">
|
||||||
href={selectedAgentDetails.docsUrl}
|
<span
|
||||||
target="_blank"
|
data-settings-label={t("settings.agents.agent")}
|
||||||
rel="noreferrer"
|
className="text-xs font-medium text-foreground"
|
||||||
aria-label={t("settings.agents.agentDocs", {
|
>
|
||||||
agent: selectedAgentDetails.name,
|
{t("settings.agents.agent")}
|
||||||
})}
|
|
||||||
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>
|
</span>
|
||||||
</SelectValue>
|
<a
|
||||||
</SelectTrigger>
|
href={selectedAgentDetails.docsUrl}
|
||||||
<SelectContent align="start">
|
target="_blank"
|
||||||
{agents.map((agentId) => {
|
rel="noreferrer"
|
||||||
const agent = detailsFor(agentId);
|
aria-label={t("settings.agents.agentDocs", {
|
||||||
return (
|
agent: selectedAgentDetails.name,
|
||||||
<SelectItem key={agent.id} value={agent.id}>
|
})}
|
||||||
|
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">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<AgentIcon
|
<AgentIcon
|
||||||
logo={agent.logo}
|
logo={selectedAgentDetails.logo}
|
||||||
icon={agent.icon}
|
icon={selectedAgentDetails.icon}
|
||||||
darkIcon={agent.darkIcon}
|
darkIcon={selectedAgentDetails.darkIcon}
|
||||||
invertIconInDark={agent.invertIconInDark}
|
invertIconInDark={selectedAgentDetails.invertIconInDark}
|
||||||
color={agent.color}
|
color={selectedAgentDetails.color}
|
||||||
mark={agent.mark}
|
mark={selectedAgentDetails.mark}
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{agent.name}</span>
|
<span className="truncate">
|
||||||
{localDetection &&
|
{selectedAgentDetails.name}
|
||||||
loaded &&
|
</span>
|
||||||
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>
|
</span>
|
||||||
</SelectItem>
|
</SelectValue>
|
||||||
);
|
</SelectTrigger>
|
||||||
})}
|
<SelectContent align="start">
|
||||||
</SelectContent>
|
{agents.map((agentId) => {
|
||||||
</Select>
|
const agent = detailsFor(agentId);
|
||||||
</div>
|
return (
|
||||||
|
<SelectItem key={agent.id} value={agent.id}>
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(10rem,0.4fr)] items-start gap-3 max-md:grid-cols-1">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<div className="flex min-w-0 flex-col gap-1.5">
|
<AgentIcon
|
||||||
<span
|
logo={agent.logo}
|
||||||
data-settings-label={t("settings.agents.model")}
|
icon={agent.icon}
|
||||||
className="text-xs font-medium text-foreground"
|
darkIcon={agent.darkIcon}
|
||||||
>
|
invertIconInDark={agent.invertIconInDark}
|
||||||
{t("settings.agents.model")}
|
color={agent.color}
|
||||||
</span>
|
mark={agent.mark}
|
||||||
<Popover
|
/>
|
||||||
open={modelPickerOpen}
|
<span className="truncate">{agent.name}</span>
|
||||||
onOpenChange={(open) => {
|
{localDetection &&
|
||||||
setModelPickerOpen(open);
|
loaded &&
|
||||||
if (!open) {
|
detectedAgents.has(agent.id) ? (
|
||||||
setModelSearch("");
|
<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}
|
||||||
<PopoverTrigger asChild={true}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={t("settings.agents.model")}
|
|
||||||
aria-expanded={modelPickerOpen}
|
|
||||||
title={selectedModel}
|
|
||||||
className="flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-3 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:border-transparent dark:bg-white/[0.06] dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<span className="min-w-0 truncate font-mono text-xs">
|
|
||||||
{labelFor(selectedModel)}
|
|
||||||
</span>
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={ChevronDownStandardIcon}
|
|
||||||
strokeWidth={2}
|
|
||||||
className="size-4 shrink-0 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
align="start"
|
|
||||||
sideOffset={4}
|
|
||||||
className="w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-2rem)] gap-0 rounded-lg p-1"
|
|
||||||
>
|
|
||||||
<Command
|
|
||||||
shouldFilter={false}
|
|
||||||
className="rounded-none bg-transparent p-0"
|
|
||||||
>
|
|
||||||
<CommandInput
|
|
||||||
value={modelSearch}
|
|
||||||
onValueChange={setModelSearch}
|
|
||||||
aria-label={t("settings.agents.searchModels")}
|
|
||||||
placeholder={t("settings.agents.searchModels")}
|
|
||||||
className="font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>{t("settings.agents.noModels")}</CommandEmpty>
|
|
||||||
{visibleModels.map((model) => (
|
|
||||||
<CommandItem
|
|
||||||
key={model}
|
|
||||||
value={model}
|
|
||||||
data-checked={model === selectedModel}
|
|
||||||
onSelect={() => {
|
|
||||||
modelSelectionChanged.current = true;
|
|
||||||
setSelectedModel(model);
|
|
||||||
setSelectedVariant(knownVariants[model] ?? null);
|
|
||||||
setVariants([]);
|
|
||||||
setDefaultVariant(null);
|
|
||||||
setVariantsFailed(false);
|
|
||||||
setVariantsLoading(isHuggingFaceRepo(model));
|
|
||||||
setModelSearch("");
|
|
||||||
setModelPickerOpen(false);
|
|
||||||
resetCopied();
|
|
||||||
}}
|
|
||||||
className="cursor-pointer font-mono text-xs"
|
|
||||||
>
|
|
||||||
<span className="min-w-0 truncate" title={model}>
|
|
||||||
{labelFor(model)}
|
|
||||||
</span>
|
</span>
|
||||||
</CommandItem>
|
</SelectItem>
|
||||||
))}
|
);
|
||||||
</CommandList>
|
})}
|
||||||
{matchingModels.length > visibleModels.length ? (
|
</SelectContent>
|
||||||
<p className="border-t border-border/60 px-3 py-2 text-ui-11 text-muted-foreground">
|
</Select>
|
||||||
{t("settings.agents.showingModels", {
|
</div>
|
||||||
shown: visibleModels.length,
|
|
||||||
total: matchingModels.length,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-col gap-1.5">
|
<div className="flex min-w-0 flex-col gap-1.5">
|
||||||
<span
|
<div className="flex h-5 items-center">
|
||||||
data-settings-label={t("settings.agents.quantization")}
|
<span
|
||||||
className="text-xs font-medium text-foreground"
|
data-settings-label={t("settings.agents.model")}
|
||||||
>
|
className="text-xs font-medium text-foreground"
|
||||||
{t("settings.agents.quantization")}
|
>
|
||||||
</span>
|
{t("settings.agents.model")}
|
||||||
<Select
|
</span>
|
||||||
value={selectedVariant ?? undefined}
|
</div>
|
||||||
onValueChange={(variant) => {
|
<Popover
|
||||||
chosenVariant.current = { model: selectedModel, variant };
|
open={modelPickerOpen}
|
||||||
setSelectedVariant(variant);
|
onOpenChange={(open) => {
|
||||||
resetCopied();
|
setModelPickerOpen(open);
|
||||||
}}
|
if (!open) {
|
||||||
disabled={variantsLoading || variants.length === 0}
|
setModelSearch("");
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
aria-label={t("settings.agents.quantization")}
|
|
||||||
className="w-full rounded-lg font-mono text-xs"
|
|
||||||
>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={
|
|
||||||
variantsLoading
|
|
||||||
? t("settings.agents.loadingQuantizations")
|
|
||||||
: t("settings.agents.noQuantizations")
|
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild={true}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("settings.agents.model")}
|
||||||
|
aria-expanded={modelPickerOpen}
|
||||||
|
title={selectedModel}
|
||||||
|
className="flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-3 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:border-transparent dark:bg-white/[0.06] dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 truncate font-mono text-xs">
|
||||||
|
{labelFor(selectedModel)}
|
||||||
|
</span>
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={ChevronDownStandardIcon}
|
||||||
|
strokeWidth={2}
|
||||||
|
className="size-4 shrink-0 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
align="start"
|
||||||
|
sideOffset={4}
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-2rem)] gap-0 rounded-lg p-1"
|
||||||
>
|
>
|
||||||
{selectedVariant}
|
<Command
|
||||||
</SelectValue>
|
shouldFilter={false}
|
||||||
</SelectTrigger>
|
className="rounded-none bg-transparent p-0"
|
||||||
<SelectContent align="start">
|
>
|
||||||
{variants.map((variant) => {
|
<CommandInput
|
||||||
const metadata = [
|
value={modelSearch}
|
||||||
variant.quant === defaultVariant
|
onValueChange={setModelSearch}
|
||||||
? t("settings.agents.recommended")
|
aria-label={t("settings.agents.searchModels")}
|
||||||
: null,
|
placeholder={t("settings.agents.searchModels")}
|
||||||
variant.downloaded ? t("settings.agents.downloaded") : null,
|
className="font-mono text-xs"
|
||||||
formatBytes(
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
{t("settings.agents.noModels")}
|
||||||
|
</CommandEmpty>
|
||||||
|
{visibleModels.map((model) => (
|
||||||
|
<CommandItem
|
||||||
|
key={model}
|
||||||
|
value={model}
|
||||||
|
data-checked={model === selectedModel}
|
||||||
|
onSelect={() => {
|
||||||
|
modelSelectionChanged.current = true;
|
||||||
|
setSelectedModel(model);
|
||||||
|
setSelectedVariant(knownVariants[model] ?? null);
|
||||||
|
setVariants([]);
|
||||||
|
setVariantsFailed(false);
|
||||||
|
setVariantsLoading(isHuggingFaceRepo(model));
|
||||||
|
setModelSearch("");
|
||||||
|
setModelPickerOpen(false);
|
||||||
|
resetCopied();
|
||||||
|
}}
|
||||||
|
className="cursor-pointer font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 truncate" title={model}>
|
||||||
|
{labelFor(model)}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
{matchingModels.length > visibleModels.length ? (
|
||||||
|
<p className="border-t border-border/60 px-3 py-2 text-ui-11 text-muted-foreground">
|
||||||
|
{t("settings.agents.showingModels", {
|
||||||
|
shown: visibleModels.length,
|
||||||
|
total: matchingModels.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 flex-col gap-1.5">
|
||||||
|
<div className="flex h-5 items-center">
|
||||||
|
<span
|
||||||
|
data-settings-label={t("settings.agents.quantization")}
|
||||||
|
className="text-xs font-medium text-foreground"
|
||||||
|
>
|
||||||
|
{t("settings.agents.quantization")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={selectedVariant ?? undefined}
|
||||||
|
onValueChange={(variant) => {
|
||||||
|
chosenVariant.current = { model: selectedModel, variant };
|
||||||
|
setSelectedVariant(variant);
|
||||||
|
resetCopied();
|
||||||
|
}}
|
||||||
|
disabled={variantsLoading || variants.length === 0}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
aria-label={t("settings.agents.quantization")}
|
||||||
|
className="w-full rounded-lg font-mono text-xs"
|
||||||
|
>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
variantsLoading
|
||||||
|
? t("settings.agents.loadingQuantizations")
|
||||||
|
: t("settings.agents.noQuantizations")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedVariant}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="start" className="min-w-[16rem]">
|
||||||
|
{variants.map((variant) => {
|
||||||
|
// 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,
|
variant.download_size_bytes ?? variant.size_bytes,
|
||||||
),
|
);
|
||||||
].filter(Boolean);
|
return (
|
||||||
return (
|
<SelectItem
|
||||||
<SelectItem key={variant.quant} value={variant.quant}>
|
key={variant.quant}
|
||||||
<span className="font-mono text-xs">{variant.quant}</span>
|
value={variant.quant}
|
||||||
{metadata.length > 0 ? (
|
// Stretch the item text so the size can sit flush right,
|
||||||
<span className="text-ui-10 text-muted-foreground">
|
// giving the list a clean two-column read.
|
||||||
{metadata.join(" · ")}
|
className="[&>span:last-child]:w-full [&>span:last-child]:justify-between"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs whitespace-nowrap">
|
||||||
|
{variant.quant}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
{size ? (
|
||||||
</SelectItem>
|
<span className="text-ui-10 whitespace-nowrap text-muted-foreground">
|
||||||
);
|
{size}
|
||||||
})}
|
</span>
|
||||||
</SelectContent>
|
) : null}
|
||||||
</Select>
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1346,27 +1362,16 @@ export function AgentsTab() {
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-col gap-2 rounded-lg border border-border bg-muted/20 p-3">
|
<div className="flex min-w-0 flex-col gap-2.5">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<span className="text-xs font-medium text-foreground">
|
||||||
<span className="text-xs font-medium text-foreground">
|
{t("settings.agents.generatedCommand")}
|
||||||
{t("settings.agents.generatedCommand")}
|
</span>
|
||||||
</span>
|
<CopyableCode
|
||||||
<button
|
value={command}
|
||||||
type="button"
|
copyLabel={t("settings.agents.copyGeneratedCommand")}
|
||||||
onClick={handleCopy}
|
copied={copied}
|
||||||
aria-label={t("settings.agents.copyGeneratedCommand")}
|
onCopy={handleCopy}
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<SubagentSection
|
<SubagentSection
|
||||||
|
|
@ -1406,7 +1411,7 @@ export function AgentsTab() {
|
||||||
title={t("settings.agents.remote.title")}
|
title={t("settings.agents.remote.title")}
|
||||||
description={t("settings.agents.remote.description")}
|
description={t("settings.agents.remote.description")}
|
||||||
>
|
>
|
||||||
<div className="pt-2">
|
<div className="pt-3">
|
||||||
<CommandBlock command={remoteCommand} />
|
<CommandBlock command={remoteCommand} />
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
@ -1415,7 +1420,7 @@ export function AgentsTab() {
|
||||||
title={t("settings.agents.passthrough.title")}
|
title={t("settings.agents.passthrough.title")}
|
||||||
description={t("settings.agents.passthrough.description")}
|
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 }) => (
|
{PASSTHROUGH_EXAMPLES.map(({ agent, flags }) => (
|
||||||
<CommandBlock key={flags} command={example(agent, flags)} />
|
<CommandBlock key={flags} command={example(agent, flags)} />
|
||||||
))}
|
))}
|
||||||
|
|
@ -1426,7 +1431,7 @@ export function AgentsTab() {
|
||||||
title={t("settings.agents.dryRun.title")}
|
title={t("settings.agents.dryRun.title")}
|
||||||
description={t("settings.agents.dryRun.description")}
|
description={t("settings.agents.dryRun.description")}
|
||||||
>
|
>
|
||||||
<div className="pt-2">
|
<div className="pt-3">
|
||||||
<CommandBlock command={example("claude", DRY_RUN_FLAGS)} />
|
<CommandBlock command={example("claude", DRY_RUN_FLAGS)} />
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,10 @@ function formatBytes(value: number | null): string | null {
|
||||||
function formatGiB(value: number | null | undefined): string {
|
function formatGiB(value: number | null | undefined): string {
|
||||||
const safe = isFiniteNumber(value) ? Math.max(0, value) : 0;
|
const safe = isFiniteNumber(value) ? Math.max(0, value) : 0;
|
||||||
const digits = safe >= 10 ? 1 : 2;
|
const digits = safe >= 10 ? 1 : 2;
|
||||||
return `${safe.toFixed(digits)} GiB`;
|
// digits is never 0, so toFixed always leaves a decimal point and trimming
|
||||||
|
// trailing zeros cannot reach an integer digit. "64.0" reads as "64".
|
||||||
|
const text = safe.toFixed(digits).replace(/\.?0+$/, "");
|
||||||
|
return `${text} GiB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMb(value: number | null | undefined): string {
|
function formatMb(value: number | null | undefined): string {
|
||||||
|
|
@ -116,7 +119,7 @@ function MetricTile({
|
||||||
const percentKnown = isFiniteNumber(percent);
|
const percentKnown = isFiniteNumber(percent);
|
||||||
const safePercent = clampPercent(percent);
|
const safePercent = clampPercent(percent);
|
||||||
return (
|
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">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="truncate text-ui-11 font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
<span className="truncate text-ui-11 font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||||
{label}
|
{label}
|
||||||
|
|
@ -124,7 +127,9 @@ function MetricTile({
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 font-mono text-xs tabular-nums",
|
"shrink-0 font-mono text-xs tabular-nums",
|
||||||
percentKnown ? usageTextClass(safePercent) : "text-muted-foreground",
|
percentKnown
|
||||||
|
? usageTextClass(safePercent)
|
||||||
|
: "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{percentKnown ? formatPercent(safePercent) : "--"}
|
{percentKnown ? formatPercent(safePercent) : "--"}
|
||||||
|
|
@ -141,7 +146,7 @@ function MetricTile({
|
||||||
<Progress
|
<Progress
|
||||||
value={percentKnown ? safePercent : 0}
|
value={percentKnown ? safePercent : 0}
|
||||||
aria-label={label}
|
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)}
|
indicatorClassName={usageIndicatorClass(safePercent)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -327,7 +332,9 @@ export function ResourcesTab() {
|
||||||
const hasGpu =
|
const hasGpu =
|
||||||
(displayedGpu?.available ?? false) && metrics.devices.length > 0;
|
(displayedGpu?.available ?? false) && metrics.devices.length > 0;
|
||||||
const backendLabel = (
|
const backendLabel = (
|
||||||
displayedGpu?.backend ?? systemInfo.device_backend ?? "cpu"
|
displayedGpu?.backend ??
|
||||||
|
systemInfo.device_backend ??
|
||||||
|
"cpu"
|
||||||
).toUpperCase();
|
).toUpperCase();
|
||||||
const modelsFolderPath = hfCache
|
const modelsFolderPath = hfCache
|
||||||
? hfCache.cacheHome
|
? hfCache.cacheHome
|
||||||
|
|
@ -483,54 +490,66 @@ export function ResourcesTab() {
|
||||||
? formatPercent(safePercent)
|
? formatPercent(safePercent)
|
||||||
: unknownLabel;
|
: unknownLabel;
|
||||||
return (
|
return (
|
||||||
|
// Name over backend on the left, figures over the meter on the
|
||||||
|
// right. The meter tracks the figures' width rather than the
|
||||||
|
// pane's, so it reads as one device's usage, not a rule.
|
||||||
<div
|
<div
|
||||||
key={`${device.index ?? index}-${device.name ?? "gpu"}`}
|
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 justify-between gap-x-4 gap-y-2 py-3 max-[992px]:flex-col max-[992px]:items-stretch"
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-start justify-between gap-4">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="truncate text-sm font-medium text-foreground">
|
||||||
<div className="truncate text-sm font-medium text-foreground">
|
{device.name ?? t("settings.resources.gpu.unknownDevice")}
|
||||||
{device.name ??
|
</div>
|
||||||
t("settings.resources.gpu.unknownDevice")}
|
<div className="mt-1 flex min-w-0 items-center gap-2">
|
||||||
</div>
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
||||||
{ordinal === undefined
|
{ordinal === undefined
|
||||||
? backendLabel
|
? backendLabel
|
||||||
: `${t("settings.resources.gpu.deviceWithIndex", {
|
: `${t("settings.resources.gpu.deviceWithIndex", {
|
||||||
index: ordinal,
|
index: ordinal,
|
||||||
})}, ${backendLabel}`}
|
})}, ${backendLabel}`}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
{/* Same accent pill as the New tags, which stays legible
|
||||||
<div className="shrink-0 font-mono text-xs tabular-nums text-muted-foreground">
|
on the light background. */}
|
||||||
<span>
|
<span className="shrink-0 rounded-full bg-control-accent/10 px-2 py-1 text-ui-10 leading-none font-semibold tabular-nums text-control-accent">
|
||||||
{percentText}{" "}
|
{percentText}{" "}
|
||||||
{t("settings.resources.gpu.vramUtilization")}
|
{t("settings.resources.gpu.vramUtilization")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1 text-xs text-muted-foreground sm:grid-cols-3 sm:gap-2">
|
{/* Meter under the figures, so it spans their width instead of
|
||||||
<span className="min-w-0 truncate font-mono tabular-nums">
|
being squeezed into the gap beside them. Same 392px as the
|
||||||
{t("settings.resources.gpu.used", {
|
Model downloads control, so both blocks start on one edge.
|
||||||
value: usedText,
|
Below 992px the dialog stops filling its 960px cap and the
|
||||||
})}
|
device name would truncate, so the row stacks instead. */}
|
||||||
</span>
|
<div className="flex w-[392px] shrink-0 flex-col items-stretch gap-2.5 max-[992px]:w-full">
|
||||||
<span className="min-w-0 truncate font-mono tabular-nums sm:text-center">
|
{/* Ruled between the three readings: run together they are
|
||||||
{t("settings.resources.gpu.free", {
|
easy to misread as one number. */}
|
||||||
value: freeText,
|
{/* min-w-0 on each reading, or truncate cannot fire: a flex
|
||||||
})}
|
item defaults to min-width:auto and the longest locales
|
||||||
</span>
|
would push past the block instead of ellipsizing. */}
|
||||||
<span className="min-w-0 truncate font-mono tabular-nums sm:text-right">
|
<div className="flex items-center justify-between gap-3 font-mono text-ui-11 tabular-nums text-muted-foreground">
|
||||||
{t("settings.resources.gpu.total", {
|
<span className="min-w-0 truncate">
|
||||||
value: totalText,
|
{t("settings.resources.gpu.used", { value: usedText })}
|
||||||
})}
|
</span>
|
||||||
</span>
|
<span aria-hidden className="h-3 w-px shrink-0 bg-border" />
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
{t("settings.resources.gpu.free", { value: freeText })}
|
||||||
|
</span>
|
||||||
|
<span aria-hidden className="h-3 w-px shrink-0 bg-border" />
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
{t("settings.resources.gpu.total", {
|
||||||
|
value: totalText,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={safePercent}
|
||||||
|
aria-label={device.name ?? "GPU"}
|
||||||
|
className="h-1.5 w-full rounded-full bg-muted dark:bg-black/40"
|
||||||
|
indicatorClassName={usageIndicatorClass(safePercent)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
|
||||||
value={safePercent}
|
|
||||||
aria-label={device.name ?? "GPU"}
|
|
||||||
className="h-1.5 rounded-full bg-muted"
|
|
||||||
indicatorClassName={usageIndicatorClass(safePercent)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,7 @@ export const ar = {
|
||||||
codingAgents: "وكلاء البرمجة",
|
codingAgents: "وكلاء البرمجة",
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"شغّل وكيل برمجة مقابل هذا الخادم. يستخدم النموذج المُحمَّل؛ الخادم المحلي يُنشئ مفتاح API تلقائيًا، والخادم البعيد يُضمّنه في الأمر.",
|
"شغّل وكيل برمجة مقابل هذا الخادم. يستخدم النموذج المُحمَّل؛ الخادم المحلي يُنشئ مفتاح API تلقائيًا، والخادم البعيد يُضمّنه في الأمر.",
|
||||||
codingAgentsSwap: "استبدل claude بـ codex أو openclaw أو opencode أو hermes أو pi.",
|
codingAgentsSwap: "استبدل claude بـ codex أو openclaw أو opencode أو hermes.",
|
||||||
codingAgentDetected: "مثبّت على هذا الجهاز",
|
codingAgentDetected: "مثبّت على هذا الجهاز",
|
||||||
codingAgentsDetectedHint: "تم اكتشافه على هذا الجهاز: {agents}.",
|
codingAgentsDetectedHint: "تم اكتشافه على هذا الجهاز: {agents}.",
|
||||||
relativeNever: "أبدًا",
|
relativeNever: "أبدًا",
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,7 @@ export const de = {
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"Starten Sie einen Coding-Agent gegen diesen Server. Er verwendet das geladene Modell; ein lokaler Server erstellt automatisch einen API-Schlüssel, ein entfernter fügt ihn dem Befehl hinzu.",
|
"Starten Sie einen Coding-Agent gegen diesen Server. Er verwendet das geladene Modell; ein lokaler Server erstellt automatisch einen API-Schlüssel, ein entfernter fügt ihn dem Befehl hinzu.",
|
||||||
codingAgentsSwap:
|
codingAgentsSwap:
|
||||||
"Ersetzen Sie claude durch codex, openclaw, opencode, hermes oder pi.",
|
"Ersetzen Sie claude durch codex, openclaw, opencode oder hermes.",
|
||||||
codingAgentDetected: "Auf diesem Gerät installiert",
|
codingAgentDetected: "Auf diesem Gerät installiert",
|
||||||
codingAgentsDetectedHint: "Auf diesem Gerät erkannt: {agents}.",
|
codingAgentsDetectedHint: "Auf diesem Gerät erkannt: {agents}.",
|
||||||
relativeNever: "nie",
|
relativeNever: "nie",
|
||||||
|
|
|
||||||
|
|
@ -589,9 +589,9 @@ export const en = {
|
||||||
agents: {
|
agents: {
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
description:
|
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:
|
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",
|
readDocs: "Read the docs",
|
||||||
copy: "Copy",
|
copy: "Copy",
|
||||||
copied: "Copied",
|
copied: "Copied",
|
||||||
|
|
@ -660,7 +660,7 @@ export const en = {
|
||||||
launch: "Launch the agent, or just print the command and environment.",
|
launch: "Launch the agent, or just print the command and environment.",
|
||||||
persist: "Keep Unsloth-managed agent storage between runs.",
|
persist: "Keep Unsloth-managed agent storage between runs.",
|
||||||
asSubagent:
|
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).",
|
apiKey: "Provide your Unsloth API key (or set UNSLOTH_API_KEY).",
|
||||||
yolo: "Skip approval prompts. Use only in trusted environments.",
|
yolo: "Skip approval prompts. Use only in trusted environments.",
|
||||||
},
|
},
|
||||||
|
|
@ -843,8 +843,7 @@ export const en = {
|
||||||
codingAgents: "Coding agents",
|
codingAgents: "Coding agents",
|
||||||
codingAgentsHint:
|
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.",
|
"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:
|
codingAgentsSwap: "Swap claude for codex, openclaw, opencode, or hermes.",
|
||||||
"Swap claude for codex, openclaw, opencode, hermes, or pi.",
|
|
||||||
codingAgentDetected: "Installed on this machine",
|
codingAgentDetected: "Installed on this machine",
|
||||||
codingAgentsDetectedHint: "Detected on this machine: {agents}.",
|
codingAgentsDetectedHint: "Detected on this machine: {agents}.",
|
||||||
relativeNever: "never",
|
relativeNever: "never",
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@ export const es = {
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"Inicia un agente de programación contra este servidor. Usa el modelo cargado; un servidor local genera una clave de API automáticamente y uno remoto la incluye en el comando.",
|
"Inicia un agente de programación contra este servidor. Usa el modelo cargado; un servidor local genera una clave de API automáticamente y uno remoto la incluye en el comando.",
|
||||||
codingAgentsSwap:
|
codingAgentsSwap:
|
||||||
"Reemplaza claude por codex, openclaw, opencode, hermes o pi.",
|
"Reemplaza claude por codex, openclaw, opencode o hermes.",
|
||||||
codingAgentDetected: "Instalado en esta máquina",
|
codingAgentDetected: "Instalado en esta máquina",
|
||||||
codingAgentsDetectedHint: "Detectados en esta máquina: {agents}.",
|
codingAgentsDetectedHint: "Detectados en esta máquina: {agents}.",
|
||||||
relativeNever: "nunca",
|
relativeNever: "nunca",
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ export const fr = {
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"Lancez un agent de codage sur ce serveur. Il utilise le modèle chargé ; un serveur local génère automatiquement une clé API, un serveur distant l'inclut dans la commande.",
|
"Lancez un agent de codage sur ce serveur. Il utilise le modèle chargé ; un serveur local génère automatiquement une clé API, un serveur distant l'inclut dans la commande.",
|
||||||
codingAgentsSwap:
|
codingAgentsSwap:
|
||||||
"Remplacez claude par codex, openclaw, opencode, hermes ou pi.",
|
"Remplacez claude par codex, openclaw, opencode ou hermes.",
|
||||||
codingAgentDetected: "Installé sur cette machine",
|
codingAgentDetected: "Installé sur cette machine",
|
||||||
codingAgentsDetectedHint: "Détecté sur cette machine : {agents}.",
|
codingAgentsDetectedHint: "Détecté sur cette machine : {agents}.",
|
||||||
relativeNever: "jamais",
|
relativeNever: "jamais",
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,7 @@ export const hi = {
|
||||||
codingAgents: "कोडिंग एजेंट",
|
codingAgents: "कोडिंग एजेंट",
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"इस सर्वर के विरुद्ध एक कोडिंग एजेंट लॉन्च करें। यह लोड किए गए मॉडल का उपयोग करता है; एक स्थानीय सर्वर स्वचालित रूप से API key बनाता है, एक रिमोट सर्वर इसे कमांड में शामिल करता है।",
|
"इस सर्वर के विरुद्ध एक कोडिंग एजेंट लॉन्च करें। यह लोड किए गए मॉडल का उपयोग करता है; एक स्थानीय सर्वर स्वचालित रूप से API key बनाता है, एक रिमोट सर्वर इसे कमांड में शामिल करता है।",
|
||||||
codingAgentsSwap: "claude को codex, openclaw, opencode, hermes, या pi से बदलें।",
|
codingAgentsSwap: "claude को codex, openclaw, opencode, या hermes से बदलें।",
|
||||||
codingAgentDetected: "इस मशीन पर इंस्टॉल है",
|
codingAgentDetected: "इस मशीन पर इंस्टॉल है",
|
||||||
codingAgentsDetectedHint: "इस मशीन पर पाया गया: {agents}।",
|
codingAgentsDetectedHint: "इस मशीन पर पाया गया: {agents}।",
|
||||||
relativeNever: "कभी नहीं",
|
relativeNever: "कभी नहीं",
|
||||||
|
|
|
||||||
|
|
@ -511,7 +511,7 @@ export const ja = {
|
||||||
setupDocs: "セットアップドキュメント:",
|
setupDocs: "セットアップドキュメント:",
|
||||||
codingAgents: "コーディングエージェント",
|
codingAgents: "コーディングエージェント",
|
||||||
codingAgentsHint: "このサーバーに対してコーディングエージェントを起動します。読み込み済みのモデルを使用します。ローカルサーバーでは API キーが自動的に発行され、リモートサーバーではコマンドに含まれます。",
|
codingAgentsHint: "このサーバーに対してコーディングエージェントを起動します。読み込み済みのモデルを使用します。ローカルサーバーでは API キーが自動的に発行され、リモートサーバーではコマンドに含まれます。",
|
||||||
codingAgentsSwap: "claude を codex、openclaw、opencode、hermes、pi に置き換えられます。",
|
codingAgentsSwap: "claude を codex、openclaw、opencode、hermes に置き換えられます。",
|
||||||
codingAgentDetected: "このマシンにインストール済み",
|
codingAgentDetected: "このマシンにインストール済み",
|
||||||
codingAgentsDetectedHint: "このマシンで検出されました: {agents}。",
|
codingAgentsDetectedHint: "このマシンで検出されました: {agents}。",
|
||||||
relativeNever: "なし",
|
relativeNever: "なし",
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@ export const ko = {
|
||||||
codingAgents: "코딩 에이전트",
|
codingAgents: "코딩 에이전트",
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"이 서버를 대상으로 코딩 에이전트를 실행합니다. 로드된 모델을 사용하며, 로컬 서버는 API 키를 자동으로 발급하고 원격 서버는 명령에 포함합니다.",
|
"이 서버를 대상으로 코딩 에이전트를 실행합니다. 로드된 모델을 사용하며, 로컬 서버는 API 키를 자동으로 발급하고 원격 서버는 명령에 포함합니다.",
|
||||||
codingAgentsSwap: "claude를 codex, openclaw, opencode, hermes 또는 pi로 바꾸세요.",
|
codingAgentsSwap: "claude를 codex, openclaw, opencode 또는 hermes로 바꾸세요.",
|
||||||
codingAgentDetected: "이 컴퓨터에 설치됨",
|
codingAgentDetected: "이 컴퓨터에 설치됨",
|
||||||
codingAgentsDetectedHint: "이 컴퓨터에서 감지됨: {agents}.",
|
codingAgentsDetectedHint: "이 컴퓨터에서 감지됨: {agents}.",
|
||||||
relativeNever: "없음",
|
relativeNever: "없음",
|
||||||
|
|
|
||||||
|
|
@ -552,7 +552,7 @@ export const ptBR = {
|
||||||
codingAgents: "Agentes de código",
|
codingAgents: "Agentes de código",
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"Inicie um agente de código conectado a este servidor. Ele usa o modelo carregado; um servidor local gera uma chave de API automaticamente, um remoto a inclui no comando.",
|
"Inicie um agente de código conectado a este servidor. Ele usa o modelo carregado; um servidor local gera uma chave de API automaticamente, um remoto a inclui no comando.",
|
||||||
codingAgentsSwap: "Troque claude por codex, openclaw, opencode, hermes ou pi.",
|
codingAgentsSwap: "Troque claude por codex, openclaw, opencode ou hermes.",
|
||||||
codingAgentDetected: "Instalado nesta máquina",
|
codingAgentDetected: "Instalado nesta máquina",
|
||||||
codingAgentsDetectedHint: "Detectado nesta máquina: {agents}.",
|
codingAgentsDetectedHint: "Detectado nesta máquina: {agents}.",
|
||||||
relativeNever: "nunca",
|
relativeNever: "nunca",
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,7 @@ export const ru = {
|
||||||
codingAgents: "Кодинг-агенты",
|
codingAgents: "Кодинг-агенты",
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"Запустите кодинг-агента к этому серверу. Он использует загруженную модель; локальный сервер выпускает ключ API автоматически, удалённый включает его в команду.",
|
"Запустите кодинг-агента к этому серверу. Он использует загруженную модель; локальный сервер выпускает ключ API автоматически, удалённый включает его в команду.",
|
||||||
codingAgentsSwap: "Замените claude на codex, openclaw, opencode, hermes или pi.",
|
codingAgentsSwap: "Замените claude на codex, openclaw, opencode или hermes.",
|
||||||
codingAgentDetected: "Установлен на этой машине",
|
codingAgentDetected: "Установлен на этой машине",
|
||||||
codingAgentsDetectedHint: "Обнаружены на этой машине: {agents}.",
|
codingAgentsDetectedHint: "Обнаружены на этой машине: {agents}.",
|
||||||
relativeNever: "никогда",
|
relativeNever: "никогда",
|
||||||
|
|
|
||||||
|
|
@ -539,7 +539,7 @@ export const zhCN = {
|
||||||
codingAgents: "编程智能体",
|
codingAgents: "编程智能体",
|
||||||
codingAgentsHint:
|
codingAgentsHint:
|
||||||
"针对此服务器启动编程智能体。它会使用已加载的模型;本地服务器会自动生成 API 密钥,远程服务器则会将其包含在命令中。",
|
"针对此服务器启动编程智能体。它会使用已加载的模型;本地服务器会自动生成 API 密钥,远程服务器则会将其包含在命令中。",
|
||||||
codingAgentsSwap: "可将 claude 替换为 codex、openclaw、opencode、hermes 或 pi。",
|
codingAgentsSwap: "可将 claude 替换为 codex、openclaw、opencode 或 hermes。",
|
||||||
codingAgentDetected: "已安装在本机",
|
codingAgentDetected: "已安装在本机",
|
||||||
codingAgentsDetectedHint: "本机检测到:{agents}。",
|
codingAgentsDetectedHint: "本机检测到:{agents}。",
|
||||||
relativeNever: "从未",
|
relativeNever: "从未",
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,20 @@ html[data-chat-font] .aui-root {
|
||||||
in the same style pass that swaps the color tokens. Driving it from
|
in the same style pass that swaps the color tokens. Driving it from
|
||||||
React state paints one frame late while the whole page recalculates,
|
React state paints one frame late while the whole page recalculates,
|
||||||
which briefly leaves the ring on the previous card. */
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The card removes its outline, so the border is the only focus indicator.
|
||||||
|
This rule is unlayered like the one above, which would otherwise beat
|
||||||
|
Tailwind's layered focus-visible:border-ring and leave nothing visible. */
|
||||||
|
.dark .palette-card:focus-visible {
|
||||||
|
border-color: var(--ring);
|
||||||
|
}
|
||||||
|
|
||||||
.palette-card:hover {
|
.palette-card:hover {
|
||||||
border-color: var(--ring-soft);
|
border-color: var(--ring-soft);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue