Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Daniel Han
d75fa36832 Studio: copy the OS-matched update command and keep checkout installs local
The web update banner copied both the macOS/Linux curl and the Windows irm
one-liner joined by a newline, so pasting into a single terminal ran the wrong
shell's command and errored. Detect the OS from navigator.userAgent and copy
only the matching command; the banner still displays both.

In the settings update panel, editable/local_repo checkouts were told to run
the remote curl/irm installer, which is the PyPI path (install.sh only installs
locally with --local) and would replace an editable checkout with a PyPI
package. Use the OS-specific local installer (./install.sh --local /
.\install.ps1 --local) as the checkout update command and drop the now
redundant duplicate block plus its unused localInstallerFallback locale key.
2026-06-14 12:26:34 +00:00
Daniel Han
84cf03c015 studio: recommend the installer one-liner instead of unsloth studio update
Update banners, the settings update panel, and the stale/MTP warnings now
point users at the install script one-liner (curl on macOS/Linux, irm on
Windows) rather than the `unsloth studio update` subcommand. Surfaces that
cannot detect the OS show both commands.

Internal callers, CI workflows, and the `unsloth studio update` subcommand
itself are unchanged.
2026-06-04 07:34:14 +00:00
11 changed files with 72 additions and 70 deletions

View file

@ -3651,8 +3651,9 @@ class LlamaCppBackend:
logger.warning(
"Requested MTP speculative decoding but "
"llama-server lacks --spec-type mtp/draft-mtp; "
"run `unsloth studio update`. Loading without "
"speculative decoding."
"update by running curl -fsSL https://unsloth.ai/install.sh | sh "
"(macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex "
"(Windows). Loading without speculative decoding."
)
return False
draft_n_max = _resolved_draft_n_max()

View file

@ -344,7 +344,9 @@ async def lifespan(app: FastAPI):
if _caps.get("found") and not _caps.get("supports_mtp"):
_msg = (
"llama.cpp prebuilt lacks MTP support "
"(--spec-type mtp/draft-mtp). Run `unsloth studio update`. "
"(--spec-type mtp/draft-mtp). Update by running "
"curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or "
"irm https://unsloth.ai/install.ps1 | iex (Windows). "
"MTP GGUFs will load without speculative decoding."
)
_log.warning(_msg)

View file

@ -317,7 +317,8 @@ def test_format_stale_warning_contains_actionable_command():
assert "b9190" in msg
assert "b9300" in msg
assert "5 days" in msg
assert "unsloth studio update" in msg
assert "install.sh" in msg
assert "install.ps1" in msg
def test_format_stale_warning_singular_day():

View file

@ -233,8 +233,9 @@ def format_stale_warning(info: dict) -> str:
age_str = f"{age} day{'s' if age != 1 else ''}" if age is not None else "some time"
return (
f"llama.cpp prebuilt is {age_str} behind: installed "
f"{installed}, latest {latest}. Run `unsloth studio update` "
f"to refresh."
f"{installed}, latest {latest}. Update to refresh by running "
f"curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or "
f"irm https://unsloth.ai/install.ps1 | iex (Windows)."
)

View file

@ -102,7 +102,7 @@ export function UpdateBanner({
: isManualLinuxPackage
? "Open the GitHub release page to install the Linux package"
: isExternalServer
? "Run `unsloth studio update` from your terminal"
? "To update, run curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex (Windows) from your terminal"
: "A new app update is available"}
</p>
</div>

View file

@ -8,7 +8,11 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { AnimatePresence, motion } from "motion/react";
import { type ReactElement, useEffect, useRef, useState } from "react";
const STUDIO_UPDATE_CMD = "unsloth studio update";
const STUDIO_UPDATE_UNIX_CMD = "curl -fsSL https://unsloth.ai/install.sh | sh";
const STUDIO_UPDATE_WINDOWS_CMD = "irm https://unsloth.ai/install.ps1 | iex";
// Match "windows" (every Windows UA carries "Windows NT") rather than "win",
// which also matches "Darwin" and would mis-detect macOS as Windows.
const WINDOWS_UA_RE = /windows/i;
const RELEASE_NOTES_URL = "https://unsloth.ai/docs/new/changelog";
const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1];
@ -36,7 +40,15 @@ export function WebUpdateBanner({
}
async function handleCopyCommand() {
if (!(await copyToClipboard(STUDIO_UPDATE_CMD))) {
// Copy only the command for the user's OS: pasting both (Unix + PowerShell)
// into a single terminal runs the wrong one and errors out.
const isWindows =
typeof navigator !== "undefined" &&
WINDOWS_UA_RE.test(navigator.userAgent || "");
const command = isWindows
? STUDIO_UPDATE_WINDOWS_CMD
: STUDIO_UPDATE_UNIX_CMD;
if (!(await copyToClipboard(command))) {
return;
}
setCopiedVersion(status?.latestVersion ?? null);
@ -90,8 +102,23 @@ export function WebUpdateBanner({
</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
Installed package: {status.currentVersion}. To update Studio,
run this in your terminal, then restart Studio.
run the command for your OS in a terminal, then restart
Studio.
</p>
<div className="mt-2 space-y-1">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
macOS / Linux
</p>
<code className="block overflow-x-auto rounded bg-muted/60 px-2 py-1 font-mono text-[11px] text-foreground">
{STUDIO_UPDATE_UNIX_CMD}
</code>
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
Windows (PowerShell)
</p>
<code className="block overflow-x-auto rounded bg-muted/60 px-2 py-1 font-mono text-[11px] text-foreground">
{STUDIO_UPDATE_WINDOWS_CMD}
</code>
</div>
</div>
</div>

View file

@ -1,8 +1,8 @@
// 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 { copyToClipboard } from "@/lib/copy-to-clipboard";
import { useT } from "@/i18n";
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { cn } from "@/lib/utils";
import { Copy01Icon, Tick02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -10,13 +10,9 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type { ReactElement } from "react";
import { useEffect, useRef, useState } from "react";
const STUDIO_UPDATE_CMD = "unsloth studio update";
const STUDIO_UPDATE_FALLBACK_UNIX_CMD =
"curl -fsSL https://unsloth.ai/install.sh | sh";
const STUDIO_UPDATE_FALLBACK_WINDOWS_CMD =
"irm https://unsloth.ai/install.ps1 | iex";
const STUDIO_UPDATE_UNIX_CMD = "curl -fsSL https://unsloth.ai/install.sh | sh";
const STUDIO_UPDATE_WINDOWS_CMD = "irm https://unsloth.ai/install.ps1 | iex";
const STUDIO_LOCAL_PULL_CMD = "git pull --ff-only";
const STUDIO_LOCAL_UPDATE_CMD = "unsloth studio update --local";
const STUDIO_LOCAL_FALLBACK_UNIX_CMD = "./install.sh --local";
const STUDIO_LOCAL_FALLBACK_WINDOWS_CMD = ".\\install.ps1 --local";
@ -145,6 +141,9 @@ export function UpdateStudioInstructions({
const shell = shellOverride ?? defaultShell;
const prefersReducedMotion = useReducedMotion();
const windows = shell === "windows";
const updateCmd = windows
? STUDIO_UPDATE_WINDOWS_CMD
: STUDIO_UPDATE_UNIX_CMD;
const localInstallSource = isLocalInstallSource(installSource);
const checkoutInstallSource =
installSource === "editable" || installSource === "local_repo";
@ -224,16 +223,9 @@ export function UpdateStudioInstructions({
command={STUDIO_LOCAL_PULL_CMD}
copyLabel={t("settings.about.update.gitPullCommand")}
/>
<CopyableCommand
command={STUDIO_LOCAL_UPDATE_CMD}
copyLabel={t("settings.about.update.localUpdateCommand")}
/>
<p className="text-xs text-muted-foreground leading-relaxed">
{t("settings.about.update.localInstallerFallback")}
</p>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={`local-fallback-${shell}`}
key={`local-update-${shell}`}
initial={fadeInitial}
animate={fadeAnimate}
exit={fadeExit}
@ -245,7 +237,7 @@ export function UpdateStudioInstructions({
? STUDIO_LOCAL_FALLBACK_WINDOWS_CMD
: STUDIO_LOCAL_FALLBACK_UNIX_CMD
}
copyLabel={t("settings.about.update.localInstallerCommand")}
copyLabel={t("settings.about.update.localUpdateCommand")}
/>
</motion.div>
</AnimatePresence>
@ -291,17 +283,20 @@ export function UpdateStudioInstructions({
<p className="text-xs text-muted-foreground leading-relaxed">
{t("settings.about.update.curlOrPypi")}
</p>
<CopyableCommand
command={STUDIO_UPDATE_CMD}
copyLabel={t("settings.about.update.updateCommand")}
/>
<p className="text-xs text-muted-foreground leading-relaxed">
{t("settings.about.update.localCheckout")}
</p>
<CopyableCommand
command={STUDIO_LOCAL_UPDATE_CMD}
copyLabel={t("settings.about.update.localUpdateCommand")}
/>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={`unknown-update-${shell}`}
initial={fadeInitial}
animate={fadeAnimate}
exit={fadeExit}
transition={fadeTransition}
>
<CopyableCommand
command={updateCmd}
copyLabel={t("settings.about.update.updateCommand")}
/>
</motion.div>
</AnimatePresence>
<p className="text-xs text-muted-foreground leading-relaxed">
{t("settings.about.update.restartAfterUpdate")}
</p>
@ -320,28 +315,17 @@ export function UpdateStudioInstructions({
{getStudioUpdateInstructionLine(shell, t)}
</motion.p>
</AnimatePresence>
<CopyableCommand
command={STUDIO_UPDATE_CMD}
copyLabel={t("settings.about.update.updateCommand")}
/>
<p className="text-xs text-muted-foreground leading-relaxed">
{t("settings.about.update.fallbackInstruction")}
</p>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={`fallback-${shell}`}
key={`update-${shell}`}
initial={fadeInitial}
animate={fadeAnimate}
exit={fadeExit}
transition={fadeTransition}
>
<CopyableCommand
command={
windows
? STUDIO_UPDATE_FALLBACK_WINDOWS_CMD
: STUDIO_UPDATE_FALLBACK_UNIX_CMD
}
copyLabel={t("settings.about.update.fallbackCommand")}
command={updateCmd}
copyLabel={t("settings.about.update.updateCommand")}
/>
</motion.div>
</AnimatePresence>

View file

@ -71,8 +71,8 @@ function externalConflictMessage(preflight: DesktopPreflightResult) {
}
return preflight.port
? `A Studio server for this install is already running from a terminal on port ${preflight.port}. Stop that server, or run \`unsloth studio update\` from that terminal before using the desktop app.`
: "A Studio server for this install is already running from a terminal. Stop that server, or run `unsloth studio update` from that terminal before using the desktop app.";
? `A Studio server for this install is already running from a terminal on port ${preflight.port}. Stop that server, or update by running curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex (Windows) from that terminal before using the desktop app.`
: "A Studio server for this install is already running from a terminal. Stop that server, or update by running curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex (Windows) from that terminal before using the desktop app.";
}
async function waitForManagedServerReady(
@ -248,8 +248,8 @@ export function useTauriBackend() {
} else {
setBackendError(
preflight.disposition === "owned_stale"
? "Desktop-owned Studio backend is too old for this desktop app. Run `unsloth studio update`, then restart Studio."
: "Managed Studio install is too old. Run `unsloth studio update`.",
? "Desktop-owned Studio backend is too old for this desktop app. Update by running curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex (Windows), then restart Studio."
: "Managed Studio install is too old. Update by running curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex (Windows).",
);
}
return;

View file

@ -307,8 +307,6 @@ export const en = {
"Pull latest changes from your Unsloth repo checkout, then update Studio locally:",
gitPullCommand: "git pull command",
localUpdateCommand: "local update command",
localInstallerFallback:
"If the Studio update command is unavailable, run the local installer from that checkout:",
localInstallerCommand: "local installer command",
sourceInstallDetected:
"This looks like a source or VCS package install. Reinstall from the original local path or Git URL you used.",
@ -320,11 +318,6 @@ export const en = {
"Studio could not detect how it was installed. Check how you installed Studio first, then choose the matching update path.",
curlOrPypi: "For curl or PyPI installs, run:",
updateCommand: "update command",
localCheckout:
"For local checkout installs, update from that checkout instead and use the local update command:",
fallbackInstruction:
"If that fails or unsloth studio update is unavailable, run:",
fallbackCommand: "fallback command",
},
},
},

View file

@ -288,8 +288,6 @@ export const zhCN = {
"从你的 Unsloth 仓库 checkout 拉取最新变更,然后本地更新 Studio",
gitPullCommand: "git pull 命令",
localUpdateCommand: "本地更新命令",
localInstallerFallback:
"如果 Studio 更新命令不可用,请从该 checkout 运行本地安装器:",
localInstallerCommand: "本地安装器命令",
sourceInstallDetected:
"这看起来是源码或 VCS 包安装。请从最初使用的本地路径或 Git URL 重新安装。",
@ -300,11 +298,6 @@ export const zhCN = {
"Studio 无法检测安装方式。请先确认你如何安装 Studio然后选择匹配的更新方式。",
curlOrPypi: "对于 curl 或 PyPI 安装,请运行:",
updateCommand: "更新命令",
localCheckout:
"对于本地 checkout 安装,请改为从该 checkout 更新并使用本地更新命令:",
fallbackInstruction:
"如果失败,或 unsloth studio update 不可用,请运行:",
fallbackCommand: "备用命令",
},
},
},

View file

@ -30,7 +30,7 @@ fn external_conflict_message(conflict: &crate::preflight::ExternalBackendConflic
);
}
format!(
"A Studio server for this install is already running from a terminal on port {}. Stop that server, or run `unsloth studio update` from that terminal before using desktop repair/update.",
"A Studio server for this install is already running from a terminal on port {}. Stop that server, or update by running curl -fsSL https://unsloth.ai/install.sh | sh (macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex (Windows) from that terminal before using desktop repair/update.",
conflict.port
)
}