diff --git a/install.ps1 b/install.ps1 index 77c0034125..2b7fd0533b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,6 +8,29 @@ function Install-UnslothStudio { $ErrorActionPreference = "Stop" $script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq "1") + # ── Tauri structured output ── + function Write-TauriLog { + param([string]$Tag, [string]$Message) + if ($TauriMode) { + Write-Host "[TAURI:$Tag] $Message" + } + } + + function Exit-InstallFailure { + param( + [Parameter(Mandatory = $true)][string]$Message, + [int]$Code = 1 + ) + if ($Code -eq 0) { $Code = 1 } + Write-TauriLog "ERROR" $Message + if (Get-Command Restore-StudioVenvRollback -CommandType Function -ErrorAction SilentlyContinue) { + Restore-StudioVenvRollback + } + if ($TauriMode) { + exit $Code + } + } + # ── Parse flags ── $StudioLocalInstall = $false $PackageName = "unsloth" @@ -26,7 +49,7 @@ function Install-UnslothStudio { $i++ if ($i -ge $argList.Count) { Write-Host "[ERROR] --package requires an argument." -ForegroundColor Red - return + return (Exit-InstallFailure "--package requires an argument.") } $PackageName = $argList[$i] } @@ -42,22 +65,14 @@ function Install-UnslothStudio { $RepoRoot = (Resolve-Path (Split-Path -Parent $PSCommandPath)).Path if (-not (Test-Path (Join-Path $RepoRoot "pyproject.toml"))) { Write-Host "[ERROR] --local must be run from the unsloth repo root (pyproject.toml not found at $RepoRoot)" -ForegroundColor Red - return + return (Exit-InstallFailure "--local must be run from the unsloth repo root") } } # Validate --package to prevent injection into shell/Python commands if ($PackageName -notmatch '^[a-zA-Z0-9][a-zA-Z0-9._-]*$') { Write-Host "[ERROR] --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" -ForegroundColor Red - return - } - - # ── Tauri structured output ── - function Write-TauriLog { - param([string]$Tag, [string]$Message) - if ($TauriMode) { - Write-Host "[TAURI:$Tag] $Message" - } + return (Exit-InstallFailure "--package name contains invalid characters") } $PythonVersion = "3.13" @@ -630,7 +645,7 @@ shell.Run cmd, 0, False step "winget" "not available" "Red" substep "Install it from https://aka.ms/getwinget" "Yellow" substep "or install Python $PythonVersion and uv manually, then re-run." "Yellow" - return + return (Exit-InstallFailure "winget is not available") } # ── Helper: detect a working Python 3.11-3.13 on the system ── @@ -749,7 +764,7 @@ shell.Run cmd, 0, False Write-Host " Please install Python $PythonVersion manually from https://www.python.org/downloads/" -ForegroundColor Yellow Write-Host " Make sure to check 'Add Python to PATH' during installation." -ForegroundColor Yellow Write-Host " Then re-run this installer." -ForegroundColor Yellow - return + return (Exit-InstallFailure "Python installation failed") } } @@ -773,7 +788,7 @@ shell.Run cmd, 0, False if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { step "uv" "could not be installed" "Red" substep "Install it from https://docs.astral.sh/uv/" "Yellow" - return + return (Exit-InstallFailure "uv could not be installed") } # ── Create venv (migrate old layout if possible, otherwise fresh) ── @@ -786,11 +801,68 @@ shell.Run cmd, 0, False $VenvPython = Join-Path $VenvDir "Scripts\python.exe" $_Migrated = $false + $script:StudioVenvRollbackDir = $null + $script:StudioVenvRollbackTarget = $VenvDir + $script:StudioVenvRollbackActive = $false + + function Start-StudioVenvRollback { + param([Parameter(Mandatory = $true)][string]$ExistingDir) + $stamp = Get-Date -Format "yyyyMMddHHmmss" + $candidate = Join-Path $StudioHome "unsloth_studio.rollback.$stamp.$PID" + $suffix = 0 + while (Test-Path $candidate) { + $suffix++ + $candidate = Join-Path $StudioHome "unsloth_studio.rollback.$stamp.$PID.$suffix" + } + Move-Item -Path $ExistingDir -Destination $candidate -ErrorAction Stop + $script:StudioVenvRollbackDir = $candidate + $script:StudioVenvRollbackTarget = $ExistingDir + $script:StudioVenvRollbackActive = $true + substep "previous environment preserved for rollback" + } + + function Restore-StudioVenvRollback { + if (-not $script:StudioVenvRollbackActive) { return } + $backup = $script:StudioVenvRollbackDir + $target = $script:StudioVenvRollbackTarget + if (-not $backup -or -not (Test-Path $backup)) { + $script:StudioVenvRollbackActive = $false + return + } + substep "restoring previous environment after failed install..." "Yellow" + try { + if (Test-Path $target) { + Remove-Item -Recurse -Force $target -ErrorAction SilentlyContinue + } + Move-Item -Path $backup -Destination $target -Force -ErrorAction Stop + substep "restored previous environment" + $script:StudioVenvRollbackActive = $false + $script:StudioVenvRollbackDir = $null + } catch { + Write-Host "[WARN] Could not restore previous environment from $backup to $target" -ForegroundColor Yellow + Write-Host " $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + function Complete-StudioVenvRollback { + if (-not $script:StudioVenvRollbackActive) { return } + $backup = $script:StudioVenvRollbackDir + if ($backup -and (Test-Path $backup)) { + Remove-Item -Recurse -Force $backup -ErrorAction SilentlyContinue + } + $script:StudioVenvRollbackActive = $false + $script:StudioVenvRollbackDir = $null + } if (Test-Path $VenvPython) { - # New layout already exists -- nuke for fresh install - substep "removing existing environment for fresh install..." - Remove-Item -Recurse -Force $VenvDir + # New layout already exists -- replace only after preserving rollback copy. + substep "preserving existing environment for rollback..." + try { + Start-StudioVenvRollback -ExistingDir $VenvDir + } catch { + Write-Host "[ERROR] Could not prepare existing environment for reinstall: $($_.Exception.Message)" -ForegroundColor Red + return (Exit-InstallFailure "Could not prepare existing environment for reinstall") + } } elseif (Test-Path (Join-Path $StudioHome ".venv\Scripts\python.exe")) { # Old layout (~/.unsloth/studio/.venv) exists -- validate before migrating $OldVenv = Join-Path $StudioHome ".venv" @@ -799,18 +871,23 @@ shell.Run cmd, 0, False $prevEAP2 = $ErrorActionPreference $ErrorActionPreference = "Continue" try { - & $OldPy -c "import torch; A = torch.ones((2,2)); B = A + A" 2>$null | Out-Null - $torchOk = ($LASTEXITCODE -eq 0) - } catch { $torchOk = $false } + if ($SkipTorch) { + & $OldPy -c "import sys; print(sys.executable)" 2>$null | Out-Null + } else { + & $OldPy -c "import torch; A = torch.ones((2,2)); B = A + A" 2>$null | Out-Null + } + $legacyOk = ($LASTEXITCODE -eq 0) + } catch { $legacyOk = $false } $ErrorActionPreference = $prevEAP2 - if ($torchOk) { + if ($legacyOk) { substep "legacy environment is healthy -- migrating..." Move-Item -Path $OldVenv -Destination $VenvDir -Force substep "moved .venv -> unsloth_studio" $_Migrated = $true } else { substep "legacy environment failed validation -- creating fresh environment" "Yellow" - Remove-Item -Recurse -Force $OldVenv -ErrorAction SilentlyContinue + $invalidVenv = Join-Path $StudioHome (".venv.invalid.{0}.{1}" -f (Get-Date -Format "yyyyMMddHHmmss"), $PID) + Move-Item -Path $OldVenv -Destination $invalidVenv -Force -ErrorAction SilentlyContinue } } elseif (Test-Path (Join-Path $env:USERPROFILE "unsloth_studio\Scripts\python.exe")) { # CWD-relative venv from old install.ps1 -- migrate to absolute path @@ -826,9 +903,8 @@ shell.Run cmd, 0, False substep "$VenvDir" $venvExit = Invoke-InstallCommand { uv venv $VenvDir --python "$($DetectedPython.Path)" } if ($venvExit -ne 0) { - Write-TauriLog "ERROR" "Failed to create virtual environment (exit code $venvExit)" Write-Host "[ERROR] Failed to create virtual environment (exit code $venvExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to create virtual environment (exit code $venvExit)" $venvExit) } } else { step "venv" "using migrated environment" @@ -946,14 +1022,14 @@ shell.Run cmd, 0, False } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) } if ($StudioLocalInstall) { substep "overlaying local repo (editable)..." $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } if ($overlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) } } } elseif ($TorchIndexUrl) { @@ -964,9 +1040,8 @@ shell.Run cmd, 0, False substep "installing PyTorch ($TorchIndexUrl)..." $torchInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl } if ($torchInstallExit -ne 0) { - Write-TauriLog "ERROR" "Failed to install PyTorch (exit code $torchInstallExit)" Write-Host "[ERROR] Failed to install PyTorch (exit code $torchInstallExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to install PyTorch (exit code $torchInstallExit)" $torchInstallExit) } } @@ -988,9 +1063,8 @@ shell.Run cmd, 0, False $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } } if ($baseInstallExit -ne 0) { - Write-TauriLog "ERROR" "Failed to install unsloth (exit code $baseInstallExit)" Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) } if ($StudioLocalInstall) { @@ -998,7 +1072,7 @@ shell.Run cmd, 0, False $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } if ($overlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) } } } else { @@ -1009,53 +1083,23 @@ shell.Run cmd, 0, False $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.8" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) } substep "overlaying local repo (editable)..." $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } if ($overlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) } } else { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --torch-backend=auto -- "$PackageName" } if ($baseInstallExit -ne 0) { - Write-TauriLog "ERROR" "Failed to install unsloth (exit code $baseInstallExit)" Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) } } } - # Hotfix: patch install_python_stack.py for Windows GUI stdout - # The PyPI version crashes with OSError when stdout is piped from a GUI app. - # Copy our fixed version (bundled by Tauri) over the installed one. - # Remove this block once PyPI ships the fix from commit 18c5aae7. - if ($TauriMode) { - $rawPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.ScriptName } - $scriptDir = Split-Path -Parent ($rawPath -replace '^\\\\\?\\', '') - $fixedPy = Join-Path $scriptDir "install_python_stack.py" - $target = Join-Path $VenvDir "Lib\site-packages\studio\install_python_stack.py" - $sentinel = "# UNSLOTH_DESKTOP_HOTFIX_APPLIED_v1" - $sentinelPattern = [regex]::Escape($sentinel) - if ((Test-Path $fixedPy) -and (Test-Path $target)) { - $installed = Get-Content $target -Raw - if ($installed -notmatch $sentinelPattern) { - Copy-Item $fixedPy $target -Force - Add-Content -Path $target -Value "`n$sentinel" - substep "patched install_python_stack.py (stdout fix)" - } else { - substep "install_python_stack.py already has stdout fix" - } - } elseif ((Test-Path $fixedPy) -and (Test-Path (Split-Path $target))) { - Copy-Item $fixedPy $target -Force - Add-Content -Path $target -Value "`n$sentinel" - substep "patched install_python_stack.py (stdout fix)" - } else { - Write-Host "[WARN] Could not patch install_python_stack.py (bundled file or target dir missing)" -ForegroundColor Yellow - } - } - # ── Run studio setup ── # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. @@ -1063,12 +1107,11 @@ shell.Run cmd, 0, False step "setup" "running unsloth studio setup..." $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" if (-not (Test-Path $UnslothExe)) { - Write-TauriLog "ERROR" "unsloth CLI was not installed correctly" Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow Write-Host " This usually means an older unsloth version was installed that does not include the Studio CLI." -ForegroundColor Yellow Write-Host " Try re-running the installer or see: https://github.com/unslothai/unsloth?tab=readme-ov-file#-quickstart" -ForegroundColor Yellow - return + return (Exit-InstallFailure "unsloth CLI was not installed correctly") } # Tell setup.ps1 to skip base package installation (install.ps1 already did it) $env:SKIP_STUDIO_BASE = "1" @@ -1090,12 +1133,16 @@ shell.Run cmd, 0, False # and bypass the fast-path version check from PR #4667. $studioArgs = @('studio', 'setup') if ($script:UnslothVerbose) { $studioArgs += '--verbose' } - & $UnslothExe @studioArgs - $setupExit = $LASTEXITCODE + $env:UNSLOTH_INSTALL_ROLLBACK_MANAGED = "1" + try { + & $UnslothExe @studioArgs + $setupExit = $LASTEXITCODE + } finally { + Remove-Item Env:UNSLOTH_INSTALL_ROLLBACK_MANAGED -ErrorAction SilentlyContinue + } if ($setupExit -ne 0) { - Write-TauriLog "ERROR" "unsloth studio setup failed (exit code $setupExit)" Write-Host "[ERROR] unsloth studio setup failed (exit code $setupExit)" -ForegroundColor Red - return + return (Exit-InstallFailure "unsloth studio setup failed (exit code $setupExit)" $setupExit) } # ── Expose `unsloth` via a shim dir containing only unsloth.exe ── @@ -1168,6 +1215,7 @@ shell.Run cmd, 0, False step "path" "added unsloth launcher to PATH" } Refresh-SessionPath # sync current session with registry + Complete-StudioVenvRollback # ── Tauri mode: done, skip shortcuts and auto-launch ── if ($TauriMode) { diff --git a/install.sh b/install.sh index 6c28b6eda4..95ab382f2d 100755 --- a/install.sh +++ b/install.sh @@ -165,6 +165,60 @@ tauri_log() { PYTHON_VERSION="" # resolved after platform detection STUDIO_HOME="$HOME/.unsloth/studio" VENV_DIR="$STUDIO_HOME/unsloth_studio" +_VENV_ROLLBACK_DIR="" +_VENV_ROLLBACK_TARGET="$VENV_DIR" +_VENV_ROLLBACK_ACTIVE=false + +_start_studio_venv_replacement() { + _existing_dir="$1" + _stamp=$(date +%Y%m%d%H%M%S 2>/dev/null || echo "time") + _candidate="$STUDIO_HOME/unsloth_studio.rollback.$_stamp.$$" + _suffix=0 + while [ -e "$_candidate" ]; do + _suffix=$((_suffix + 1)) + _candidate="$STUDIO_HOME/unsloth_studio.rollback.$_stamp.$$.$_suffix" + done + mv "$_existing_dir" "$_candidate" + _VENV_ROLLBACK_DIR="$_candidate" + _VENV_ROLLBACK_TARGET="$_existing_dir" + _VENV_ROLLBACK_ACTIVE=true + substep "previous environment preserved for rollback" +} + +_restore_studio_venv_replacement() { + [ "$_VENV_ROLLBACK_ACTIVE" = true ] || return 0 + [ -n "$_VENV_ROLLBACK_DIR" ] && [ -d "$_VENV_ROLLBACK_DIR" ] || { + _VENV_ROLLBACK_ACTIVE=false + return 0 + } + substep "restoring previous environment after failed install..." "$C_WARN" + rm -rf "$_VENV_ROLLBACK_TARGET" + if mv "$_VENV_ROLLBACK_DIR" "$_VENV_ROLLBACK_TARGET"; then + substep "restored previous environment" + _VENV_ROLLBACK_ACTIVE=false + _VENV_ROLLBACK_DIR="" + else + echo "⚠️ Could not restore previous environment from $_VENV_ROLLBACK_DIR to $_VENV_ROLLBACK_TARGET" >&2 + fi +} + +_commit_studio_venv_replacement() { + [ "$_VENV_ROLLBACK_ACTIVE" = true ] || return 0 + if [ -n "$_VENV_ROLLBACK_DIR" ] && [ -d "$_VENV_ROLLBACK_DIR" ]; then + rm -rf "$_VENV_ROLLBACK_DIR" || true + fi + _VENV_ROLLBACK_ACTIVE=false + _VENV_ROLLBACK_DIR="" +} + +_on_install_exit() { + _status=$? + if [ "$_status" -ne 0 ]; then + _restore_studio_venv_replacement + fi + exit "$_status" +} +trap _on_install_exit EXIT # ── Helper: download a URL to a file (supports curl and wget) ── download() { @@ -883,9 +937,15 @@ if [ -n "$MISSING" ]; then if command -v apt-get >/dev/null 2>&1; then _smart_apt_install $MISSING else - echo " apt-get is not available. Please install with your package manager:" + echo " Automatic system package installation is supported on apt-based" + echo " Linux distributions (Ubuntu/Debian) only. Please install the" + echo " missing dependencies with your package manager, then re-run setup:" echo " $MISSING" - echo " Then re-run Unsloth Studio setup." + echo "" + echo " Examples:" + echo " Fedora/RHEL: sudo dnf install cmake git gcc gcc-c++ make libcurl-devel" + echo " Arch: sudo pacman -S --needed cmake git base-devel curl" + echo " openSUSE: sudo zypper install cmake git gcc gcc-c++ make libcurl-devel" exit 1 fi ;; @@ -957,12 +1017,19 @@ mkdir -p "$STUDIO_HOME" _MIGRATED=false if [ -x "$VENV_DIR/bin/python" ]; then - # New layout already exists — nuke for fresh install - rm -rf "$VENV_DIR" + # New layout already exists — replace only after preserving rollback copy. + substep "preserving existing environment for rollback..." + _start_studio_venv_replacement "$VENV_DIR" elif [ -x "$STUDIO_HOME/.venv/bin/python" ]; then - # Old layout exists — validate before migrating + # Old layout exists — validate before migrating. + # In no-torch mode, a missing torch package is expected; validate Python only. substep "found legacy Studio environment, validating..." - if "$STUDIO_HOME/.venv/bin/python" -c " + _legacy_ok=false + if [ "$SKIP_TORCH" = true ]; then + if "$STUDIO_HOME/.venv/bin/python" -c "import sys; print(sys.executable)" >/dev/null 2>&1; then + _legacy_ok=true + fi + elif "$STUDIO_HOME/.venv/bin/python" -c " import torch device = 'cuda' if torch.cuda.is_available() else 'cpu' A = torch.ones((10, 10), device=device) @@ -972,13 +1039,17 @@ D = A + B E = D @ C torch.testing.assert_close(torch.unique(E), torch.tensor((20,), device=E.device, dtype=E.dtype)) " >/dev/null 2>&1; then + _legacy_ok=true + fi + if [ "$_legacy_ok" = true ]; then echo "✅ Legacy environment is healthy — migrating..." mv "$STUDIO_HOME/.venv" "$VENV_DIR" echo " Moved ~/.unsloth/studio/.venv → $VENV_DIR" _MIGRATED=true else echo "⚠️ Legacy environment failed validation — creating fresh environment" - rm -rf "$STUDIO_HOME/.venv" + _invalid_venv="$STUDIO_HOME/.venv.invalid.$(date +%Y%m%d%H%M%S 2>/dev/null || echo time).$$" + mv "$STUDIO_HOME/.venv" "$_invalid_venv" 2>/dev/null || true fi fi @@ -1679,6 +1750,8 @@ if [ "$_SETUP_EXIT" -ne 0 ]; then exit "$_SETUP_EXIT" fi +_commit_studio_venv_replacement + # ── Tauri mode: done, skip shortcuts and auto-launch ── if [ "$TAURI_MODE" = true ]; then tauri_log "DONE" "" diff --git a/studio/frontend/src/app/auth-guards.ts b/studio/frontend/src/app/auth-guards.ts index 509b8f61af..52230f0b6f 100644 --- a/studio/frontend/src/app/auth-guards.ts +++ b/studio/frontend/src/app/auth-guards.ts @@ -9,7 +9,6 @@ import { hasRefreshToken, mustChangePassword, refreshSession, - tauriAutoAuth, } from "@/features/auth"; async function hasActiveSession(): Promise { @@ -39,7 +38,7 @@ function authRedirect(to: "/login" | "/change-password"): never { export async function requireAuth(): Promise { if (isTauri) { - await tauriAutoAuth(); + // AppProvider owns backend startup + desktop auth; route guards run before it mounts. return; } @@ -59,7 +58,6 @@ export async function requireAuth(): Promise { export async function requireGuest(): Promise { if (isTauri) { - await tauriAutoAuth(); throw redirect({ to: "/chat" }); } if (!(await hasActiveSession())) return; @@ -68,7 +66,6 @@ export async function requireGuest(): Promise { export async function requirePasswordChangeFlow(): Promise { if (isTauri) { - await tauriAutoAuth(); throw redirect({ to: "/chat" }); } diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index b75998a169..cacf318deb 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -4,12 +4,18 @@ import { StartupScreen } from "@/components/tauri/startup-screen"; import { UpdateBanner } from "@/components/tauri/update-banner"; import { UpdateScreen } from "@/components/tauri/update-screen"; +import { + WindowTitlebar, + shouldUseCustomWindowTitlebar, +} from "@/components/tauri/window-titlebar"; import { Toaster } from "@/components/ui/sonner"; +import { getTauriAuthFailure, tauriAutoAuth } from "@/features/auth"; import { useTauriBackend } from "@/hooks/use-tauri-backend"; import { useTauriUpdate } from "@/hooks/use-tauri-update"; import { isTauri } from "@/lib/api-base"; +import { useRouterState } from "@tanstack/react-router"; import { ThemeProvider } from "next-themes"; -import { useEffect, useRef, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; interface AppProviderProps { children: ReactNode; @@ -119,7 +125,15 @@ function TauriUpdateLayer({ isExternalServer }: { isExternalServer: boolean }) { ); } +const HIDDEN_TITLEBAR_SIDEBAR_ROUTES = new Set([ + "/onboarding", + "/login", + "/change-password", + "/signup", +]); + function TauriWrapper({ children }: { children: ReactNode }) { + const pathname = useRouterState({ select: (s) => s.location.pathname }); const { status, logs, error, isExternalServer, currentStepIndex, progressDetail, elevationPackages, @@ -128,6 +142,8 @@ function TauriWrapper({ children }: { children: ReactNode }) { const hasResized = useRef(false); const abortRef = useRef(false); + const [desktopAuthReady, setDesktopAuthReady] = useState(!isTauri); + const [desktopAuthRetry, setDesktopAuthRetry] = useState(0); // Show the window once the frontend mounts (for pre-running states) useEffect(() => { @@ -150,16 +166,56 @@ function TauriWrapper({ children }: { children: ReactNode }) { return () => { abortRef.current = true; }; }, [status]); - if (!isTauri) return <>{children}; - if (status === "running") return <>{children}; + useEffect(() => { + if (!isTauri) { + setDesktopAuthReady(true); + return; + } + if (status !== "running") { + setDesktopAuthReady(false); + setDesktopAuthRetry(0); + return; + } - return ( + let disposed = false; + setDesktopAuthReady(false); + tauriAutoAuth({ force: true }).then((authenticated) => { + if (disposed) return; + if (authenticated) { + setDesktopAuthReady(true); + return; + } + if (!getTauriAuthFailure()) { + window.setTimeout(() => { + if (!disposed) setDesktopAuthRetry((value) => value + 1); + }, 500); + } + }); + + return () => { disposed = true; }; + }, [status, desktopAuthRetry]); + + if (!isTauri) return <>{children}; + + const showApp = status === "running" && desktopAuthReady; + const startupStatus = status === "running" ? "starting" : status; + const startupProgressDetail = + status === "running" && !desktopAuthReady + ? "Signing in to desktop session..." + : progressDetail; + + const content = showApp ? ( + <> + + {children} + + ) : ( ); + + if (!shouldUseCustomWindowTitlebar()) return content; + + const showSidebarSurface = + showApp && !HIDDEN_TITLEBAR_SIDEBAR_ROUTES.has(pathname); + + return ( +
+ +
+ {content} +
+
+ ); } export function AppProvider({ children }: AppProviderProps) { diff --git a/studio/frontend/src/app/routes/__root.tsx b/studio/frontend/src/app/routes/__root.tsx index 19b5763557..22d149473c 100644 --- a/studio/frontend/src/app/routes/__root.tsx +++ b/studio/frontend/src/app/routes/__root.tsx @@ -81,7 +81,7 @@ function RootLayout() { pinned={pinned} setPinned={setPinned} togglePinned={togglePinned} - className="!min-h-0 h-dvh overflow-hidden" + className="!min-h-0 h-[calc(100dvh-var(--studio-titlebar-height,0px))] overflow-hidden" > diff --git a/studio/frontend/src/components/tauri/startup-screen.tsx b/studio/frontend/src/components/tauri/startup-screen.tsx index af5f241416..2b8a204afe 100644 --- a/studio/frontend/src/components/tauri/startup-screen.tsx +++ b/studio/frontend/src/components/tauri/startup-screen.tsx @@ -381,7 +381,7 @@ export function StartupScreen({ } return ( -
+
+
platform.includes(token)); +} + +async function getAppWindow(): Promise { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + return getCurrentWindow(); +} + +function WindowControlButton({ + label, + className, + onClick, + children, +}: { + label: string; + className?: string; + onClick: () => void; + children: ReactNode; +}): ReactElement { + return ( + + ); +} + +function MinimizeGlyph(): ReactElement { + return ( +