unsloth/studio/backend/utils/studio_version.py
Michael Han b2b1dcd6ad
Studio: llama.cpp update banner redesign, About tab license info, UI polish (#6196)
* Studio: llama.cpp update banner redesign, About tab license info, inline system prompt editing, naming cleanup

- Redesign the llama.cpp update banner to match the chat composer surface
  (borderless rounded card, composer shadow, Hellix Medium title), rename
  actions to Update and add a 15 minute Remind me later snooze
- Keep the banner up until the user explicitly acts on it; drop the
  outside click dismissal
- Add a Settings > General > Notifications toggle to disable the banner
  for training-only setups (on by default)
- Rename the Help settings tab to About and add a License section
  (Unsloth Studio AGPL-3.0, Unsloth Core Apache-2.0) linking to the
  license files in this repo
- Make the run settings system prompt box an inline editable textarea;
  the popup editor opens when the prompt overflows the box
- Pointer cursor on the preset dropdown chevron
- Dark mode toasts use the chat composer surface color
- Replace standalone Studio with Unsloth in user facing strings; keep
  Unsloth Studio, LM Studio, Fine-tuning Studio, Recipe Studio and CLI
  commands unchanged

* Studio: open the system prompt popup on box click, balance banner padding

- The system prompt box opens the Edit System Prompt dialog on click,
  matching the pencil action
- Slightly more bottom padding on the llama.cpp update banner so the
  spacing reads even next to the action pills

* Studio: replace unsloth studio update with the installer commands in update guidance

- The unsloth studio update command no longer works, so the About tab
  update section now shows the one-line installer (curl or irm) for
  PyPI and unknown installs, and git pull plus the local installer for
  checkouts
- Add a short note that unsloth studio update is no longer supported
- Link the Installation, Updating and Windows install docs pages
- The package update banner now copies the platform installer command
  instead of unsloth studio update

* Studio: rounder account menu, inline system prompt box with popup from the label

- Account menu corners go from 14px to 18px via a specific override,
  since list menus pin border-radius globally
- llama.cpp banner bottom padding 22px
- System prompt is an inline editable textarea again; clicking the
  System Prompt label opens the popup editor, and an overflowing
  prompt opens it on box click

* Studio: show the standard install commands in the About update section

- Both one-line install commands (MacOS/Linux/WSL and Windows
  PowerShell) are always shown, labeled like the docs, since running
  them again updates an existing install
- Drop the unsloth studio update deprecation note
- Add the Mac install guide to the docs links

* Studio: clearer platform toggle and layout in the About update section

- Section heading is Update
- Platform picker is a pair of pill buttons, MacOS / Linux and Windows,
  and only the selected platform's install command is shown
- Intro reads: To install or update Unsloth
- Local update heading separates checkout guidance from the standard
  install command

* Studio: report GitHub branch instead of dev for source checkouts

A source checkout not on an exact release tag now shows
GitHub <branch> (e.g. GitHub main) as the Studio version in About.
Detached or unusual HEADs still fall back to dev.

* Studio: tighten the About update section copy and toggle styling

- Platform toggle buttons are borderless pills
- Shorter local update wording and restart note
- Docs links read Mac and Windows

* Studio: tighten line spacing in the sidebar account button

* Studio: fix vanishing compact MCP icon on hover, single line pill tooltips

- Compact caret pills (MCP, RAG) keep their icon on hover for inactive
  pills too; the off switch hover rules hid the icon while compact mode
  hid the X, leaving an empty slot
- Compact icon tooltips and single line compact tooltips render as full
  pills; wrapped tooltips keep the 9px corners. TooltipContent measures
  line count in a ref callback since Radix mounts portal content
  without re-rendering the wrapper
- 1px gap between the name and Unsloth lines in the sidebar account
  button

* Studio: Projects hover plus button, align recents with the label

- Hovering the Projects nav item reveals a plus button that opens the
  New project dialog, with the same circular hover treatment as the
  chat row actions
- Recent chat titles start at the same x as the Recents label
- The system prompt overflow lock only engages for a non-empty prompt
  with a laid-out box, so a mis-measure cannot turn clicks into the
  popup

* Clip system prompt overflow inside the rounded box

Wrap the inline system prompt textarea in a rounded overflow-hidden
surface so scrolled text and the scrollbar stay inside the box. The
focus ring moves to the wrapper via focus-within.

* Add updating progress bar to llama banner and shorten settings copy

While an update is applying, the banner action row becomes an
indeterminate progress bar that keeps animating under reduced motion,
matching the other loading indicators. Settings descriptions across
General, Profile, Appearance, Chat, Connections, API, and About are
trimmed without losing meaning.

* Address review: desktop update note, server platform detection, zh-CN keys

The About tab no longer shows terminal install commands in the desktop
app, where the bundled backend updates through the built-in updater;
it shows a short note and the docs links instead.

fetchDeviceType now sends the auth token to /api/health, which only
reports the server platform to authed callers, and caches only a
server-reported value. Copied install commands then match the host
platform rather than the browser when they differ (WSL, SSH).

zh-CN gains translations for the new notification and license keys,
the renamed About tab title, and the desktop update note.

* Real download progress for llama.cpp updates, prompt and sidebar polish

The update worker now streams the installer output and parses its
download percent lines into job progress, exposed via the update-status
API. The installer emits finer non-tty milestones when
UNSLOTH_PROGRESS_PERCENT_STEP is set; the worker requests 5 percent
steps. The banner renders a determinate bar from the reported fraction
and falls back to the sweep until the first percent arrives.

Also removes the focus ring on the inline system prompt box and
slightly shrinks the Projects hover plus icon.
2026-06-11 09:27:34 -07:00

123 lines
3.7 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Network-free Studio release version resolution for display-only UI."""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from utils import _studio_release_build
_DEV_VERSION = "dev"
_GIT_TIMEOUT_SECONDS = 1.0
_STUDIO_TAG_RE = re.compile(r"^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.][0-9A-Za-z.-]*)?$")
_GIT_DESCRIBE_SUFFIX_RE = re.compile(r"-\d+-g[0-9A-Fa-f]+(?:-dirty)?$")
_GIT_BRANCH_RE = re.compile(r"^[0-9A-Za-z._/-]+$")
_MAX_VERSION_LENGTH = 64
def is_valid_studio_release_version(value: object) -> bool:
"""Return True for Studio release tags such as ``v0.1.39-beta``."""
if not isinstance(value, str):
return False
version = value.strip()
if not version or len(version) > _MAX_VERSION_LENGTH:
return False
if version.endswith("-dirty") or _GIT_DESCRIBE_SUFFIX_RE.search(version):
return False
return _STUDIO_TAG_RE.fullmatch(version) is not None
def _repo_root() -> Path:
return Path(__file__).resolve().parents[3]
def _path_is_in_site_packages(path: Path) -> bool:
return any(part in {"site-packages", "dist-packages"} for part in path.parts)
def _is_source_checkout(repo_root: Path) -> bool:
return (repo_root / ".git").exists() and not _path_is_in_site_packages(Path(__file__).resolve())
def _exact_git_studio_tag(repo_root: Path) -> str | None:
try:
result = subprocess.run(
[
"git",
"describe",
"--tags",
"--exact-match",
"--match",
"v[0-9]*",
"HEAD",
],
cwd = repo_root,
check = False,
stdout = subprocess.PIPE,
stderr = subprocess.DEVNULL,
text = True,
timeout = _GIT_TIMEOUT_SECONDS,
)
except (OSError, subprocess.TimeoutExpired):
return None
if result.returncode != 0:
return None
tag = result.stdout.strip()
return tag if is_valid_studio_release_version(tag) else None
def _git_branch(repo_root: Path) -> str | None:
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd = repo_root,
check = False,
stdout = subprocess.PIPE,
stderr = subprocess.DEVNULL,
text = True,
timeout = _GIT_TIMEOUT_SECONDS,
)
except (OSError, subprocess.TimeoutExpired):
return None
if result.returncode != 0:
return None
branch = result.stdout.strip()
# "HEAD" means detached, e.g. a tag or commit checkout.
if (
not branch
or branch == "HEAD"
or len(branch) > _MAX_VERSION_LENGTH
or _GIT_BRANCH_RE.fullmatch(branch) is None
):
return None
return branch
def get_studio_version(repo_root: Path | None = None) -> str:
"""Return the installed Studio release tag for display, or ``dev``.
Intentionally separate from the PyPI ``unsloth`` package version used by
update checks. Never performs network requests.
"""
resolved_repo_root = repo_root or _repo_root()
if _is_source_checkout(resolved_repo_root):
git_tag = _exact_git_studio_tag(resolved_repo_root)
if git_tag is not None:
return git_tag
branch = _git_branch(resolved_repo_root)
return f"GitHub {branch}" if branch is not None else _DEV_VERSION
stamped_version = _studio_release_build.STUDIO_RELEASE_VERSION
if is_valid_studio_release_version(stamped_version):
return stamped_version.strip()
return _DEV_VERSION