Add no-browser launch option for the Studio desktop launcher

The generated launchers (launch-studio.sh / launch-studio.ps1) always
opened the default browser once the server became healthy. Add a
--no-browser launcher flag, the UNSLOTH_STUDIO_NO_BROWSER env var, and
a persisted installer preference (studio.conf / baked into the ps1
launcher) with an interactive install prompt. When auto-open is off the
launcher still starts or attaches to the server and prints the URL, for
users who run Studio as a browser PWA or app window. The
--shortcuts-only refresh run by studio update preserves the choice.
This commit is contained in:
Unsloth 2026-07-08 22:15:16 -07:00
commit c980658592
4 changed files with 277 additions and 3 deletions

View file

@ -86,6 +86,8 @@ unsloth studio -p 8888
```
For cloud or global access, add `-H 0.0.0.0`. By default, Unsloth is accessible only locally.
Launching from the terminal never opens a browser -- open the printed URL yourself. The desktop shortcut opens your default browser once the server is up; to launch the server without that (e.g. when running Studio as a browser PWA / app window), answer "n" at the installer's browser prompt, pass `--no-browser` to the launcher, or set `UNSLOTH_STUDIO_NO_BROWSER=1`.
To reach Studio over HTTPS, use `unsloth studio --secure`. Studio stays bound to localhost and is reached only through a free Cloudflare tunnel, which publishes it at a public `https://*.trycloudflare.com` URL (it fails closed if the tunnel can't start, so the raw port is never exposed). This makes Studio reachable from the internet, so anyone with the link and API key can use it and run code: keep your API key private (see Remote access below).
#### Docker

View file

@ -99,6 +99,8 @@ function Install-UnslothStudio {
$TauriMode = $false
$SkipTorch = $false
$ShortcutsOnly = $false
# Launcher browser auto-open: "" = undecided (prompt, else keep existing, else on).
$OpenBrowserPref = ""
$WithLlamaCppDir = ""
$argList = $args
for ($i = 0; $i -lt $argList.Count; $i++) {
@ -109,6 +111,8 @@ function Install-UnslothStudio {
"--verbose" { $script:UnslothVerbose = $true }
"-v" { $script:UnslothVerbose = $true }
"--shortcuts-only" { $ShortcutsOnly = $true }
"--no-browser" { $OpenBrowserPref = '0' }
"--browser" { $OpenBrowserPref = '1' }
"--package" {
$i++
if ($i -ge $argList.Count) {
@ -640,6 +644,20 @@ function Install-UnslothStudio {
"`$portFile = `$null`n`$mutexName = 'Local\UnslothStudioLauncher'`n"
}
# Browser auto-open: explicit installer choice wins; else keep the
# value baked into the existing launcher so `studio update`
# (--shortcuts-only) never resets it.
$_openBrowser = $OpenBrowserPref
if (-not $_openBrowser -and (Test-Path -LiteralPath $launcherPs1)) {
try {
$_prevLauncher = [System.IO.File]::ReadAllText($launcherPs1)
if ($_prevLauncher -match "(?m)^\`$openBrowserDefault = '([01])'") {
$_openBrowser = $Matches[1]
}
} catch {}
}
if ($_openBrowser -ne '0') { $_openBrowser = '1' }
$launcherContent = @"
$studioHomeExport`$ErrorActionPreference = 'Stop'
`$basePort = 8888
@ -647,6 +665,30 @@ $studioHomeExport`$ErrorActionPreference = 'Stop'
`$timeoutSec = 60
`$pollIntervalMs = 1000
`$_ExpectedStudioRootId = '$_studioRootId'
`$openBrowserDefault = '$_openBrowser'
# Browser auto-open: disabled by -NoBrowser/--no-browser, the
# UNSLOTH_STUDIO_NO_BROWSER env var, or the baked installer preference.
# When off the server still starts; the URL is printed instead (PWA use).
`$openBrowser = (`$openBrowserDefault -ne '0')
if (`$env:UNSLOTH_STUDIO_NO_BROWSER -and
(`$env:UNSLOTH_STUDIO_NO_BROWSER -notin @('0', 'false', 'no', 'off'))) {
`$openBrowser = `$false
}
foreach (`$_launchArg in `$args) {
if (`$_launchArg -in @('-NoBrowser', '--no-browser')) { `$openBrowser = `$false }
elseif (`$_launchArg -in @('-Browser', '--browser')) { `$openBrowser = `$true }
}
function Open-StudioUrl {
param([Parameter(Mandatory = `$true)][string]`$Url)
if (`$openBrowser) {
Start-Process `$Url
} else {
# Hidden-window launches have no console; never fail on the echo.
try { Write-Host "Unsloth Studio is running at: `$Url" } catch {}
}
}
function Test-StudioHealth {
param([Parameter(Mandatory = `$true)][int]`$Port)
@ -743,7 +785,7 @@ function Find-FreeLaunchPort {
# If Studio is already healthy on any expected port, just open it and exit.
`$existingPort = Find-HealthyStudioPort
if (`$existingPort) {
Start-Process "http://localhost:`$existingPort"
Open-StudioUrl "http://localhost:`$existingPort"
exit 0
}
@ -760,7 +802,7 @@ try {
`$deadline = (Get-Date).AddSeconds(`$timeoutSec)
while ((Get-Date) -lt `$deadline) {
`$port = Find-HealthyStudioPort
if (`$port) { Start-Process "http://localhost:`$port"; exit 0 }
if (`$port) { Open-StudioUrl "http://localhost:`$port"; exit 0 }
Start-Sleep -Milliseconds `$pollIntervalMs
}
exit 0
@ -809,7 +851,7 @@ try {
[System.IO.File]::WriteAllText(`$portFile, "`$launchPort`n")
} catch {}
}
Start-Process "http://localhost:`$launchPort"
Open-StudioUrl "http://localhost:`$launchPort"
`$browserOpened = `$true
break
}
@ -2570,6 +2612,16 @@ exit 0
return
}
# Ask once (interactive installs only) whether the launcher should open
# the browser after the server is up. Skipped when --no-browser/--browser
# was passed or input is redirected; then an existing choice is kept.
$_browserPromptOk = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
if (-not $OpenBrowserPref -and $_browserPromptOk) {
Write-Host ""
$_browserReply = Read-Host " Open Unsloth Studio in your default browser after launch? [Y/n]"
$OpenBrowserPref = if ($_browserReply -match '^[Nn]') { '0' } else { '1' }
}
# New-StudioShortcuts gates the .lnk shortcuts on env-mode internally.
New-StudioShortcuts -UnslothExePath $UnslothExe

View file

@ -12,6 +12,7 @@
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_PYTHON=3.12 sh # pin Python version
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_STUDIO_HOME=/abs/path sh
# Equivalent flags: ./install.sh --no-torch --python 3.12 (or pipe them: sh -s -- --no-torch)
# ./install.sh --no-browser: launcher starts the server without opening the browser.
#
# Install dir priority: UNSLOTH_STUDIO_HOME > STUDIO_HOME (alias) > $HOME/.unsloth/studio
#
@ -51,6 +52,8 @@ _USER_PYTHON=""
_NO_TORCH_FLAG=false
_VERBOSE=false
_SHORTCUTS_ONLY=false
# Launcher browser auto-open: "" = undecided (prompt, else keep existing, else on).
_STUDIO_OPEN_BROWSER=""
_next_is_package=false
_next_is_python=false
_next_is_llama_cpp_dir=false
@ -82,6 +85,8 @@ for arg in "$@"; do
--no-torch) _NO_TORCH_FLAG=true ;;
--verbose|-v) _VERBOSE=true ;;
--shortcuts-only) _SHORTCUTS_ONLY=true ;;
--no-browser) _STUDIO_OPEN_BROWSER=0 ;;
--browser) _STUDIO_OPEN_BROWSER=1 ;;
--with-llama-cpp-dir) _next_is_llama_cpp_dir=true ;;
esac
done
@ -647,6 +652,21 @@ if [ -z "${UNSLOTH_EXE:-}" ] || [ ! -x "${UNSLOTH_EXE:-}" ]; then
exit 1
fi
# Browser auto-open. Priority: --no-browser/--browser arg, then
# UNSLOTH_STUDIO_NO_BROWSER env var, then studio.conf, default on.
# When off the server still starts; the URL is printed instead (PWA use).
OPEN_BROWSER="${STUDIO_OPEN_BROWSER:-1}"
case "${UNSLOTH_STUDIO_NO_BROWSER:-}" in
''|0|false|FALSE|no|NO|off|OFF) ;;
*) OPEN_BROWSER=0 ;;
esac
for _arg in "$@"; do
case "$_arg" in
--no-browser) OPEN_BROWSER=0 ;;
--browser) OPEN_BROWSER=1 ;;
esac
done
BASE_PORT=8888
MAX_PORT_OFFSET=20
TIMEOUT_SEC=60
@ -780,6 +800,10 @@ _find_launch_port() {
# ── Open browser ──
_open_browser() {
_url="$1"
if [ "$OPEN_BROWSER" = "0" ]; then
echo "Unsloth Studio is running at: $_url"
return 0
fi
if [ "$(uname)" = "Darwin" ] && command -v open >/dev/null 2>&1; then
open "$_url"
elif grep -qi microsoft /proc/version 2>/dev/null; then
@ -1011,11 +1035,21 @@ LAUNCHER_EOF
chmod +x "$_css_launcher"
# Browser auto-open: explicit installer choice wins; else keep the existing
# studio.conf value so `studio update` (--shortcuts-only) never resets it.
_css_open_browser="${_STUDIO_OPEN_BROWSER:-}"
if [ -z "$_css_open_browser" ] && [ -f "$_css_data_dir/studio.conf" ]; then
_css_open_browser=$(sed -n "s/^STUDIO_OPEN_BROWSER='\([01]\)'\$/\1/p" \
"$_css_data_dir/studio.conf" 2>/dev/null | head -n 1)
fi
[ "$_css_open_browser" = "0" ] || _css_open_browser=1
# studio.conf: exe path + (env-mode only) persisted env vars so fresh
# shells launch the right install without re-exporting.
_css_quoted_exe=$(printf '%s' "$_css_exe" | sed "s/'/'\\\\''/g")
{
printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'"
printf '%s\n' "STUDIO_OPEN_BROWSER='$_css_open_browser'"
if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then
# When an override resolves to the legacy default, llama.cpp
# still lives at ~/.unsloth/llama.cpp (one shared build).
@ -3160,6 +3194,18 @@ esac
# create_studio_shortcuts gates persistent menu shortcuts on env-mode;
# launcher + studio.conf + icon are always written.
if [ "$TAURI_MODE" != true ]; then
# Ask once (interactive installs only) whether the launcher should open
# the browser after the server is up. Skipped when --no-browser/--browser
# was passed or no TTY; then an existing choice is kept, defaulting to on.
if [ -z "$_STUDIO_OPEN_BROWSER" ] && [ -t 1 ] && [ -r /dev/tty ]; then
echo ""
printf " Open Unsloth Studio in your default browser after launch? [Y/n] "
read -r _browser_reply </dev/tty || _browser_reply="y"
case "$_browser_reply" in
[nN]*) _STUDIO_OPEN_BROWSER=0 ;;
*) _STUDIO_OPEN_BROWSER=1 ;;
esac
fi
create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"
fi

View file

@ -0,0 +1,174 @@
#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
# The generated desktop launchers must support suppressing the automatic
# default-browser open after the server becomes healthy (PWA users run Studio
# in a browser-app window, not the OS default browser). Covered surface:
# - launch-studio.sh: --no-browser flag, UNSLOTH_STUDIO_NO_BROWSER env var,
# persisted STUDIO_OPEN_BROWSER from studio.conf; prints the URL when off.
# - install.sh: --no-browser flag, interactive prompt, studio.conf persistence
# that survives the --shortcuts-only refresh run by `unsloth studio update`.
# - install.ps1: same feature baked into launch-studio.ps1 (Open-StudioUrl).
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/../../install.sh"
INSTALL_PS1="$SCRIPT_DIR/../../install.ps1"
PASS=0
FAIL=0
assert_contains() {
_label="$1"; _haystack="$2"; _needle="$3"
if echo "$_haystack" | grep -qF -- "$_needle"; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected to find '$_needle')"
FAIL=$((FAIL + 1))
fi
}
assert_not_contains() {
_label="$1"; _haystack="$2"; _needle="$3"
if echo "$_haystack" | grep -qF -- "$_needle"; then
echo " FAIL: $_label (found '$_needle' but should not)"
FAIL=$((FAIL + 1))
else
echo " PASS: $_label"
PASS=$((PASS + 1))
fi
}
echo ""
echo "=== install.sh launcher template ==="
# Extract the heredoc that generates ~/.local/share/unsloth/launch-studio.sh.
_launcher=$(awk '/cat > "\$_css_launcher"/{found=1} found{print} /^LAUNCHER_EOF$/{found=0}' "$INSTALL_SH")
assert_contains \
"launcher template: --no-browser argument handled" \
"$_launcher" "--no-browser) OPEN_BROWSER=0"
assert_contains \
"launcher template: UNSLOTH_STUDIO_NO_BROWSER env var handled" \
"$_launcher" "UNSLOTH_STUDIO_NO_BROWSER"
assert_contains \
"launcher template: studio.conf preference is the default" \
"$_launcher" 'OPEN_BROWSER="${STUDIO_OPEN_BROWSER:-1}"'
assert_contains \
"launcher template: _open_browser is gated on OPEN_BROWSER" \
"$_launcher" '[ "$OPEN_BROWSER" = "0" ]'
assert_contains \
"launcher template: URL still printed when auto-open is off" \
"$_launcher" "Unsloth Studio is running at:"
echo ""
echo "=== install.sh installer plumbing ==="
_installer=$(cat "$INSTALL_SH")
assert_contains \
"install.sh: --no-browser flag parsed" \
"$_installer" "--no-browser) _STUDIO_OPEN_BROWSER=0"
assert_contains \
"install.sh: preference persisted into studio.conf" \
"$_installer" "STUDIO_OPEN_BROWSER='\$_css_open_browser'"
assert_contains \
"install.sh: existing studio.conf choice preserved on refresh" \
"$_installer" "s/^STUDIO_OPEN_BROWSER="
assert_contains \
"install.sh: interactive prompt asks about browser auto-open" \
"$_installer" "Open Unsloth Studio in your default browser after launch?"
echo ""
echo "=== install.sh _open_browser gating (functional) ==="
# Extract the _open_browser function from the (column-0) launcher heredoc and
# drive it with stubbed browser openers on PATH.
_fn=$(printf '%s\n' "$_launcher" | awk '/^_open_browser\(\) \{/{found=1} found{print} found && /^\}/{exit}')
if [ -z "$_fn" ]; then
echo " FAIL: could not extract _open_browser from launcher template"
FAIL=$((FAIL + 1))
else
_tmp=$(mktemp -d)
trap 'rm -rf "$_tmp"' EXIT
for _stub in open xdg-open; do
printf '#!/bin/sh\necho "BROWSER_OPENED:$1" >> "$RECORD"\n' > "$_tmp/$_stub"
chmod +x "$_tmp/$_stub"
done
# Off: no browser process, URL echoed instead.
_out=$(RECORD="$_tmp/record_off" PATH="$_tmp:$PATH" bash -c \
"OPEN_BROWSER=0; $_fn; _open_browser http://localhost:9999")
assert_contains \
"OPEN_BROWSER=0 prints the URL" \
"$_out" "Unsloth Studio is running at: http://localhost:9999"
if [ -f "$_tmp/record_off" ]; then
echo " FAIL: OPEN_BROWSER=0 still invoked a browser opener"
FAIL=$((FAIL + 1))
else
echo " PASS: OPEN_BROWSER=0 does not invoke a browser opener"
PASS=$((PASS + 1))
fi
# On (default): browser opener invoked with the URL.
RECORD="$_tmp/record_on" PATH="$_tmp:$PATH" bash -c \
"OPEN_BROWSER=1; $_fn; _open_browser http://localhost:9999" > /dev/null
# xdg-open is backgrounded inside _open_browser; give the stub a moment.
_i=0
while [ ! -s "$_tmp/record_on" ] && [ "$_i" -lt 20 ]; do
sleep 0.1
_i=$((_i + 1))
done
assert_contains \
"OPEN_BROWSER=1 invokes a browser opener with the URL" \
"$(cat "$_tmp/record_on" 2>/dev/null)" "BROWSER_OPENED:http://localhost:9999"
fi
echo ""
echo "=== install.ps1 launcher template ==="
# grep the file directly: piping the whole installer into grep -q trips
# SIGPIPE noise from echo once grep exits on first match.
assert_file_contains() {
_label="$1"; _file="$2"; _needle="$3"
if grep -qF -- "$_needle" "$_file"; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected to find '$_needle')"
FAIL=$((FAIL + 1))
fi
}
assert_file_contains \
"install.ps1: --no-browser flag parsed" \
"$INSTALL_PS1" "\"--no-browser\" { \$OpenBrowserPref = '0' }"
assert_file_contains \
"install.ps1: preference baked into launch-studio.ps1" \
"$INSTALL_PS1" "openBrowserDefault = '\$_openBrowser'"
assert_file_contains \
"install.ps1: launcher honors UNSLOTH_STUDIO_NO_BROWSER" \
"$INSTALL_PS1" "UNSLOTH_STUDIO_NO_BROWSER"
assert_file_contains \
"install.ps1: gated helper defined" \
"$INSTALL_PS1" "function Open-StudioUrl {"
assert_file_contains \
"install.ps1: interactive prompt asks about browser auto-open" \
"$INSTALL_PS1" "Open Unsloth Studio in your default browser after launch?"
# All launcher URL opens must route through the gated helper.
_ps1_direct_open=$(grep -cF 'Start-Process "http://localhost:' "$INSTALL_PS1" || true)
if [ "$_ps1_direct_open" -eq 0 ]; then
echo " PASS: no ungated Start-Process http://localhost calls remain"
PASS=$((PASS + 1))
else
echo " FAIL: $_ps1_direct_open ungated Start-Process http://localhost call(s) remain"
FAIL=$((FAIL + 1))
fi
echo ""
echo "=== Results ==="
echo " PASS: $PASS"
echo " FAIL: $FAIL"
if [ "$FAIL" -gt 0 ]; then
echo "FAILED"
exit 1
fi
echo "ALL PASSED"