Add Agents settings tab for unsloth start (#7303)
* Add Agents settings tab for unsloth start Adds a Settings > Agents tab documenting the `unsloth start` command: quickstart, supported agents with click-to-copy commands, model selection, common options, remote Studio setup, argument pass-through, and a dry-run preview. Agent CLIs found on PATH are badged as installed. Also removes the "New" badge from the System and Chat tabs. * Use official brand logos for agents, invert Ollama and OpenRouter in dark mode Claude Code and OpenAI Codex now use the Anthropic and OpenAI logos from the provider-logos registry; agents without an official asset keep the monogram tile. Also inverts the Ollama and OpenRouter logos in dark mode so their monochrome marks stay visible. * Title Agents tab "Agents (unsloth start)" and move it below Connections The in-tab header now reads "Agents (unsloth start)" while the sidebar label stays "Agents". Reorders the tab to sit below Connections. * Address review: guard PATH detection, fix copy timeout, OS-aware remote snippet - Only probe agent PATH in the desktop app on a loopback backend, so Installed badges are not driven by a remote server's environment. - Show the "none found" note only when detection actually ran and returned empty, not when the call failed. - Share one copy hook that resets its timeout on rapid clicks and clears it on unmount. - Render the Remote Studio snippet with PowerShell syntax on Windows. - Note that --no-launch can still load a model when --model is set. - Drop unused quickstart translation keys. * Use client OS for remote commands, fix copy a11y and model wording (#7303) - Pick the remote snippet shell from the client platform, not the server deviceType - Single-line the model examples so they paste in POSIX, PowerShell and cmd - Split the pass-through block into independent one-command copies - Derive detection visibility instead of clearing state in the effect - Announce copy success to assistive tech - Correct the quickstart/model copy: bare start uses the loaded model * Agents tab: flag the Codex row when the loaded model is not GGUF * Agents tab: target the active Studio server, wrap narrow rows, index the tab's search terms * Agents tab: build copied commands from the browser-reachable Studio and show the key placeholder * Agents tab: index the intro for agent-name searches and keep long commands inside the panel * Agents tab: omit --api-key so the CLI can replay a saved key for the base * Agents tab: label the indexed heading rows and fall back to the active desktop API base * Agents tab: name every supported agent in the indexed intro for PR #7303 * Tighten the agents tab comments for PR #7303 * Build the agents tab example commands from the active Studio base for PR #7303 * Take the agent command shell from the Studio host for PR #7303 * Pick the command shell from where the CLI runs for PR #7303 --------- Co-authored-by: Daniel Han <danielhanchen@gmail.com> Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
This commit is contained in:
parent
671d6dbf69
commit
0220104f51
6 changed files with 559 additions and 7 deletions
|
|
@ -40,10 +40,10 @@ interface ApiProviderLogoProps {
|
|||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a registry provider's logo when its asset exists under
|
||||
* `public/provider-logos/`. OpenAI's is inverted in dark mode for contrast.
|
||||
*/
|
||||
// Monochrome logos vanish on a dark background.
|
||||
const DARK_INVERT_LOGOS = new Set(["openai", "ollama", "openrouter"]);
|
||||
|
||||
/** Provider logo from `public/provider-logos/`; monochrome ones invert in dark mode. */
|
||||
export function ApiProviderLogo({ providerType, className, title }: ApiProviderLogoProps) {
|
||||
const src = apiProviderLogoSrc(providerType);
|
||||
if (!src && isCustomProviderType(providerType)) {
|
||||
|
|
@ -63,7 +63,7 @@ export function ApiProviderLogo({ providerType, className, title }: ApiProviderL
|
|||
aria-hidden
|
||||
className={cn(
|
||||
"shrink-0 object-contain",
|
||||
providerType === "openai" && "dark:invert",
|
||||
providerType && DARK_INVERT_LOGOS.has(providerType) && "dark:invert",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { type TranslationKey, useT } from "@/i18n";
|
|||
import { cn } from "@/lib/utils";
|
||||
import { MicIcon } from "@/lib/mic-icon";
|
||||
import {
|
||||
BotIcon,
|
||||
Cancel01Icon,
|
||||
CloudIcon,
|
||||
CpuIcon,
|
||||
|
|
@ -40,6 +41,7 @@ import {
|
|||
useSettingsDialogStore,
|
||||
} from "./stores/settings-dialog-store";
|
||||
import { AboutTab } from "./tabs/about-tab";
|
||||
import { AgentsTab } from "./tabs/agents-tab";
|
||||
import { ApiKeysTab } from "./tabs/api-keys-tab";
|
||||
import { AppearanceTab } from "./tabs/appearance-tab";
|
||||
import { ChatTab } from "./tabs/chat-tab";
|
||||
|
|
@ -71,13 +73,11 @@ const TABS: TabDef[] = [
|
|||
id: "resources",
|
||||
labelKey: "settings.tabs.resources",
|
||||
icon: CpuIcon,
|
||||
badgeKey: "common.new",
|
||||
},
|
||||
{
|
||||
id: "chat",
|
||||
labelKey: "settings.tabs.chat",
|
||||
icon: Message01Icon,
|
||||
badgeKey: "common.new",
|
||||
},
|
||||
{
|
||||
id: "api-keys",
|
||||
|
|
@ -89,6 +89,12 @@ const TABS: TabDef[] = [
|
|||
labelKey: "settings.tabs.connections",
|
||||
icon: CloudIcon,
|
||||
},
|
||||
{
|
||||
id: "agents",
|
||||
labelKey: "settings.tabs.agents",
|
||||
icon: BotIcon,
|
||||
badgeKey: "common.new",
|
||||
},
|
||||
{
|
||||
id: "voice",
|
||||
labelKey: "settings.tabs.voice",
|
||||
|
|
@ -124,6 +130,8 @@ function renderTab(tab: SettingsTab) {
|
|||
return <DataTab />;
|
||||
case "api-keys":
|
||||
return <ApiKeysTab />;
|
||||
case "agents":
|
||||
return <AgentsTab />;
|
||||
case "about":
|
||||
return <AboutTab />;
|
||||
}
|
||||
|
|
@ -222,6 +230,7 @@ export function SettingsDialog() {
|
|||
connections: null,
|
||||
data: null,
|
||||
"api-keys": null,
|
||||
agents: null,
|
||||
about: null,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,19 @@ export const SETTINGS_SEARCH_INDEX: Record<SettingsTab, TranslationKey[]> = {
|
|||
"settings.apiKeys.description",
|
||||
"settings.apiKeys.accessTokens",
|
||||
],
|
||||
agents: [
|
||||
// Heading and intro carry the searched terms ("unsloth start", agent names); titles do not.
|
||||
"settings.agents.title",
|
||||
"settings.agents.description",
|
||||
"settings.agents.intro",
|
||||
"settings.agents.quickstart.title",
|
||||
"settings.agents.supportedAgents.title",
|
||||
"settings.agents.models.title",
|
||||
"settings.agents.options.title",
|
||||
"settings.agents.remote.title",
|
||||
"settings.agents.passthrough.title",
|
||||
"settings.agents.dryRun.title",
|
||||
],
|
||||
connections: [],
|
||||
voice: [
|
||||
"settings.voice.dictation.sectionTitle",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type SettingsTab =
|
|||
| "connections"
|
||||
| "data"
|
||||
| "api-keys"
|
||||
| "agents"
|
||||
| "about";
|
||||
|
||||
export type SettingsScrollTarget = "about-updates";
|
||||
|
|
@ -69,6 +70,7 @@ function loadInitialTab(): SettingsTab {
|
|||
"connections",
|
||||
"data",
|
||||
"api-keys",
|
||||
"agents",
|
||||
"about",
|
||||
];
|
||||
return valid.includes(stored as SettingsTab)
|
||||
|
|
|
|||
466
studio/frontend/src/features/settings/tabs/agents-tab.tsx
Normal file
466
studio/frontend/src/features/settings/tabs/agents-tab.tsx
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { getClientPlatform } from "@/components/tauri/window-titlebar";
|
||||
import { fetchDeviceType, usePlatformStore } from "@/config/env";
|
||||
import { useT } from "@/i18n";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
import { getApiBase, isTauri } from "@/lib/api-base";
|
||||
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||
import { Tick02Icon } from "@/lib/tick-icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ArrowUpRight01Icon,
|
||||
Book03Icon,
|
||||
Copy01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useChatRuntimeStore } from "@/features/chat";
|
||||
import { ApiProviderLogo } from "../../chat/api-provider-logo";
|
||||
import { type CodingAgentsInfo, loadCodingAgents } from "../api/coding-agents";
|
||||
import {
|
||||
buildAgentCommand,
|
||||
isLoopbackHost,
|
||||
normalizeHost,
|
||||
} from "../components/agent-command";
|
||||
import { SettingsSection } from "../components/settings-section";
|
||||
|
||||
const DOCS_URL = "https://unsloth.ai/docs/integrations/unsloth-start";
|
||||
|
||||
function isLoopbackBase(base: string): boolean {
|
||||
try {
|
||||
return isLoopbackHost(normalizeHost(new URL(base).hostname));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop-only: a browser loopback URL may be an SSH/port forward to another host.
|
||||
function canUseLocalAgentDetection(base: string): boolean {
|
||||
return isTauri && isLoopbackBase(base);
|
||||
}
|
||||
|
||||
// One timeout, reset on re-click and cleared on unmount, so the tick never leaks.
|
||||
function useCopyButton(text: string) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const copy = async () => {
|
||||
if (!(await copyToClipboard(text))) return;
|
||||
setCopied(true);
|
||||
if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setCopied(false);
|
||||
timeoutRef.current = null;
|
||||
}, 1600);
|
||||
};
|
||||
|
||||
return { copied, copy };
|
||||
}
|
||||
|
||||
// Ids match the backend detection list; agents without an official `logo` asset get a monogram.
|
||||
// Names are untranslated, so `settings.agents.intro` lists them all to keep them searchable.
|
||||
const SUPPORTED_AGENTS: {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
color?: string;
|
||||
mark?: string;
|
||||
}[] = [
|
||||
{ id: "claude", name: "Claude Code", logo: "anthropic" },
|
||||
{ id: "codex", name: "OpenAI Codex", logo: "openai" },
|
||||
{ id: "hermes", name: "Hermes", color: "#8B5CF6", mark: "He" },
|
||||
{ id: "openclaw", name: "OpenClaw", color: "#F59E0B", mark: "Ol" },
|
||||
{ id: "opencode", name: "OpenCode", color: "#3B82F6", mark: "Oc" },
|
||||
{ id: "pi", name: "Pi", color: "#EC4899", mark: "Pi" },
|
||||
];
|
||||
|
||||
/** Official brand logo when available, else a brand-colored monogram tile. */
|
||||
function AgentIcon({
|
||||
logo,
|
||||
color,
|
||||
mark,
|
||||
}: {
|
||||
logo?: string;
|
||||
color?: string;
|
||||
mark?: string;
|
||||
}) {
|
||||
if (logo) {
|
||||
return (
|
||||
<span className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md">
|
||||
<ApiProviderLogo providerType={logo} className="size-7 rounded-md" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
aria-hidden={true}
|
||||
style={{ backgroundColor: color }}
|
||||
className="flex size-7 shrink-0 items-center justify-center rounded-md font-heading text-[11px] font-semibold text-white"
|
||||
>
|
||||
{mark}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineCommand({ command }: { command: string }) {
|
||||
const t = useT();
|
||||
const { copied, copy } = useCopyButton(command);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
title={copied ? t("settings.agents.copied") : t("settings.agents.copy")}
|
||||
aria-label={`${
|
||||
copied ? t("settings.agents.copied") : t("settings.agents.copy")
|
||||
}: ${command}`}
|
||||
className="inline-flex min-w-0 max-w-full items-center gap-2 rounded-md border border-border bg-muted/40 py-1.5 pl-2.5 pr-2 font-mono text-xs text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:bg-white/[0.04]"
|
||||
>
|
||||
{/* Truncate: a remote base makes the command long enough to push the icon out. */}
|
||||
<span className="truncate whitespace-nowrap">{command}</span>
|
||||
<HugeiconsIcon
|
||||
icon={copied ? Tick02Icon : Copy01Icon}
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
copied ? "text-control-accent" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<span className="sr-only" role="status" aria-live="polite">
|
||||
{copied ? t("settings.agents.copied") : ""}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Flag tokens are literal; only the descriptions are localized.
|
||||
const OPTION_ROWS: { flag: string; descKey: TranslationKey }[] = [
|
||||
{ flag: "--model, -m", descKey: "settings.agents.options.model" },
|
||||
{
|
||||
flag: "--context-length",
|
||||
descKey: "settings.agents.options.contextLength",
|
||||
},
|
||||
{ flag: "--gguf-variant", descKey: "settings.agents.options.ggufVariant" },
|
||||
{
|
||||
flag: "--load-in-4bit / --no-load-in-4bit",
|
||||
descKey: "settings.agents.options.loadIn4bit",
|
||||
},
|
||||
{
|
||||
flag: "--tensor-parallel / --no-tensor-parallel",
|
||||
descKey: "settings.agents.options.tensorParallel",
|
||||
},
|
||||
{ flag: "--serve / --no-serve", descKey: "settings.agents.options.serve" },
|
||||
{
|
||||
flag: "--launch / --no-launch",
|
||||
descKey: "settings.agents.options.launch",
|
||||
},
|
||||
{
|
||||
flag: "--persist / --no-persist",
|
||||
descKey: "settings.agents.options.persist",
|
||||
},
|
||||
{ flag: "--api-key", descKey: "settings.agents.options.apiKey" },
|
||||
{ flag: "--yolo", descKey: "settings.agents.options.yolo" },
|
||||
];
|
||||
|
||||
const QUICKSTART_AGENT = "claude";
|
||||
|
||||
// Flags only: agentCommand supplies the prefix so every example targets the Studio
|
||||
// this tab shows. Kept single line so the copy pastes as-is.
|
||||
const MODEL_SUFFIX_FLAGS =
|
||||
"--model unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL --context-length 32768";
|
||||
|
||||
const MODEL_VARIANT_FLAGS =
|
||||
"--model unsloth/gemma-4-E2B-it-GGUF --gguf-variant UD-Q4_K_XL --context-length 32768";
|
||||
|
||||
const REMOTE_CMD_UNIX = `export UNSLOTH_STUDIO_URL=https://studio.example.com
|
||||
export UNSLOTH_API_KEY=sk-unsloth-...
|
||||
unsloth start claude`;
|
||||
|
||||
// PowerShell uses $env: assignments; export is POSIX-only.
|
||||
const REMOTE_CMD_WINDOWS = `$env:UNSLOTH_STUDIO_URL = "https://studio.example.com"
|
||||
$env:UNSLOTH_API_KEY = "sk-unsloth-..."
|
||||
unsloth start claude`;
|
||||
|
||||
// Independent alternatives, each with its own copy button (not one script).
|
||||
const PASSTHROUGH_EXAMPLES = [
|
||||
{ agent: "claude", flags: "--continue" },
|
||||
{ agent: "codex", flags: "--persist resume --last" },
|
||||
];
|
||||
|
||||
const DRY_RUN_FLAGS = "--no-launch";
|
||||
|
||||
function CommandBlock({ command }: { command: string }) {
|
||||
const t = useT();
|
||||
const { copied, copy } = useCopyButton(command);
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<pre className="hover-scrollbar overflow-x-auto rounded-lg border border-border bg-muted/40 py-3 pl-3.5 pr-11 text-xs leading-relaxed text-foreground dark:bg-white/[0.04]">
|
||||
<code className="font-mono whitespace-pre">{command}</code>
|
||||
</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
aria-label={
|
||||
copied ? t("settings.agents.copied") : t("settings.agents.copy")
|
||||
}
|
||||
className="absolute top-2 right-2 flex size-7 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>
|
||||
<span className="sr-only" role="status" aria-live="polite">
|
||||
{copied ? t("settings.agents.copied") : ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentsTab() {
|
||||
const t = useT();
|
||||
const serverUrl = usePlatformStore((s) => s.serverUrl);
|
||||
const deviceType = usePlatformStore((s) => s.deviceType);
|
||||
const [info, setInfo] = useState<CodingAgentsInfo | null>(null);
|
||||
|
||||
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
||||
const localDetection = canUseLocalAgentDetection(serverUrl ?? origin);
|
||||
|
||||
// The remote snippet runs on the client, so use the client platform, not deviceType.
|
||||
// Anchor the match: a bare includes("win") would also match "darwin".
|
||||
const [isWindowsClient] = useState(() => {
|
||||
const p = getClientPlatform();
|
||||
return p.startsWith("win") || p.includes("windows");
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void fetchDeviceType({ force: true });
|
||||
}, []);
|
||||
|
||||
// A remote backend's PATH says nothing about the machine running the copied command.
|
||||
useEffect(() => {
|
||||
if (!localDetection) return;
|
||||
let cancelled = false;
|
||||
loadCodingAgents()
|
||||
.then((next) => {
|
||||
if (!cancelled) setInfo(next);
|
||||
})
|
||||
.catch(() => {
|
||||
// Best-effort; the tab still works without PATH detection.
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [localDetection]);
|
||||
|
||||
// Derive visibility from localDetection instead of clearing info in the effect.
|
||||
const visibleInfo = localDetection ? info : null;
|
||||
const detected = new Set(visibleInfo?.detected ?? []);
|
||||
const remoteCommand = isWindowsClient ? REMOTE_CMD_WINDOWS : REMOTE_CMD_UNIX;
|
||||
|
||||
// `codex` needs a GGUF model (unsloth_cli's _require_gguf_for_codex exits otherwise), so flag
|
||||
// its row instead of offering a failing command. Same three signals the API usage panel uses.
|
||||
const activeGgufVariant = useChatRuntimeStore((s) => s.activeGgufVariant);
|
||||
const activeNativePathToken = useChatRuntimeStore(
|
||||
(s) => s.activeNativePathToken,
|
||||
);
|
||||
const ggufContextLength = useChatRuntimeStore((s) => s.ggufContextLength);
|
||||
const isGguf =
|
||||
activeGgufVariant != null ||
|
||||
activeNativePathToken != null ||
|
||||
ggufContextLength != null;
|
||||
|
||||
// Build from the reachable base: a bare `unsloth start` only probes 127.0.0.1:8888, but the
|
||||
// desktop falls back across 8888-8908 and Studio may be remote. The browser must use its own
|
||||
// origin, since /api/health reports the backend's localhost (the user's, behind a tunnel);
|
||||
// the desktop has no window origin and falls back to getApiBase() while serverUrl loads.
|
||||
// No --api-key: the CLI caches an explicit key per base, so a placeholder would overwrite a
|
||||
// working saved one. Omitting it replays the saved key; the remote section covers first setup.
|
||||
const commandBase = isTauri ? (serverUrl ?? getApiBase()) : origin;
|
||||
// The command runs wherever the CLI is. For a loopback base that is this Studio's
|
||||
// own host, so use deviceType, which reports wsl where the browser would claim
|
||||
// Windows and emit $env: syntax bash rejects. A remote base is reached from the
|
||||
// viewer's machine instead, so only the client platform describes that shell.
|
||||
const commandOs =
|
||||
(isLoopbackBase(commandBase) ? deviceType === "windows" : isWindowsClient)
|
||||
? "windows"
|
||||
: "unix";
|
||||
const agentCommand = (agentId: string) =>
|
||||
buildAgentCommand(commandBase, null, commandOs, agentId);
|
||||
const example = (agentId: string, flags: string) =>
|
||||
`${agentCommand(agentId)} ${flags}`;
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 max-w-full flex-col gap-6">
|
||||
{/* data-settings-label lets indexed settings search scroll to these. */}
|
||||
<header className="flex min-w-0 flex-col gap-1">
|
||||
<h1
|
||||
data-settings-label={t("settings.agents.title")}
|
||||
className="text-xl font-semibold font-heading"
|
||||
>
|
||||
{t("settings.agents.title")}
|
||||
</h1>
|
||||
<p
|
||||
data-settings-label={t("settings.agents.description")}
|
||||
className="text-xs text-muted-foreground leading-relaxed"
|
||||
>
|
||||
{t("settings.agents.description")}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<p
|
||||
data-settings-label={t("settings.agents.intro")}
|
||||
className="text-sm text-muted-foreground leading-relaxed"
|
||||
>
|
||||
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em] text-foreground dark:bg-white/[0.08]">
|
||||
unsloth start
|
||||
</code>{" "}
|
||||
{t("settings.agents.intro")}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={Book03Icon} className="size-3.5" />
|
||||
{t("settings.agents.readDocs")}
|
||||
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
|
||||
</a>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.quickstart.title")}
|
||||
description={t("settings.agents.quickstart.description")}
|
||||
>
|
||||
<div className="pt-2">
|
||||
<CommandBlock command={agentCommand(QUICKSTART_AGENT)} />
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.supportedAgents.title")}
|
||||
description={t("settings.agents.supportedAgents.description")}
|
||||
>
|
||||
<div className="mt-1 flex flex-col divide-y divide-border/60">
|
||||
{SUPPORTED_AGENTS.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-2.5"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<AgentIcon
|
||||
logo={agent.logo}
|
||||
color={agent.color}
|
||||
mark={agent.mark}
|
||||
/>
|
||||
<span className="truncate text-sm font-medium text-foreground">
|
||||
{agent.name}
|
||||
</span>
|
||||
{detected.has(agent.id) ? (
|
||||
<span className="shrink-0 rounded-full bg-control-accent/10 px-2 py-1 text-[10px] leading-none font-semibold text-control-accent">
|
||||
{t("settings.agents.quickstart.installed")}
|
||||
</span>
|
||||
) : null}
|
||||
{agent.id === "codex" && !isGguf ? (
|
||||
<span className="shrink-0 rounded-full bg-muted px-2 py-1 text-[10px] leading-none font-semibold text-muted-foreground">
|
||||
{t("settings.agents.supportedAgents.requiresGguf")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<InlineCommand command={agentCommand(agent.id)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{visibleInfo !== null && detected.size === 0 ? (
|
||||
<p className="pt-3 text-xs text-muted-foreground">
|
||||
{t("settings.agents.quickstart.noneDetected")}
|
||||
</p>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.models.title")}
|
||||
description={t("settings.agents.models.description")}
|
||||
>
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{t("settings.agents.models.suffixLabel")}
|
||||
</span>
|
||||
<CommandBlock command={example("codex", MODEL_SUFFIX_FLAGS)} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{t("settings.agents.models.variantLabel")}
|
||||
</span>
|
||||
<CommandBlock command={example("codex", MODEL_VARIANT_FLAGS)} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.options.title")}
|
||||
description={t("settings.agents.options.description")}
|
||||
>
|
||||
<div className="mt-1 flex flex-col divide-y divide-border/60">
|
||||
{OPTION_ROWS.map((row) => (
|
||||
<div
|
||||
key={row.flag}
|
||||
className="grid grid-cols-[minmax(0,11rem)_1fr] items-start gap-x-5 gap-y-1 py-2.5 max-sm:grid-cols-1"
|
||||
>
|
||||
<code className="min-w-0 break-words font-mono text-xs font-medium text-foreground">
|
||||
{row.flag}
|
||||
</code>
|
||||
<span className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t(row.descKey)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.remote.title")}
|
||||
description={t("settings.agents.remote.description")}
|
||||
>
|
||||
<div className="pt-2">
|
||||
<CommandBlock command={remoteCommand} />
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.passthrough.title")}
|
||||
description={t("settings.agents.passthrough.description")}
|
||||
>
|
||||
<div className="flex flex-col gap-2 pt-2">
|
||||
{PASSTHROUGH_EXAMPLES.map(({ agent, flags }) => (
|
||||
<CommandBlock key={flags} command={example(agent, flags)} />
|
||||
))}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t("settings.agents.dryRun.title")}
|
||||
description={t("settings.agents.dryRun.description")}
|
||||
>
|
||||
<div className="pt-2">
|
||||
<CommandBlock command={example("claude", DRY_RUN_FLAGS)} />
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -101,6 +101,7 @@ export const en = {
|
|||
connections: "Connections",
|
||||
data: "Data",
|
||||
apiKeys: "API",
|
||||
agents: "Agents",
|
||||
about: "About",
|
||||
},
|
||||
voice: {
|
||||
|
|
@ -580,6 +581,67 @@ export const en = {
|
|||
unknown: "Unknown",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: "Agents (unsloth start)",
|
||||
description:
|
||||
"Connect coding agents like Claude Code and Codex to a model running locally in Unsloth.",
|
||||
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 a OpenAI-compatible server for the agent and never touches your agent's config files.",
|
||||
readDocs: "Read the docs",
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
quickstart: {
|
||||
title: "Quickstart",
|
||||
description:
|
||||
"Launch an agent against the model currently loaded in Studio. Load a model first, then swap claude for any supported agent below.",
|
||||
noneDetected: "No supported agent CLIs were found on your PATH.",
|
||||
installed: "Installed",
|
||||
},
|
||||
supportedAgents: {
|
||||
title: "Supported agents",
|
||||
description: "Each agent launches with its own command:",
|
||||
requiresGguf: "Needs a GGUF model",
|
||||
},
|
||||
models: {
|
||||
title: "Choosing a model",
|
||||
description:
|
||||
"Pass --model to pick a model and quantization, and --context-length to set the window. Use a quantization suffix, or an explicit --gguf-variant flag.",
|
||||
suffixLabel: "With a quantization suffix",
|
||||
variantLabel: "With an explicit variant flag",
|
||||
},
|
||||
options: {
|
||||
title: "Common options",
|
||||
description:
|
||||
"Unsloth flags are parsed first; anything it doesn't recognize is passed straight through to the agent.",
|
||||
model:
|
||||
"Select a model. Without --model, unsloth start uses the model currently loaded in Studio and errors if none is loaded.",
|
||||
contextLength:
|
||||
"Set the requested context length (alias: --max-seq-length).",
|
||||
ggufVariant: "Choose the GGUF quantization variant.",
|
||||
loadIn4bit: "Toggle 4-bit loading for Hugging Face models.",
|
||||
tensorParallel: "Toggle tensor-parallel across multiple GPUs.",
|
||||
serve: "Enable or disable the automatic local server.",
|
||||
launch: "Launch the agent, or just print the command and environment.",
|
||||
persist: "Keep Unsloth-managed agent storage between runs.",
|
||||
apiKey: "Provide your Unsloth API key (or set UNSLOTH_API_KEY).",
|
||||
yolo: "Skip approval prompts. Use only in trusted environments.",
|
||||
},
|
||||
remote: {
|
||||
title: "Connect to a remote Studio",
|
||||
description:
|
||||
"Point unsloth start at a Studio running elsewhere by setting these before launching (or pass --api-key directly):",
|
||||
},
|
||||
passthrough: {
|
||||
title: "Passing agent arguments",
|
||||
description:
|
||||
"Arguments after the Unsloth flags are forwarded to the agent itself, so native commands like resume still work:",
|
||||
},
|
||||
dryRun: {
|
||||
title: "Preview without launching",
|
||||
description:
|
||||
"Add --no-launch to print the environment and command instead of launching the agent. If --model is set, the model may still be resolved and loaded.",
|
||||
},
|
||||
},
|
||||
chat: {
|
||||
title: "Chat",
|
||||
description: "Customize how chat behaves on this device.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue