-
+
+
{command}
+
-
-
-
- {t("settings.agents.subagent.setupCommand")}
-
-
-
- {commandCopy.copied
- ? t("settings.agents.copied")
- : t("settings.agents.copy")}
-
-
-
- {command}
-
+
+
+ {t("settings.agents.subagent.setupCommand")}
+
+
-
-
-
- {t("settings.agents.subagent.usagePrompt", { agent: agent.name })}
-
-
-
- {promptCopy.copied
- ? t("settings.agents.copied")
- : t("settings.agents.copy")}
-
-
-
- {prompt}
-
+
+
+ {t("settings.agents.subagent.usagePrompt", { agent: agent.name })}
+
+
);
@@ -665,7 +670,6 @@ export function AgentsTab() {
const [modelSearch, setModelSearch] = useState("");
const [modelPickerOpen, setModelPickerOpen] = useState(false);
const [variants, setVariants] = useState
([]);
- const [defaultVariant, setDefaultVariant] = useState(null);
const [selectedVariant, setSelectedVariant] = useState(
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 (
-
+
{/* data-settings-label lets indexed settings search scroll to these. */}
-
+ {/* 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. */}
+
unsloth start
- {" "}
+ {" "}
{t("settings.agents.intro")}
-
-
- {t("settings.agents.readDocs")}
-
-
-
-
-
-
{
- agentSelectionChanged.current = true;
- setSelectedAgent(agent);
- resetCopied();
- }}
- >
-
-
-
-
- {selectedAgentDetails.name}
+ {/* Keyed to this pane, not the viewport. The dialog leaves the tab
+ about 440px at a 768px window, where three columns crush the agent
+ and model controls; 34rem is the point all three stay usable. */}
+
+
+
+ {/* Fixed height on every column header, so the padded docs link
+ here cannot push this control below the other two. */}
+
+
{
+ agentSelectionChanged.current = true;
+ setSelectedAgent(agent);
+ resetCopied();
+ }}
+ >
+
+
- {agent.name}
- {localDetection &&
- loaded &&
- detectedAgents.has(agent.id) ? (
-
- {t("settings.agents.quickstart.installed")}
-
- ) : null}
+
+ {selectedAgentDetails.name}
+
-
- );
- })}
-
-
-
-
-
-
-
- {t("settings.agents.model")}
-
-
{
- setModelPickerOpen(open);
- if (!open) {
- setModelSearch("");
- }
- }}
- >
-
-
-
- {labelFor(selectedModel)}
-
-
-
-
-
-
-
-
- {t("settings.agents.noModels")}
- {visibleModels.map((model) => (
- {
- 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"
- >
-
- {labelFor(model)}
+
+
+
+ {agents.map((agentId) => {
+ const agent = detailsFor(agentId);
+ return (
+
+
+
+ {agent.name}
+ {localDetection &&
+ loaded &&
+ detectedAgents.has(agent.id) ? (
+
+ {t("settings.agents.quickstart.installed")}
+
+ ) : null}
-
- ))}
-
- {matchingModels.length > visibleModels.length ? (
-
- {t("settings.agents.showingModels", {
- shown: visibleModels.length,
- total: matchingModels.length,
- })}
-
- ) : null}
-
-
-
-
+
+ );
+ })}
+
+
+
-
-
- {t("settings.agents.quantization")}
-
-
{
- chosenVariant.current = { model: selectedModel, variant };
- setSelectedVariant(variant);
- resetCopied();
- }}
- disabled={variantsLoading || variants.length === 0}
- >
-
-
+
+
+ {t("settings.agents.model")}
+
+
+ {
+ setModelPickerOpen(open);
+ if (!open) {
+ setModelSearch("");
}
+ }}
+ >
+
+
+
+ {labelFor(selectedModel)}
+
+
+
+
+
- {selectedVariant}
-
-
-
- {variants.map((variant) => {
- const metadata = [
- variant.quant === defaultVariant
- ? t("settings.agents.recommended")
- : null,
- variant.downloaded ? t("settings.agents.downloaded") : null,
- formatBytes(
+
+
+
+
+ {t("settings.agents.noModels")}
+
+ {visibleModels.map((model) => (
+ {
+ 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"
+ >
+
+ {labelFor(model)}
+
+
+ ))}
+
+ {matchingModels.length > visibleModels.length ? (
+
+ {t("settings.agents.showingModels", {
+ shown: visibleModels.length,
+ total: matchingModels.length,
+ })}
+
+ ) : null}
+
+
+
+
+
+
+
+
+ {t("settings.agents.quantization")}
+
+
+
{
+ chosenVariant.current = { model: selectedModel, variant };
+ setSelectedVariant(variant);
+ resetCopied();
+ }}
+ disabled={variantsLoading || variants.length === 0}
+ >
+
+
+ {selectedVariant}
+
+
+
+ {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,
- ),
- ].filter(Boolean);
- return (
-
- {variant.quant}
- {metadata.length > 0 ? (
-
- {metadata.join(" · ")}
+ );
+ return (
+
+
+ {variant.quant}
- ) : null}
-
- );
- })}
-
-
+ {size ? (
+
+ {size}
+
+ ) : null}
+
+ );
+ })}
+
+
+
@@ -1346,27 +1362,16 @@ export function AgentsTab() {
) : null}
-
-
-
- {t("settings.agents.generatedCommand")}
-
-
-
- {copied ? t("settings.agents.copied") : t("settings.agents.copy")}
-
-
-
- {command}
-
+
+
+ {t("settings.agents.generatedCommand")}
+
+
-
+
@@ -1415,7 +1420,7 @@ export function AgentsTab() {
title={t("settings.agents.passthrough.title")}
description={t("settings.agents.passthrough.description")}
>
-
+
{PASSTHROUGH_EXAMPLES.map(({ agent, flags }) => (
))}
@@ -1426,7 +1431,7 @@ export function AgentsTab() {
title={t("settings.agents.dryRun.title")}
description={t("settings.agents.dryRun.description")}
>
-
+
diff --git a/studio/frontend/src/features/settings/tabs/resources-tab.tsx b/studio/frontend/src/features/settings/tabs/resources-tab.tsx
index f45ddeba7c..1a6eb5abbb 100644
--- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx
+++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx
@@ -72,7 +72,10 @@ function formatBytes(value: number | null): string | null {
function formatGiB(value: number | null | undefined): string {
const safe = isFiniteNumber(value) ? Math.max(0, value) : 0;
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 {
@@ -116,7 +119,7 @@ function MetricTile({
const percentKnown = isFiniteNumber(percent);
const safePercent = clampPercent(percent);
return (
-
+
{label}
@@ -124,7 +127,9 @@ function MetricTile({
{percentKnown ? formatPercent(safePercent) : "--"}
@@ -141,7 +146,7 @@ function MetricTile({
@@ -327,7 +332,9 @@ export function ResourcesTab() {
const hasGpu =
(displayedGpu?.available ?? false) && metrics.devices.length > 0;
const backendLabel = (
- displayedGpu?.backend ?? systemInfo.device_backend ?? "cpu"
+ displayedGpu?.backend ??
+ systemInfo.device_backend ??
+ "cpu"
).toUpperCase();
const modelsFolderPath = hfCache
? hfCache.cacheHome
@@ -483,54 +490,66 @@ export function ResourcesTab() {
? formatPercent(safePercent)
: unknownLabel;
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.
-
-
-
- {device.name ??
- t("settings.resources.gpu.unknownDevice")}
-
-
+
+
+ {device.name ?? t("settings.resources.gpu.unknownDevice")}
+
+
+
{ordinal === undefined
? backendLabel
: `${t("settings.resources.gpu.deviceWithIndex", {
index: ordinal,
})}, ${backendLabel}`}
-
-
-
-
+
+ {/* Same accent pill as the New tags, which stays legible
+ on the light background. */}
+
{percentText}{" "}
{t("settings.resources.gpu.vramUtilization")}
-
-
- {t("settings.resources.gpu.used", {
- value: usedText,
- })}
-
-
- {t("settings.resources.gpu.free", {
- value: freeText,
- })}
-
-
- {t("settings.resources.gpu.total", {
- value: totalText,
- })}
-
+ {/* Meter under the figures, so it spans their width instead of
+ being squeezed into the gap beside them. Same 392px as the
+ Model downloads control, so both blocks start on one edge.
+ Below 992px the dialog stops filling its 960px cap and the
+ device name would truncate, so the row stacks instead. */}
+
+ {/* Ruled between the three readings: run together they are
+ easy to misread as one number. */}
+ {/* min-w-0 on each reading, or truncate cannot fire: a flex
+ item defaults to min-width:auto and the longest locales
+ would push past the block instead of ellipsizing. */}
+
+
+ {t("settings.resources.gpu.used", { value: usedText })}
+
+
+
+ {t("settings.resources.gpu.free", { value: freeText })}
+
+
+
+ {t("settings.resources.gpu.total", {
+ value: totalText,
+ })}
+
+
+
-
);
})
diff --git a/studio/frontend/src/i18n/locales/ar.ts b/studio/frontend/src/i18n/locales/ar.ts
index 47d5032fae..927470140d 100644
--- a/studio/frontend/src/i18n/locales/ar.ts
+++ b/studio/frontend/src/i18n/locales/ar.ts
@@ -451,7 +451,7 @@ export const ar = {
codingAgents: "وكلاء البرمجة",
codingAgentsHint:
"شغّل وكيل برمجة مقابل هذا الخادم. يستخدم النموذج المُحمَّل؛ الخادم المحلي يُنشئ مفتاح API تلقائيًا، والخادم البعيد يُضمّنه في الأمر.",
- codingAgentsSwap: "استبدل claude بـ codex أو openclaw أو opencode أو hermes أو pi.",
+ codingAgentsSwap: "استبدل claude بـ codex أو openclaw أو opencode أو hermes.",
codingAgentDetected: "مثبّت على هذا الجهاز",
codingAgentsDetectedHint: "تم اكتشافه على هذا الجهاز: {agents}.",
relativeNever: "أبدًا",
diff --git a/studio/frontend/src/i18n/locales/de.ts b/studio/frontend/src/i18n/locales/de.ts
index cb7d603f42..bde49dd253 100644
--- a/studio/frontend/src/i18n/locales/de.ts
+++ b/studio/frontend/src/i18n/locales/de.ts
@@ -469,7 +469,7 @@ export const de = {
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.",
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",
codingAgentsDetectedHint: "Auf diesem Gerät erkannt: {agents}.",
relativeNever: "nie",
diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts
index bdfcf38231..642b224d2a 100644
--- a/studio/frontend/src/i18n/locales/en.ts
+++ b/studio/frontend/src/i18n/locales/en.ts
@@ -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.",
},
@@ -843,8 +843,7 @@ export const en = {
codingAgents: "Coding agents",
codingAgentsHint:
"Launch a coding agent against this server. It uses the loaded model; a local server mints an API key automatically, a remote one includes it in the command.",
- codingAgentsSwap:
- "Swap claude for codex, openclaw, opencode, hermes, or pi.",
+ codingAgentsSwap: "Swap claude for codex, openclaw, opencode, or hermes.",
codingAgentDetected: "Installed on this machine",
codingAgentsDetectedHint: "Detected on this machine: {agents}.",
relativeNever: "never",
diff --git a/studio/frontend/src/i18n/locales/es.ts b/studio/frontend/src/i18n/locales/es.ts
index f7cb0e11f6..66d5281701 100644
--- a/studio/frontend/src/i18n/locales/es.ts
+++ b/studio/frontend/src/i18n/locales/es.ts
@@ -466,7 +466,7 @@ export const es = {
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.",
codingAgentsSwap:
- "Reemplaza claude por codex, openclaw, opencode, hermes o pi.",
+ "Reemplaza claude por codex, openclaw, opencode o hermes.",
codingAgentDetected: "Instalado en esta máquina",
codingAgentsDetectedHint: "Detectados en esta máquina: {agents}.",
relativeNever: "nunca",
diff --git a/studio/frontend/src/i18n/locales/fr.ts b/studio/frontend/src/i18n/locales/fr.ts
index 4f2838391f..152577174c 100644
--- a/studio/frontend/src/i18n/locales/fr.ts
+++ b/studio/frontend/src/i18n/locales/fr.ts
@@ -464,7 +464,7 @@ export const fr = {
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.",
codingAgentsSwap:
- "Remplacez claude par codex, openclaw, opencode, hermes ou pi.",
+ "Remplacez claude par codex, openclaw, opencode ou hermes.",
codingAgentDetected: "Installé sur cette machine",
codingAgentsDetectedHint: "Détecté sur cette machine : {agents}.",
relativeNever: "jamais",
diff --git a/studio/frontend/src/i18n/locales/hi.ts b/studio/frontend/src/i18n/locales/hi.ts
index 33b827f314..c04de206af 100644
--- a/studio/frontend/src/i18n/locales/hi.ts
+++ b/studio/frontend/src/i18n/locales/hi.ts
@@ -449,7 +449,7 @@ export const hi = {
codingAgents: "कोडिंग एजेंट",
codingAgentsHint:
"इस सर्वर के विरुद्ध एक कोडिंग एजेंट लॉन्च करें। यह लोड किए गए मॉडल का उपयोग करता है; एक स्थानीय सर्वर स्वचालित रूप से API key बनाता है, एक रिमोट सर्वर इसे कमांड में शामिल करता है।",
- codingAgentsSwap: "claude को codex, openclaw, opencode, hermes, या pi से बदलें।",
+ codingAgentsSwap: "claude को codex, openclaw, opencode, या hermes से बदलें।",
codingAgentDetected: "इस मशीन पर इंस्टॉल है",
codingAgentsDetectedHint: "इस मशीन पर पाया गया: {agents}।",
relativeNever: "कभी नहीं",
diff --git a/studio/frontend/src/i18n/locales/ja.ts b/studio/frontend/src/i18n/locales/ja.ts
index 978fde6281..e38dfa59e5 100644
--- a/studio/frontend/src/i18n/locales/ja.ts
+++ b/studio/frontend/src/i18n/locales/ja.ts
@@ -511,7 +511,7 @@ export const ja = {
setupDocs: "セットアップドキュメント:",
codingAgents: "コーディングエージェント",
codingAgentsHint: "このサーバーに対してコーディングエージェントを起動します。読み込み済みのモデルを使用します。ローカルサーバーでは API キーが自動的に発行され、リモートサーバーではコマンドに含まれます。",
- codingAgentsSwap: "claude を codex、openclaw、opencode、hermes、pi に置き換えられます。",
+ codingAgentsSwap: "claude を codex、openclaw、opencode、hermes に置き換えられます。",
codingAgentDetected: "このマシンにインストール済み",
codingAgentsDetectedHint: "このマシンで検出されました: {agents}。",
relativeNever: "なし",
diff --git a/studio/frontend/src/i18n/locales/ko.ts b/studio/frontend/src/i18n/locales/ko.ts
index aa8a4fd47b..c159a8befc 100644
--- a/studio/frontend/src/i18n/locales/ko.ts
+++ b/studio/frontend/src/i18n/locales/ko.ts
@@ -450,7 +450,7 @@ export const ko = {
codingAgents: "코딩 에이전트",
codingAgentsHint:
"이 서버를 대상으로 코딩 에이전트를 실행합니다. 로드된 모델을 사용하며, 로컬 서버는 API 키를 자동으로 발급하고 원격 서버는 명령에 포함합니다.",
- codingAgentsSwap: "claude를 codex, openclaw, opencode, hermes 또는 pi로 바꾸세요.",
+ codingAgentsSwap: "claude를 codex, openclaw, opencode 또는 hermes로 바꾸세요.",
codingAgentDetected: "이 컴퓨터에 설치됨",
codingAgentsDetectedHint: "이 컴퓨터에서 감지됨: {agents}.",
relativeNever: "없음",
diff --git a/studio/frontend/src/i18n/locales/pt-br.ts b/studio/frontend/src/i18n/locales/pt-br.ts
index 84cd3f945e..322f0af5e4 100644
--- a/studio/frontend/src/i18n/locales/pt-br.ts
+++ b/studio/frontend/src/i18n/locales/pt-br.ts
@@ -552,7 +552,7 @@ export const ptBR = {
codingAgents: "Agentes de código",
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.",
- codingAgentsSwap: "Troque claude por codex, openclaw, opencode, hermes ou pi.",
+ codingAgentsSwap: "Troque claude por codex, openclaw, opencode ou hermes.",
codingAgentDetected: "Instalado nesta máquina",
codingAgentsDetectedHint: "Detectado nesta máquina: {agents}.",
relativeNever: "nunca",
diff --git a/studio/frontend/src/i18n/locales/ru.ts b/studio/frontend/src/i18n/locales/ru.ts
index 7725212e3b..36f29f1d22 100644
--- a/studio/frontend/src/i18n/locales/ru.ts
+++ b/studio/frontend/src/i18n/locales/ru.ts
@@ -449,7 +449,7 @@ export const ru = {
codingAgents: "Кодинг-агенты",
codingAgentsHint:
"Запустите кодинг-агента к этому серверу. Он использует загруженную модель; локальный сервер выпускает ключ API автоматически, удалённый включает его в команду.",
- codingAgentsSwap: "Замените claude на codex, openclaw, opencode, hermes или pi.",
+ codingAgentsSwap: "Замените claude на codex, openclaw, opencode или hermes.",
codingAgentDetected: "Установлен на этой машине",
codingAgentsDetectedHint: "Обнаружены на этой машине: {agents}.",
relativeNever: "никогда",
diff --git a/studio/frontend/src/i18n/locales/zh-CN.ts b/studio/frontend/src/i18n/locales/zh-CN.ts
index 06326ed008..dda5dd481c 100644
--- a/studio/frontend/src/i18n/locales/zh-CN.ts
+++ b/studio/frontend/src/i18n/locales/zh-CN.ts
@@ -539,7 +539,7 @@ export const zhCN = {
codingAgents: "编程智能体",
codingAgentsHint:
"针对此服务器启动编程智能体。它会使用已加载的模型;本地服务器会自动生成 API 密钥,远程服务器则会将其包含在命令中。",
- codingAgentsSwap: "可将 claude 替换为 codex、openclaw、opencode、hermes 或 pi。",
+ codingAgentsSwap: "可将 claude 替换为 codex、openclaw、opencode 或 hermes。",
codingAgentDetected: "已安装在本机",
codingAgentsDetectedHint: "本机检测到:{agents}。",
relativeNever: "从未",
diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css
index 4797401ead..e298dbbe8c 100644
--- a/studio/frontend/src/index.css
+++ b/studio/frontend/src/index.css
@@ -664,6 +664,20 @@ 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;
+}
+
+/* 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 {
border-color: var(--ring-soft);
}