Windows dev experience: CUDA detection, GPU display cleanup, hide drag region, dev-mode update status

This commit is contained in:
Jamie Pine
2026-03-14 03:24:39 -07:00
parent e18757bab3
commit e89a7eb7e7
7 changed files with 175 additions and 142 deletions
@@ -113,7 +113,7 @@ export function ConnectionForm() {
<Badge variant={health.gpu_available ? 'default' : 'secondary'}> <Badge variant={health.gpu_available ? 'default' : 'secondary'}>
GPU: {health.gpu_available ? 'Available' : 'Not Available'} GPU: {health.gpu_available ? 'Available' : 'Not Available'}
</Badge> </Badge>
{health.vram_used_mb && ( {health.vram_used_mb != null && health.vram_used_mb > 0 && (
<Badge variant="outline">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge> <Badge variant="outline">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>
)} )}
</div> </div>
@@ -26,7 +26,7 @@ export function GpuAcceleration() {
// Query CUDA backend status // Query CUDA backend status
const { const {
data: cudaStatus, data: cudaStatus,
isLoading: cudaStatusLoading, isLoading: _cudaStatusLoading,
refetch: refetchCudaStatus, refetch: refetchCudaStatus,
} = useQuery({ } = useQuery({
queryKey: ['cuda-status', serverUrl], queryKey: ['cuda-status', serverUrl],
@@ -218,35 +218,33 @@ export function GpuAcceleration() {
<CardTitle>GPU Acceleration</CardTitle> <CardTitle>GPU Acceleration</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Current status */} {/* GPU status */}
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium">Backend</div> {health.gpu_available && health.gpu_type ? (
<div className="text-sm text-muted-foreground"> <>
{isCurrentlyCuda <div className="text-sm font-medium">
? 'CUDA (GPU accelerated)' {health.gpu_type.replace(/^(CUDA|ROCm|MPS|Metal|XPU|DirectML)\s*\((.+)\)$/, '$2') ||
: hasNativeGpu health.gpu_type}
? `${health.backend_type === 'mlx' ? 'MLX' : 'PyTorch'} (GPU accelerated)`
: 'CPU'}
</div>
</div>
{/* GPU info from health */}
{health.gpu_type && (
<div className="space-y-1">
<div className="text-sm font-medium">GPU</div>
<div className="text-sm text-muted-foreground">{health.gpu_type}</div>
{health.vram_used_mb != null && (
<div className="text-xs text-muted-foreground">
VRAM: {health.vram_used_mb.toFixed(0)} MB used
</div> </div>
)} <div className="text-sm text-muted-foreground">
</div> {health.gpu_type.replace(/\s*\(.+\)$/, '')}
)} {health.vram_used_mb != null && health.vram_used_mb > 0
? ` \u00b7 ${health.vram_used_mb.toFixed(0)} MB VRAM used`
: ''}
</div>
</>
) : (
<>
<div className="text-sm font-medium">CPU</div>
<div className="text-sm text-muted-foreground">No GPU acceleration available</div>
</>
)}
</div>
{/* Native GPU detected - no CUDA download needed */} {/* Native GPU detected - no CUDA download needed */}
{/* CUDA download section - only show when native GPU is NOT detected (i.e., Windows/Linux NVIDIA users) */} {/* CUDA download section - only show when no GPU is active (native or CUDA) */}
{!hasNativeGpu && ( {!hasNativeGpu && !isCurrentlyCuda && (
<> <>
{/* Download progress */} {/* Download progress */}
{cudaDownloading && downloadProgress && ( {cudaDownloading && downloadProgress && (
@@ -11,6 +11,7 @@ export function UpdateStatus() {
const platform = usePlatform(); const platform = usePlatform();
const { status, checkForUpdates, downloadAndInstall, restartAndInstall } = useAutoUpdater(false); const { status, checkForUpdates, downloadAndInstall, restartAndInstall } = useAutoUpdater(false);
const [currentVersion, setCurrentVersion] = useState<string>(''); const [currentVersion, setCurrentVersion] = useState<string>('');
const isDev = !import.meta.env?.PROD;
useEffect(() => { useEffect(() => {
platform.metadata platform.metadata
@@ -20,11 +21,7 @@ export function UpdateStatus() {
}, [platform]); }, [platform]);
return ( return (
<Card <Card role="region" aria-label="App Updates" tabIndex={0}>
role="region"
aria-label="App Updates"
tabIndex={0}
>
<CardHeader> <CardHeader>
<CardTitle>App Updates</CardTitle> <CardTitle>App Updates</CardTitle>
</CardHeader> </CardHeader>
@@ -32,97 +29,110 @@ export function UpdateStatus() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium">Current Version</div> <div className="text-sm font-medium">Current Version</div>
<div className="text-sm text-muted-foreground">v{currentVersion}</div> <div className="text-sm text-muted-foreground">
v{currentVersion}
{isDev ? ' (dev)' : ''}
</div>
</div> </div>
<Button {!isDev && (
onClick={checkForUpdates} <Button
disabled={status.checking || status.downloading || status.readyToInstall} onClick={checkForUpdates}
variant="outline" disabled={status.checking || status.downloading || status.readyToInstall}
size="sm" variant="outline"
> size="sm"
<RefreshCw className={`h-4 w-4 mr-2 ${status.checking ? 'animate-spin' : ''}`} /> >
Check for Updates <RefreshCw className={`h-4 w-4 mr-2 ${status.checking ? 'animate-spin' : ''}`} />
</Button> Check for Updates
</Button>
)}
</div> </div>
{status.checking && ( {isDev ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
<RefreshCw className="h-4 w-4 animate-spin" /> Auto-updates are disabled in development mode.
Checking for updates...
</div> </div>
)} ) : (
<>
{status.error && ( {status.checking && (
<div className="flex items-center gap-2 text-sm text-destructive"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<AlertCircle className="h-4 w-4" /> <RefreshCw className="h-4 w-4 animate-spin" />
{status.error} Checking for updates...
</div>
)}
{status.available && !status.downloading && !status.readyToInstall && (
<div className="space-y-3 p-4 border rounded-lg bg-primary/5">
<div className="flex items-center justify-between">
<div>
<div className="font-semibold">Update Available</div>
<div className="text-sm text-muted-foreground">Version {status.version}</div>
</div> </div>
<Badge>New</Badge> )}
</div>
<Button onClick={downloadAndInstall} className="w-full" size="sm">
<Download className="h-4 w-4 mr-2" />
Download Update
</Button>
</div>
)}
{status.downloading && ( {status.error && (
<div className="space-y-2"> <div className="flex items-center gap-2 text-sm text-destructive">
<div className="flex items-center justify-between text-sm"> <AlertCircle className="h-4 w-4" />
<div className="flex items-center gap-2"> {status.error}
<Download className="h-4 w-4" />
Downloading update...
</div> </div>
{status.downloadProgress !== undefined && ( )}
<span className="text-muted-foreground">{status.downloadProgress}%</span>
)} {status.available && !status.downloading && !status.readyToInstall && (
</div> <div className="space-y-3 p-4 border rounded-lg bg-primary/5">
<Progress value={status.downloadProgress} /> <div className="flex items-center justify-between">
{status.downloadedBytes !== undefined && <div>
status.totalBytes !== undefined && <div className="font-semibold">Update Available</div>
status.totalBytes > 0 && ( <div className="text-sm text-muted-foreground">Version {status.version}</div>
<div className="text-xs text-muted-foreground"> </div>
{(status.downloadedBytes / 1024 / 1024).toFixed(1)} MB /{' '} <Badge>New</Badge>
{(status.totalBytes / 1024 / 1024).toFixed(1)} MB
</div> </div>
)} <Button onClick={downloadAndInstall} className="w-full" size="sm">
</div> <Download className="h-4 w-4 mr-2" />
)} Download Update
</Button>
</div>
)}
{status.readyToInstall && ( {status.downloading && (
<div className="space-y-3 p-4 border rounded-lg bg-accent/30 border-accent/50"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between text-sm">
<div> <div className="flex items-center gap-2">
<div className="font-semibold">Update Ready to Install</div> <Download className="h-4 w-4" />
Downloading update...
</div>
{status.downloadProgress !== undefined && (
<span className="text-muted-foreground">{status.downloadProgress}%</span>
)}
</div>
<Progress value={status.downloadProgress} />
{status.downloadedBytes !== undefined &&
status.totalBytes !== undefined &&
status.totalBytes > 0 && (
<div className="text-xs text-muted-foreground">
{(status.downloadedBytes / 1024 / 1024).toFixed(1)} MB /{' '}
{(status.totalBytes / 1024 / 1024).toFixed(1)} MB
</div>
)}
</div>
)}
{status.readyToInstall && (
<div className="space-y-3 p-4 border rounded-lg bg-accent/30 border-accent/50">
<div className="flex items-center gap-2">
<div>
<div className="font-semibold">Update Ready to Install</div>
<div className="text-sm text-muted-foreground">
Version {status.version} has been downloaded
</div>
</div>
</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Version {status.version} has been downloaded The app needs to restart to complete the installation. You can do this now or
later at your convenience.
</div> </div>
<Button onClick={restartAndInstall} className="w-full" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
Restart Now
</Button>
</div> </div>
</div> )}
<div className="text-sm text-muted-foreground">
The app needs to restart to complete the installation. You can do this now or later at
your convenience.
</div>
<Button onClick={restartAndInstall} className="w-full" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
Restart Now
</Button>
</div>
)}
{!status.available && !status.checking && !status.error && status.checking === false && ( {!status.available && !status.checking && !status.error && (
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
You're up to date You're up to date
</div> </div>
)}
</>
)} )}
</CardContent> </CardContent>
</Card> </Card>
+5 -6
View File
@@ -1,8 +1,7 @@
const isWindows = navigator.userAgent.includes('Windows');
export function TitleBarDragRegion() { export function TitleBarDragRegion() {
return ( if (isWindows) return null;
<div
data-tauri-drag-region return <div data-tauri-drag-region className="fixed top-0 left-0 right-0 h-12 z-[9999]" />;
className="fixed top-0 left-0 right-0 h-12 z-[9999]"
/>
);
} }
+5 -2
View File
@@ -2,11 +2,14 @@
* UI layout constants for safe area padding * UI layout constants for safe area padding
*/ */
const isWindows = typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows');
/** /**
* Top safe area padding - height of the drag region bar * Top safe area padding - height of the drag region bar
* Corresponds to Tailwind's pt-12 (3rem / 48px) * On macOS this accounts for the overlay titlebar (48px).
* On Windows the native title bar is outside the webview, so no padding is needed.
*/ */
export const TOP_SAFE_AREA_PADDING = 'pt-12'; export const TOP_SAFE_AREA_PADDING = isWindows ? 'pt-8' : 'pt-12';
/** /**
* Bottom safe area padding - height of the audio player * Bottom safe area padding - height of the audio player
+1 -1
View File
@@ -263,7 +263,7 @@ async def health():
gpu_type=gpu_type, gpu_type=gpu_type,
vram_used_mb=vram_used, vram_used_mb=vram_used,
backend_type=backend_type, backend_type=backend_type,
backend_variant=os.environ.get("VOICEBOX_BACKEND_VARIANT", "cpu"), backend_variant=os.environ.get("VOICEBOX_BACKEND_VARIANT", "cuda" if torch.cuda.is_available() else "cpu"),
) )
+47 -24
View File
@@ -66,6 +66,11 @@ setup-python:
} }
Write-Host "Installing Python dependencies..." Write-Host "Installing Python dependencies..."
& "{{ python }}" -m pip install --upgrade pip -q & "{{ python }}" -m pip install --upgrade pip -q
$hasNvidia = $null -ne (Get-WmiObject Win32_VideoController | Where-Object { $_.Name -match 'NVIDIA' })
if ($hasNvidia) { \
Write-Host "NVIDIA GPU detected — installing PyTorch with CUDA support..."; \
& "{{ pip }}" install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126; \
}
& "{{ pip }}" install -r {{ backend_dir }}/requirements.txt & "{{ pip }}" install -r {{ backend_dir }}/requirements.txt
& "{{ pip }}" install --no-deps chatterbox-tts & "{{ pip }}" install --no-deps chatterbox-tts
& "{{ pip }}" install git+https://github.com/QwenLM/Qwen3-TTS.git & "{{ pip }}" install git+https://github.com/QwenLM/Qwen3-TTS.git
@@ -77,30 +82,37 @@ setup-js:
# ─── Development ────────────────────────────────────────────────────── # ─── Development ──────────────────────────────────────────────────────
# Start backend + frontend for development (two processes, one terminal) # Start backend (if not already running) + frontend for development
[unix] [unix]
dev: _ensure-venv _ensure-sidecar dev: _ensure-venv _ensure-sidecar
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
trap 'kill 0' EXIT
echo "Starting backend on http://localhost:17493 ..." backend_pid=""
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 & if curl -sf http://127.0.0.1:17493/health > /dev/null 2>&1; then
sleep 2 echo "Backend already running on http://localhost:17493"
else
echo "Starting backend on http://localhost:17493 ..."
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 &
backend_pid=$!
sleep 2
fi
trap '[ -n "$backend_pid" ] && kill "$backend_pid" 2>/dev/null; wait' EXIT
echo "Starting Tauri desktop app..." echo "Starting Tauri desktop app..."
cd {{ tauri_dir }} && bun run tauri dev & cd {{ tauri_dir }} && bun run tauri dev
wait
[windows] [windows]
dev: _ensure-venv _ensure-sidecar dev: _ensure-venv _ensure-sidecar
Write-Host "Starting backend on http://localhost:17493 ..." $backendJob = $null
$backend = Start-Process -NoNewWindow -PassThru -FilePath "{{ python }}" -ArgumentList "-m", "uvicorn", "backend.main:app", "--reload", "--port", "17493" try { $null = Invoke-WebRequest -Uri "http://127.0.0.1:17493/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; Write-Host "Backend already running on http://localhost:17493" } catch { \
Start-Sleep -Seconds 2 Write-Host "Starting backend on http://localhost:17493 ..."; \
$backendJob = Start-Job -ScriptBlock { & "{{ python }}" -m uvicorn backend.main:app --reload --port 17493 } -WorkingDirectory (Get-Location); \
Start-Sleep -Seconds 2; \
}
Write-Host "Starting Tauri desktop app..." Write-Host "Starting Tauri desktop app..."
$frontend = Start-Process -NoNewWindow -PassThru -WorkingDirectory "{{ tauri_dir }}" -FilePath "bun" -ArgumentList "run", "tauri", "dev" try { Set-Location "{{ tauri_dir }}"; bun run tauri dev } finally { if ($backendJob) { Stop-Job $backendJob -ErrorAction SilentlyContinue; Remove-Job $backendJob -Force -ErrorAction SilentlyContinue } }
try { Wait-Process -Id $backend.Id, $frontend.Id } finally { Stop-Process -Id $backend.Id, $frontend.Id -ErrorAction SilentlyContinue }
# Start backend only # Start backend only
[unix] [unix]
@@ -115,25 +127,36 @@ dev-backend: _ensure-venv
dev-frontend: _ensure-sidecar dev-frontend: _ensure-sidecar
cd {{ tauri_dir }} && bun run tauri dev cd {{ tauri_dir }} && bun run tauri dev
# Start backend + web app (no Tauri) # Start backend (if not already running) + web app (no Tauri)
[unix] [unix]
dev-web: _ensure-venv dev-web: _ensure-venv
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
trap 'kill 0' EXIT
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 & backend_pid=""
sleep 2 if curl -sf http://127.0.0.1:17493/health > /dev/null 2>&1; then
cd {{ web_dir }} && bun run dev & echo "Backend already running on http://localhost:17493"
wait else
echo "Starting backend on http://localhost:17493 ..."
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 &
backend_pid=$!
sleep 2
fi
trap '[ -n "$backend_pid" ] && kill "$backend_pid" 2>/dev/null; wait' EXIT
cd {{ web_dir }} && bun run dev
[windows] [windows]
dev-web: _ensure-venv dev-web: _ensure-venv
Write-Host "Starting backend on http://localhost:17493 ..." $backendJob = $null
$backend = Start-Process -NoNewWindow -PassThru -FilePath "{{ python }}" -ArgumentList "-m", "uvicorn", "backend.main:app", "--reload", "--port", "17493" try { $null = Invoke-WebRequest -Uri "http://127.0.0.1:17493/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; Write-Host "Backend already running on http://localhost:17493" } catch { \
Start-Sleep -Seconds 2 Write-Host "Starting backend on http://localhost:17493 ..."; \
$backendJob = Start-Job -ScriptBlock { & "{{ python }}" -m uvicorn backend.main:app --reload --port 17493 } -WorkingDirectory (Get-Location); \
Start-Sleep -Seconds 2; \
}
Write-Host "Starting web app..." Write-Host "Starting web app..."
$frontend = Start-Process -NoNewWindow -PassThru -WorkingDirectory "{{ web_dir }}" -FilePath "bun" -ArgumentList "run", "dev" try { Set-Location "{{ web_dir }}"; bun run dev } finally { if ($backendJob) { Stop-Job $backendJob -ErrorAction SilentlyContinue; Remove-Job $backendJob -Force -ErrorAction SilentlyContinue } }
try { Wait-Process -Id $backend.Id, $frontend.Id } finally { Stop-Process -Id $backend.Id, $frontend.Id -ErrorAction SilentlyContinue }
# Kill all dev processes # Kill all dev processes
[unix] [unix]