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( logger.warning(
"Requested MTP speculative decoding but " "Requested MTP speculative decoding but "
"llama-server lacks --spec-type mtp/draft-mtp; " "llama-server lacks --spec-type mtp/draft-mtp; "
"run `unsloth studio update`. Loading without " "update by running curl -fsSL https://unsloth.ai/install.sh | sh "
"speculative decoding." "(macOS/Linux) or irm https://unsloth.ai/install.ps1 | iex "
"(Windows). Loading without speculative decoding."
) )
return False return False
draft_n_max = _resolved_draft_n_max() 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"): if _caps.get("found") and not _caps.get("supports_mtp"):
_msg = ( _msg = (
"llama.cpp prebuilt lacks MTP support " "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." "MTP GGUFs will load without speculative decoding."
) )
_log.warning(_msg) _log.warning(_msg)

View file

@ -317,7 +317,8 @@ def test_format_stale_warning_contains_actionable_command():
assert "b9190" in msg assert "b9190" in msg
assert "b9300" in msg assert "b9300" in msg
assert "5 days" 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(): 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" age_str = f"{age} day{'s' if age != 1 else ''}" if age is not None else "some time"
return ( return (
f"llama.cpp prebuilt is {age_str} behind: installed " f"llama.cpp prebuilt is {age_str} behind: installed "
f"{installed}, latest {latest}. Run `unsloth studio update` " f"{installed}, latest {latest}. Update to refresh by running "
f"to refresh." 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 : isManualLinuxPackage
? "Open the GitHub release page to install the Linux package" ? "Open the GitHub release page to install the Linux package"
: isExternalServer : 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"} : "A new app update is available"}
</p> </p>
</div> </div>

View file

@ -8,7 +8,11 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { type ReactElement, useEffect, useRef, useState } from "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 RELEASE_NOTES_URL = "https://unsloth.ai/docs/new/changelog";
const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; 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() { 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; return;
} }
setCopiedVersion(status?.latestVersion ?? null); setCopiedVersion(status?.latestVersion ?? null);
@ -90,8 +102,23 @@ export function WebUpdateBanner({
</p> </p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground"> <p className="mt-1 text-xs leading-relaxed text-muted-foreground">
Installed package: {status.currentVersion}. To update Studio, 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> </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>
</div> </div>

View file

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

View file

@ -71,8 +71,8 @@ function externalConflictMessage(preflight: DesktopPreflightResult) {
} }
return preflight.port 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 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 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 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( async function waitForManagedServerReady(
@ -248,8 +248,8 @@ export function useTauriBackend() {
} else { } else {
setBackendError( setBackendError(
preflight.disposition === "owned_stale" preflight.disposition === "owned_stale"
? "Desktop-owned Studio backend is too old for this desktop app. Run `unsloth studio update`, then restart Studio." ? "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. Run `unsloth studio update`.", : "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; return;

View file

@ -307,8 +307,6 @@ export const en = {
"Pull latest changes from your Unsloth repo checkout, then update Studio locally:", "Pull latest changes from your Unsloth repo checkout, then update Studio locally:",
gitPullCommand: "git pull command", gitPullCommand: "git pull command",
localUpdateCommand: "local update command", localUpdateCommand: "local update command",
localInstallerFallback:
"If the Studio update command is unavailable, run the local installer from that checkout:",
localInstallerCommand: "local installer command", localInstallerCommand: "local installer command",
sourceInstallDetected: sourceInstallDetected:
"This looks like a source or VCS package install. Reinstall from the original local path or Git URL you used.", "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.", "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:", curlOrPypi: "For curl or PyPI installs, run:",
updateCommand: "update command", 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", "从你的 Unsloth 仓库 checkout 拉取最新变更,然后本地更新 Studio",
gitPullCommand: "git pull 命令", gitPullCommand: "git pull 命令",
localUpdateCommand: "本地更新命令", localUpdateCommand: "本地更新命令",
localInstallerFallback:
"如果 Studio 更新命令不可用,请从该 checkout 运行本地安装器:",
localInstallerCommand: "本地安装器命令", localInstallerCommand: "本地安装器命令",
sourceInstallDetected: sourceInstallDetected:
"这看起来是源码或 VCS 包安装。请从最初使用的本地路径或 Git URL 重新安装。", "这看起来是源码或 VCS 包安装。请从最初使用的本地路径或 Git URL 重新安装。",
@ -300,11 +298,6 @@ export const zhCN = {
"Studio 无法检测安装方式。请先确认你如何安装 Studio然后选择匹配的更新方式。", "Studio 无法检测安装方式。请先确认你如何安装 Studio然后选择匹配的更新方式。",
curlOrPypi: "对于 curl 或 PyPI 安装,请运行:", curlOrPypi: "对于 curl 或 PyPI 安装,请运行:",
updateCommand: "更新命令", updateCommand: "更新命令",
localCheckout:
"对于本地 checkout 安装,请改为从该 checkout 更新并使用本地更新命令:",
fallbackInstruction:
"如果失败,或 unsloth studio update 不可用,请运行:",
fallbackCommand: "备用命令",
}, },
}, },
}, },

View file

@ -30,7 +30,7 @@ fn external_conflict_message(conflict: &crate::preflight::ExternalBackendConflic
); );
} }
format!( 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 conflict.port
) )
} }