diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte index 73de742b..031f2575 100644 --- a/dashboard/src/routes/+page.svelte +++ b/dashboard/src/routes/+page.svelte @@ -229,7 +229,7 @@ const showOnboarding = $derived(onboardingStep > 0); // Recommended models for onboarding (sorted by fit, then size desc, limited to 6) - const onboardingModels = $derived(() => { + const onboardingModels = $derived.by(() => { if (models.length === 0) return []; return [...models] .filter((m) => getModelMemoryFitStatus(m) !== "too_large") @@ -242,19 +242,30 @@ .slice(0, 6); }); - // Track onboarding instance status for auto-advancing steps + // Track onboarding instance status for auto-advancing steps. + // Handles cached models: if no download is needed, skip step 4 entirely. $effect(() => { if (onboardingStep === 4 && instanceCount > 0) { - // Check if any instance is past downloading let anyDownloading = false; + let anyReady = false; for (const [id, inst] of Object.entries(instanceData)) { const status = getInstanceDownloadStatus(id, inst); if (status.isDownloading) { anyDownloading = true; - break; + } + if ( + status.statusText === "READY" || + status.statusText === "LOADED" || + status.statusText === "RUNNING" + ) { + anyReady = true; } } - if (!anyDownloading) { + // Model already cached & ready — skip download AND loading steps + if (anyReady) { + onboardingStep = 6; + } else if (!anyDownloading) { + // Download finished (or was never needed) but not ready yet onboardingStep = 5; } } @@ -285,8 +296,11 @@ } } + let onboardingError = $state(null); + async function onboardingLaunchModel(modelId: string) { onboardingModelId = modelId; + onboardingError = null; selectPreviewModel(modelId); onboardingStep = 4; // Launch via API @@ -295,10 +309,8 @@ `/instance/placement?model_id=${encodeURIComponent(modelId)}&sharding=${selectedSharding}&instance_meta=${selectedInstanceType}&min_nodes=1`, ); if (!placementResponse.ok) { - console.error( - "Onboarding placement failed:", - await placementResponse.text(), - ); + const errorText = await placementResponse.text(); + onboardingError = `Could not place model: ${errorText}`; onboardingStep = 3; return; } @@ -309,20 +321,21 @@ body: JSON.stringify({ instance: placementData }), }); if (!response.ok) { - console.error("Onboarding launch failed:", await response.text()); + const errorText = await response.text(); + onboardingError = `Failed to launch: ${errorText}`; onboardingStep = 3; return; } setSelectedChatModel(modelId); recordRecentLaunch(modelId); } catch (error) { - console.error("Onboarding launch error:", error); + onboardingError = `Network error: ${error}`; onboardingStep = 3; } } // Helper to get onboarding download progress - const onboardingDownloadProgress = $derived(() => { + const onboardingDownloadProgress = $derived.by(() => { if (instanceCount === 0) return null; for (const [id, inst] of Object.entries(instanceData)) { const status = getInstanceDownloadStatus(id, inst); @@ -2628,7 +2641,16 @@

- {#if onboardingModels().length === 0} + {#if onboardingError} +
+ {onboardingError} +
+ {/if} + + {#if onboardingModels.length === 0}
Loading models... @@ -2636,7 +2658,7 @@
{:else}
- {#each onboardingModels() as model} + {#each onboardingModels as model} {@const sizeGB = getModelSizeGB(model)} {@const fitsNow = hasEnoughMemory(model)} {@const tags = modelTags()[model.id] || []} @@ -2712,28 +2734,24 @@

- {#if onboardingDownloadProgress()} + {#if onboardingDownloadProgress}
+ {onboardingDownloadProgress.percentage.toFixed(1)}% {onboardingDownloadProgress()!.percentage.toFixed(1)}% - {formatBytes(onboardingDownloadProgress()!.downloadedBytes)} / - {formatBytes(onboardingDownloadProgress()!.totalBytes)}{formatBytes(onboardingDownloadProgress.downloadedBytes)} / + {formatBytes(onboardingDownloadProgress.totalBytes)}
- {formatSpeed(onboardingDownloadProgress()!.speed)} - ETA: {formatEta(onboardingDownloadProgress()!.etaMs)} + {formatSpeed(onboardingDownloadProgress.speed)} + ETA: {formatEta(onboardingDownloadProgress.etaMs)}
{:else} @@ -3373,11 +3391,9 @@ > {getInstanceModelId(instance)}
- {#if debugEnabled} -
- {instanceInfo.sharding} · {instanceInfo.instanceType} -
- {/if} +
+ {instanceInfo.sharding} · {instanceInfo.instanceType} +
{#if instanceModelId && instanceModelId !== "Unknown" && instanceModelId !== "Unknown Model"} {getInstanceModelId(instance)} - {#if debugEnabled} -
- {instanceInfo.sharding} · {instanceInfo.instanceType} -
- {/if} +
+ {instanceInfo.sharding} · {instanceInfo.instanceType} +
{#if instanceModelId && instanceModelId !== "Unknown" && instanceModelId !== "Unknown Model"}
None: print(banner, file=sys.stderr) if first_run: - try: - webbrowser.open(dashboard_url) - logger.info("First run detected — opening dashboard in browser") - except Exception: - logger.debug("Could not auto-open browser", exc_info=True) + # Skip browser open when running inside the native macOS app — + # FirstLaunchPopout.swift handles the auto-open with a countdown. + if not os.environ.get("EXO_RUNTIME_DIR"): + try: + webbrowser.open(dashboard_url) + logger.info("First run detected — opening dashboard in browser") + except Exception: + logger.debug("Could not auto-open browser", exc_info=True) _mark_first_run_done()