From dfed6a6921d0e2f9086984b7d5161ca575a3fd2f Mon Sep 17 00:00:00 2001 From: Unsloth Date: Tue, 28 Jul 2026 22:56:16 -0700 Subject: [PATCH 01/12] 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. --- README.md | 5 +- studio/frontend/public/agent-logos/pi.svg | 21 - .../features/chat/chat-providers-dialog.tsx | 2 +- .../features/settings/api/coding-agents.ts | 7 +- .../settings/components/usage-examples.tsx | 8 +- .../src/features/settings/tabs/agents-tab.tsx | 407 +++++++++--------- .../features/settings/tabs/resources-tab.tsx | 78 ++-- studio/frontend/src/i18n/locales/en.ts | 6 +- studio/frontend/src/index.css | 7 + 9 files changed, 256 insertions(+), 285 deletions(-) delete mode 100644 studio/frontend/public/agent-logos/pi.svg diff --git a/README.md b/README.md index e0fc8ee44c..849958bfbb 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/studio/frontend/public/agent-logos/pi.svg b/studio/frontend/public/agent-logos/pi.svg deleted file mode 100644 index 3f8a77bd1a..0000000000 --- a/studio/frontend/public/agent-logos/pi.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/studio/frontend/src/features/chat/chat-providers-dialog.tsx b/studio/frontend/src/features/chat/chat-providers-dialog.tsx index 4ab560e5ad..7e0cebea4b 100644 --- a/studio/frontend/src/features/chat/chat-providers-dialog.tsx +++ b/studio/frontend/src/features/chat/chat-providers-dialog.tsx @@ -1454,7 +1454,7 @@ export function ChatProvidersSettings({
-
+
+ + {copied ? t("settings.agents.copied") : ""} + +
+ ); +} + function CommandBlock({ command }: { command: string }) { const t = useT(); const { copied, copy } = useCopyButton(command); return ( -
-
+    
+
         {command}
       
-
- - {command} - +
+ + {t("settings.agents.subagent.setupCommand")} + +
-
-
- - {t("settings.agents.subagent.usagePrompt", { agent: agent.name })} - - -
- - {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. */} + unsloth start - {" "} + {" "} {t("settings.agents.intro")}

- - - {t("settings.agents.readDocs")} - - -
-
-
- +
+
+ + {t("settings.agents.agent")} + + + {t("settings.agents.docs")} + + +
+ { - agentSelectionChanged.current = true; - setSelectedAgent(agent); - resetCopied(); - }} - > - - - - - {selectedAgentDetails.name} - - - - - {agents.map((agentId) => { - const agent = detailsFor(agentId); - return ( - - - - {agent.name} - {localDetection && - loaded && - detectedAgents.has(agent.id) ? ( - - {t("settings.agents.quickstart.installed")} - - ) : null} + + + + + + {selectedAgentDetails.name} - - ); - })} - - -
+
+ + + + {agents.map((agentId) => { + const agent = detailsFor(agentId); + return ( + + + + {agent.name} + {localDetection && + loaded && + detectedAgents.has(agent.id) ? ( + + {t("settings.agents.quickstart.installed")} + + ) : null} + + + ); + })} + + +
-
- + {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 ( - - {variant.quant} - {metadata.length > 0 ? ( - - {metadata.join(" · ")} + + + {variant.quant} + + {size ? ( + + {size} ) : null} @@ -1346,27 +1348,16 @@ export function AgentsTab() {

) : null} -
-
- - {t("settings.agents.generatedCommand")} - - -
- - {command} - +
+ + {t("settings.agents.generatedCommand")} + +
-
+
@@ -1415,7 +1406,7 @@ export function AgentsTab() { title={t("settings.agents.passthrough.title")} description={t("settings.agents.passthrough.description")} > -
+
{PASSTHROUGH_EXAMPLES.map(({ agent, flags }) => ( ))} @@ -1426,7 +1417,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..298d159cf9 100644 --- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx @@ -116,7 +116,7 @@ function MetricTile({ const percentKnown = isFiniteNumber(percent); const safePercent = clampPercent(percent); return ( -
+
{label} @@ -141,7 +141,7 @@ function MetricTile({
@@ -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.
-
-
-
- {device.name ?? - t("settings.resources.gpu.unknownDevice")} -
-
- {ordinal === undefined - ? backendLabel - : `${t("settings.resources.gpu.deviceWithIndex", { - index: ordinal, - })}, ${backendLabel}`} -
+
+
+ {device.name ?? t("settings.resources.gpu.unknownDevice")}
-
- - {percentText}{" "} - {t("settings.resources.gpu.vramUtilization")} +
+ {ordinal === undefined + ? backendLabel + : `${t("settings.resources.gpu.deviceWithIndex", { + index: ordinal, + })}, ${backendLabel}`} +
+
+
+
+ + {t("settings.resources.gpu.used", { value: usedText })} + + + {t("settings.resources.gpu.free", { value: freeText })} + + + {t("settings.resources.gpu.total", { value: totalText })}
+ +
+ {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, - })} - -
-
); }) diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts index bdfcf38231..2b5bf7f508 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.", }, diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index 4797401ead..34897c08bc 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -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); } From 03082cdb74c1f2a75d91c984ca20b4622d3d00f2 Mon Sep 17 00:00:00 2001 From: Unsloth Date: Tue, 28 Jul 2026 23:23:03 -0700 Subject: [PATCH 02/12] Studio: keep per-GPU memory figures on narrow screens The one-row device layout hid used, free and total below 1024px, so tablets and small laptops were left with only the utilization percentage. Those widths now stack the row instead of dropping the figures, and the bar goes full width under them. --- .../features/settings/tabs/resources-tab.tsx | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/studio/frontend/src/features/settings/tabs/resources-tab.tsx b/studio/frontend/src/features/settings/tabs/resources-tab.tsx index 298d159cf9..776e643163 100644 --- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx @@ -124,7 +124,9 @@ function MetricTile({ {percentKnown ? formatPercent(safePercent) : "--"} @@ -327,7 +329,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 @@ -487,7 +491,7 @@ export function ResourcesTab() { // as a page-wide rule, so the bar is sized to the row instead.
@@ -501,8 +505,10 @@ export function ResourcesTab() { })}, ${backendLabel}`}
-
-
+ {/* Narrow screens stack instead of hiding: the used, free and + total figures stay reachable on tablets and small laptops. */} +
+
{t("settings.resources.gpu.used", { value: usedText })} @@ -513,14 +519,17 @@ export function ResourcesTab() { {t("settings.resources.gpu.total", { value: totalText })}
- -
- {percentText} {t("settings.resources.gpu.vramUtilization")} +
+ +
+ {percentText}{" "} + {t("settings.resources.gpu.vramUtilization")} +
From 937919b17f3ec304a0dba86a9a1b77606a7a65b4 Mon Sep 17 00:00:00 2001 From: Unsloth Date: Tue, 28 Jul 2026 23:47:47 -0700 Subject: [PATCH 03/12] Studio: let the GPU meter shrink beside its percentage The stacked layout gave the meter the full row width while keeping it shrink-0, so each device row ran wider than its container once the figures moved onto their own line. It now fills whatever the percentage label leaves, and keeps the fixed width at lg and above. --- studio/frontend/src/features/settings/tabs/resources-tab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/frontend/src/features/settings/tabs/resources-tab.tsx b/studio/frontend/src/features/settings/tabs/resources-tab.tsx index 776e643163..496432c7a4 100644 --- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx @@ -520,10 +520,12 @@ export function ResourcesTab() {
+ {/* Fills the space left by the percentage rather than + claiming the full row, which overflowed once stacked. */}
From ecb80670e563d0fe2a84bfadb6ead5cd774d2706 Mon Sep 17 00:00:00 2001 From: Unsloth Date: Wed, 29 Jul 2026 00:12:37 -0700 Subject: [PATCH 04/12] Studio: size the GPU row to its pane and fix the docs link name - The device row keyed its layout off the viewport, but the Settings dialog caps the tab at roughly 650px no matter how wide the window is, so on every desktop the row stayed horizontal and squeezed the device name down to a fragment. It now uses a container query against the pane and only goes to one row when there is genuinely space. - The docs link carried an aria-label that replaced its visible "unsloth start" text as the accessible name, so it could not be targeted by the label users can see. The title keeps the description. - Dropped a stale agent from the swap hint so it matches the picker. --- .../src/features/settings/tabs/agents-tab.tsx | 5 +- .../features/settings/tabs/resources-tab.tsx | 169 +++++++++--------- studio/frontend/src/i18n/locales/en.ts | 3 +- 3 files changed, 92 insertions(+), 85 deletions(-) diff --git a/studio/frontend/src/features/settings/tabs/agents-tab.tsx b/studio/frontend/src/features/settings/tabs/agents-tab.tsx index 63dc31faf8..d810048a86 100644 --- a/studio/frontend/src/features/settings/tabs/agents-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/agents-tab.tsx @@ -1094,12 +1094,13 @@ export function AgentsTab() { data-settings-label={t("settings.agents.intro")} className="text-sm text-muted-foreground leading-relaxed" > - {/* The chip is the docs entry point, so no separate link is needed. */} + {/* The chip is the docs entry point, so no separate link is needed. + No aria-label: it would replace the visible "unsloth start" as the + accessible name, leaving voice control unable to target it. */} diff --git a/studio/frontend/src/features/settings/tabs/resources-tab.tsx b/studio/frontend/src/features/settings/tabs/resources-tab.tsx index 496432c7a4..8ac51f3e38 100644 --- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx @@ -456,92 +456,99 @@ export function ResourcesTab() {
)} - {hasGpu ? ( - metrics.devices.map((device, index) => { - const ordinal = deviceOrdinal(device); - // Preserve null (unknown, e.g. Windows ROCm perf counter); coercing - // to 0 would render a fabricated 0 used / full free. - const total = device.memory_total_gb ?? null; - const used = device.vram_used_gb ?? null; - const free = - device.vram_free_gb ?? - (isFiniteNumber(total) && isFiniteNumber(used) - ? Math.max(0, total - used) - : null); - const percent = - device.vram_utilization_pct ?? - (isFiniteNumber(total) && total > 0 && isFiniteNumber(used) - ? (used / total) * 100 - : null); - const safePercent = clampPercent(percent); - const usedText = isFiniteNumber(used) - ? formatGiB(used) - : unknownLabel; - const freeText = isFiniteNumber(free) - ? formatGiB(free) - : unknownLabel; - const totalText = isFiniteNumber(total) - ? formatGiB(total) - : unknownLabel; - const percentText = isFiniteNumber(percent) - ? formatPercent(safePercent) - : unknownLabel; - return ( - // One row per device: a full-width bar under a single GPU read - // as a page-wide rule, so the bar is sized to the row instead. -
-
-
- {device.name ?? t("settings.resources.gpu.unknownDevice")} + {/* Sized against this pane, not the viewport: the Settings dialog caps + the tab at roughly 650px however wide the window is. */} +
+ {hasGpu ? ( + metrics.devices.map((device, index) => { + const ordinal = deviceOrdinal(device); + // Preserve null (unknown, e.g. Windows ROCm perf counter); coercing + // to 0 would render a fabricated 0 used / full free. + const total = device.memory_total_gb ?? null; + const used = device.vram_used_gb ?? null; + const free = + device.vram_free_gb ?? + (isFiniteNumber(total) && isFiniteNumber(used) + ? Math.max(0, total - used) + : null); + const percent = + device.vram_utilization_pct ?? + (isFiniteNumber(total) && total > 0 && isFiniteNumber(used) + ? (used / total) * 100 + : null); + const safePercent = clampPercent(percent); + const usedText = isFiniteNumber(used) + ? formatGiB(used) + : unknownLabel; + const freeText = isFiniteNumber(free) + ? formatGiB(free) + : unknownLabel; + const totalText = isFiniteNumber(total) + ? formatGiB(total) + : unknownLabel; + const percentText = isFiniteNumber(percent) + ? formatPercent(safePercent) + : unknownLabel; + return ( + // One row per device: a full-width bar under a single GPU read + // as a page-wide rule, so the bar is sized to the row instead. +
+
+
+ {device.name ?? t("settings.resources.gpu.unknownDevice")} +
+
+ {ordinal === undefined + ? backendLabel + : `${t("settings.resources.gpu.deviceWithIndex", { + index: ordinal, + })}, ${backendLabel}`} +
-
- {ordinal === undefined - ? backendLabel - : `${t("settings.resources.gpu.deviceWithIndex", { - index: ordinal, - })}, ${backendLabel}`} -
-
- {/* Narrow screens stack instead of hiding: the used, free and - total figures stay reachable on tablets and small laptops. */} -
-
- - {t("settings.resources.gpu.used", { value: usedText })} - - - {t("settings.resources.gpu.free", { value: freeText })} - - - {t("settings.resources.gpu.total", { value: totalText })} - -
-
- {/* Fills the space left by the percentage rather than + {/* Stacked until the pane is genuinely wide enough: the + figures, meter and percentage need about 50rem before the + name has room left to read. */} +
+
+ + {t("settings.resources.gpu.used", { value: usedText })} + + + {t("settings.resources.gpu.free", { value: freeText })} + + + {t("settings.resources.gpu.total", { + value: totalText, + })} + +
+
+ {/* Fills the space left by the percentage rather than claiming the full row, which overflowed once stacked. */} - -
- {percentText}{" "} - {t("settings.resources.gpu.vramUtilization")} + +
+ {percentText}{" "} + {t("settings.resources.gpu.vramUtilization")} +
-
- ); - }) - ) : ( -
- {t("settings.resources.gpu.noGpu")} -
- )} + ); + }) + ) : ( +
+ {t("settings.resources.gpu.noGpu")} +
+ )} +
diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts index 2b5bf7f508..642b224d2a 100644 --- a/studio/frontend/src/i18n/locales/en.ts +++ b/studio/frontend/src/i18n/locales/en.ts @@ -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", From 29131cc85ed89784d89d3bb3c5d5b1f14fbf6182 Mon Sep 17 00:00:00 2001 From: Unsloth Date: Wed, 29 Jul 2026 00:47:41 -0700 Subject: [PATCH 05/12] Studio: size Agents and System to their pane, restore palette focus - The GPU row's breakpoint could never fire. The settings pane is about 664px at any window size, and the name, three figures, meter and percentage need more than that, so the one-row layout was dead code. Each device is now two compact lines: name with the percentage, then the figures with a fixed-width meter. The name stays readable, the meter no longer runs the width of the pane, and there is no breakpoint that cannot be reached. - The agent, model and quantization grid switched on the viewport, so between 768px and 1024px it went to three columns inside a 440px pane and crushed every control. It now keys on the pane and stacks until the columns genuinely fit. - Dark mode dropped the palette card's resting border, but that border is the only focus indicator since the card removes its outline, and an unlayered rule beats Tailwind's layered focus-visible utility. Keyboard focus paints the ring colour again. --- .../src/features/settings/tabs/agents-tab.tsx | 445 +++++++++--------- .../features/settings/tabs/resources-tab.tsx | 66 ++- studio/frontend/src/index.css | 7 + 3 files changed, 265 insertions(+), 253 deletions(-) diff --git a/studio/frontend/src/features/settings/tabs/agents-tab.tsx b/studio/frontend/src/features/settings/tabs/agents-tab.tsx index d810048a86..c4c464d62a 100644 --- a/studio/frontend/src/features/settings/tabs/agents-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/agents-tab.tsx @@ -1113,233 +1113,240 @@ export function AgentsTab() { aria-label={t("settings.agents.commandBuilder")} className="flex w-full flex-col gap-6" > -
-
-
+ {/* 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. */} +
+
+
+
+ + {t("settings.agents.agent")} + + + {t("settings.agents.docs")} + + +
+ +
+ +
- { + 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, + ); + return ( + + + {variant.quant} + + {size ? ( + + {size} ) : null} - - - ); - })} - - -
- -
- - {t("settings.agents.model")} - - { - setModelPickerOpen(open); - if (!open) { - setModelSearch(""); - } - }} - > - - - - - - - - {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")} - - + + ); + })} + + +
diff --git a/studio/frontend/src/features/settings/tabs/resources-tab.tsx b/studio/frontend/src/features/settings/tabs/resources-tab.tsx index 8ac51f3e38..7725aaa0ad 100644 --- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx @@ -456,9 +456,7 @@ export function ResourcesTab() {
)} - {/* Sized against this pane, not the viewport: the Settings dialog caps - the tab at roughly 650px however wide the window is. */} -
+
{hasGpu ? ( metrics.devices.map((device, index) => { const ordinal = deviceOrdinal(device); @@ -490,29 +488,37 @@ 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. + // Two compact lines per device. The settings pane is about + // 664px at any window size, which cannot hold the name, three + // figures, meter and percentage on one row, so no breakpoint + // would ever fire. Splitting them keeps the name readable and + // the meter narrow instead of running the width of the pane.
-
-
- {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}`} +
-
- {ordinal === undefined - ? backendLabel - : `${t("settings.resources.gpu.deviceWithIndex", { - index: ordinal, - })}, ${backendLabel}`} +
+ {percentText}{" "} + {t("settings.resources.gpu.vramUtilization")}
- {/* Stacked until the pane is genuinely wide enough: the - figures, meter and percentage need about 50rem before the - name has room left to read. */} -
-
+ {/* Wraps rather than overflows once the pane gets narrow. */} +
+
{t("settings.resources.gpu.used", { value: usedText })} @@ -525,20 +531,12 @@ export function ResourcesTab() { })}
-
- {/* Fills the space left by the percentage rather than - claiming the full row, which overflowed once stacked. */} - -
- {percentText}{" "} - {t("settings.resources.gpu.vramUtilization")} -
-
+
); diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index 34897c08bc..e298dbbe8c 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -671,6 +671,13 @@ html[data-chat-font] .aui-root { 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); } From 146cf98f237f249cf61c4c3bfb5f237b04ae7abf Mon Sep 17 00:00:00 2001 From: Unsloth Date: Wed, 29 Jul 2026 01:07:45 -0700 Subject: [PATCH 06/12] Studio: align the localized agent swap hints with the picker The English hint was updated to match the agents the picker offers, but the ten translated copies were not, so every non-English user was told to pick an option the UI does not show. --- studio/frontend/src/i18n/locales/ar.ts | 2 +- studio/frontend/src/i18n/locales/de.ts | 2 +- studio/frontend/src/i18n/locales/es.ts | 2 +- studio/frontend/src/i18n/locales/fr.ts | 2 +- studio/frontend/src/i18n/locales/hi.ts | 2 +- studio/frontend/src/i18n/locales/ja.ts | 2 +- studio/frontend/src/i18n/locales/ko.ts | 2 +- studio/frontend/src/i18n/locales/pt-br.ts | 2 +- studio/frontend/src/i18n/locales/ru.ts | 2 +- studio/frontend/src/i18n/locales/zh-CN.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) 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/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: "从未", From 8f0c0608b663c5aba07ffd109c32d4ebfaf1babb Mon Sep 17 00:00:00 2001 From: Unsloth Date: Wed, 29 Jul 2026 01:22:12 -0700 Subject: [PATCH 07/12] Studio: rework the GPU device row Restores the earlier shape, name over backend on the left, but lifts the memory figures and the meter up beside them instead of stacking them underneath. The meter is short rather than full width, which was the original complaint: at full width it read as a rule across the pane rather than a reading for one device. VRAM utilization becomes a rounded pill next to the backend label. The row wraps rather than squeezing the device name when the pane is narrow. --- .../features/settings/tabs/resources-tab.tsx | 168 +++++++++--------- 1 file changed, 81 insertions(+), 87 deletions(-) diff --git a/studio/frontend/src/features/settings/tabs/resources-tab.tsx b/studio/frontend/src/features/settings/tabs/resources-tab.tsx index 7725aaa0ad..6348d2066d 100644 --- a/studio/frontend/src/features/settings/tabs/resources-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/resources-tab.tsx @@ -456,97 +456,91 @@ export function ResourcesTab() {
)} -
- {hasGpu ? ( - metrics.devices.map((device, index) => { - const ordinal = deviceOrdinal(device); - // Preserve null (unknown, e.g. Windows ROCm perf counter); coercing - // to 0 would render a fabricated 0 used / full free. - const total = device.memory_total_gb ?? null; - const used = device.vram_used_gb ?? null; - const free = - device.vram_free_gb ?? - (isFiniteNumber(total) && isFiniteNumber(used) - ? Math.max(0, total - used) - : null); - const percent = - device.vram_utilization_pct ?? - (isFiniteNumber(total) && total > 0 && isFiniteNumber(used) - ? (used / total) * 100 - : null); - const safePercent = clampPercent(percent); - const usedText = isFiniteNumber(used) - ? formatGiB(used) - : unknownLabel; - const freeText = isFiniteNumber(free) - ? formatGiB(free) - : unknownLabel; - const totalText = isFiniteNumber(total) - ? formatGiB(total) - : unknownLabel; - const percentText = isFiniteNumber(percent) - ? formatPercent(safePercent) - : unknownLabel; - return ( - // Two compact lines per device. The settings pane is about - // 664px at any window size, which cannot hold the name, three - // figures, meter and percentage on one row, so no breakpoint - // would ever fire. Splitting them keeps the name readable and - // the meter narrow instead of running the width of the pane. -
-
-
-
- {device.name ?? - t("settings.resources.gpu.unknownDevice")} -
-
- {ordinal === undefined - ? backendLabel - : `${t("settings.resources.gpu.deviceWithIndex", { - index: ordinal, - })}, ${backendLabel}`} -
-
-
+ {hasGpu ? ( + metrics.devices.map((device, index) => { + const ordinal = deviceOrdinal(device); + // Preserve null (unknown, e.g. Windows ROCm perf counter); coercing + // to 0 would render a fabricated 0 used / full free. + const total = device.memory_total_gb ?? null; + const used = device.vram_used_gb ?? null; + const free = + device.vram_free_gb ?? + (isFiniteNumber(total) && isFiniteNumber(used) + ? Math.max(0, total - used) + : null); + const percent = + device.vram_utilization_pct ?? + (isFiniteNumber(total) && total > 0 && isFiniteNumber(used) + ? (used / total) * 100 + : null); + const safePercent = clampPercent(percent); + const usedText = isFiniteNumber(used) + ? formatGiB(used) + : unknownLabel; + const freeText = isFiniteNumber(free) + ? formatGiB(free) + : unknownLabel; + const totalText = isFiniteNumber(total) + ? formatGiB(total) + : unknownLabel; + const percentText = isFiniteNumber(percent) + ? formatPercent(safePercent) + : unknownLabel; + return ( + // Name over backend on the left, figures and meter beside them. + // The meter is deliberately short: at full width it read as a + // rule across the pane rather than a reading for one device. +
+
+
+ {device.name ?? t("settings.resources.gpu.unknownDevice")} +
+
+ + {ordinal === undefined + ? backendLabel + : `${t("settings.resources.gpu.deviceWithIndex", { + index: ordinal, + })}, ${backendLabel}`} + + {percentText}{" "} {t("settings.resources.gpu.vramUtilization")} -
-
- {/* Wraps rather than overflows once the pane gets narrow. */} -
-
- - {t("settings.resources.gpu.used", { value: usedText })} - - - {t("settings.resources.gpu.free", { value: freeText })} - - - {t("settings.resources.gpu.total", { - value: totalText, - })} - -
- +
- ); - }) - ) : ( -
- {t("settings.resources.gpu.noGpu")} -
- )} -
+
+
+ + {t("settings.resources.gpu.used", { value: usedText })} + + + {t("settings.resources.gpu.free", { value: freeText })} + + + {t("settings.resources.gpu.total", { + value: totalText, + })} + +
+ +
+
+ ); + }) + ) : ( +
+ {t("settings.resources.gpu.noGpu")} +
+ )} From 0f70c8177618d1ff2c311efd127f9899e1f65829 Mon Sep 17 00:00:00 2001 From: Unsloth Date: Wed, 29 Jul 2026 01:33:56 -0700 Subject: [PATCH 08/12] Studio: align the three Agents column headers The Coding agent header carries a padded docs link, so it measured taller than the plain labels beside it and pushed its select down. Pin every column header to the same height. --- .../src/features/settings/tabs/agents-tab.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/studio/frontend/src/features/settings/tabs/agents-tab.tsx b/studio/frontend/src/features/settings/tabs/agents-tab.tsx index c4c464d62a..a8ef07234b 100644 --- a/studio/frontend/src/features/settings/tabs/agents-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/agents-tab.tsx @@ -1119,7 +1119,9 @@ export function AgentsTab() {
-
+ {/* Fixed height on every column header, so the padded docs link + here cannot push this control below the other two. */} +
- - {t("settings.agents.model")} - +
+ + {t("settings.agents.model")} + +
{ @@ -1289,12 +1293,14 @@ export function AgentsTab() {
- - {t("settings.agents.quantization")} - +
+ + {t("settings.agents.quantization")} + +