[object Object]

← back to Exo

feat: better onboarding UX for new users (#1479)

490d2e46ba469834215f318a358b37deb5e0e9a5 · 2026-02-17 09:52:49 -0800 · Alex Cheema

## Summary

- **Auto-open dashboard** in browser on first launch (uses
`~/.exo/.dashboard_opened` marker)
- **Welcome overlay** with "Choose a Model" CTA button when no model
instance is running
- **Tutorial progress messages** during model download → loading → ready
lifecycle stages
- **Fix conversation sidebar** text contrast — bumped to white text,
added active state background
- **Simplify technical jargon** — sharding/instance type/min nodes
hidden behind collapsible "Advanced Options" toggle; strategy display
hidden behind debug mode
- **Polished DMG installer** with drag-to-Applications layout, custom
branded background, and AppleScript-configured window positioning

## Test plan

- [ ] Launch exo for the first time (delete `~/.exo/.dashboard_opened`
to simulate) — browser should auto-open
- [ ] Verify welcome overlay appears on topology when no model is loaded
- [ ] Launch a model and verify download/loading/ready messages appear
in instance cards
- [ ] Check conversation sidebar text is readable (white on dark, yellow
when active)
- [ ] Verify "Advanced Options" toggle hides/shows sharding controls
- [ ] Build DMG with `packaging/dmg/create-dmg.sh` and verify
drag-to-Applications layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 490d2e46ba469834215f318a358b37deb5e0e9a5
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date:   Tue Feb 17 09:52:49 2026 -0800

    feat: better onboarding UX for new users (#1479)
    
    ## Summary
    
    - **Auto-open dashboard** in browser on first launch (uses
    `~/.exo/.dashboard_opened` marker)
    - **Welcome overlay** with "Choose a Model" CTA button when no model
    instance is running
    - **Tutorial progress messages** during model download → loading → ready
    lifecycle stages
    - **Fix conversation sidebar** text contrast — bumped to white text,
    added active state background
    - **Simplify technical jargon** — sharding/instance type/min nodes
    hidden behind collapsible "Advanced Options" toggle; strategy display
    hidden behind debug mode
    - **Polished DMG installer** with drag-to-Applications layout, custom
    branded background, and AppleScript-configured window positioning
    
    ## Test plan
    
    - [ ] Launch exo for the first time (delete `~/.exo/.dashboard_opened`
    to simulate) — browser should auto-open
    - [ ] Verify welcome overlay appears on topology when no model is loaded
    - [ ] Launch a model and verify download/loading/ready messages appear
    in instance cards
    - [ ] Check conversation sidebar text is readable (white on dark, yellow
    when active)
    - [ ] Verify "Advanced Options" toggle hides/shows sharding controls
    - [ ] Build DMG with `packaging/dmg/create-dmg.sh` and verify
    drag-to-Applications layout
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
---
 .github/workflows/build-app.yml                    |    5 +-
 app/EXO/EXO/ContentView.swift                      |  476 +--
 app/EXO/EXO/EXOApp.swift                           |   16 +-
 app/EXO/EXO/ExoProcessController.swift             |   20 +
 app/EXO/EXO/Views/FirstLaunchPopout.swift          |  192 +
 app/EXO/EXO/Views/SettingsView.swift               |  478 +++
 app/EXO/EXO/Views/SettingsWindowController.swift   |   47 +
 dashboard/src/app.css                              |   37 +
 dashboard/src/lib/components/ChatForm.svelte       |    4 +
 dashboard/src/lib/components/ChatMessages.svelte   |    5 +-
 dashboard/src/lib/components/ChatSidebar.svelte    |   40 +-
 .../src/lib/components/ConnectionBanner.svelte     |   20 +
 dashboard/src/lib/components/HeaderNav.svelte      |   77 +-
 dashboard/src/lib/components/ModelCard.svelte      |   26 +
 .../src/lib/components/ModelPickerModal.svelte     |   67 +-
 dashboard/src/lib/components/ToastContainer.svelte |  117 +
 dashboard/src/lib/stores/app.svelte.ts             |   25 +-
 dashboard/src/lib/stores/toast.svelte.ts           |   87 +
 dashboard/src/routes/+layout.svelte                |    4 +
 dashboard/src/routes/+page.svelte                  | 4380 +++++++++++++-------
 packaging/dmg/background.png                       |  Bin 0 -> 7010 bytes
 packaging/dmg/create-dmg.sh                        |  112 +
 packaging/dmg/generate-background.py               |   91 +
 src/exo/utils/banner.py                            |   30 +
 24 files changed, 4370 insertions(+), 1986 deletions(-)

diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml
index d20bfc12..861f4697 100644
--- a/.github/workflows/build-app.yml
+++ b/.github/workflows/build-app.yml
@@ -303,11 +303,8 @@ jobs:
           SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
           /usr/bin/codesign --deep --force --timestamp --options runtime \
             --sign "$SIGNING_IDENTITY" EXO.app
-          mkdir -p dmg-root
-          cp -R EXO.app dmg-root/
-          ln -s /Applications dmg-root/Applications
           DMG_NAME="EXO-${RELEASE_VERSION}.dmg"
-          hdiutil create -volname "EXO" -srcfolder dmg-root -ov -format UDZO "$DMG_NAME"
+          bash "$GITHUB_WORKSPACE/packaging/dmg/create-dmg.sh" EXO.app "$DMG_NAME" "EXO"
           /usr/bin/codesign --force --timestamp --options runtime \
             --sign "$SIGNING_IDENTITY" "$DMG_NAME"
           if [[ -n "$APPLE_NOTARIZATION_USERNAME" ]]; then
diff --git a/app/EXO/EXO/ContentView.swift b/app/EXO/EXO/ContentView.swift
index 688fba5b..ca09fe2f 100644
--- a/app/EXO/EXO/ContentView.swift
+++ b/app/EXO/EXO/ContentView.swift
@@ -15,18 +15,12 @@ struct ContentView: View {
     @EnvironmentObject private var localNetworkChecker: LocalNetworkChecker
     @EnvironmentObject private var updater: SparkleUpdater
     @EnvironmentObject private var thunderboltBridgeService: ThunderboltBridgeService
+    @EnvironmentObject private var settingsWindowController: SettingsWindowController
     @State private var focusedNode: NodeViewModel?
     @State private var deletingInstanceIDs: Set<String> = []
     @State private var showAllNodes = false
     @State private var showAllInstances = false
-    @State private var showAdvanced = false
-    @State private var showDebugInfo = false
-    @State private var bugReportInFlight = false
-    @State private var bugReportMessage: String?
-    @State private var uninstallInProgress = false
-    @State private var pendingNamespace: String = ""
-    @State private var pendingHFToken: String = ""
-    @State private var pendingEnableImageModels = false
+    @State private var baseURLCopied = false
 
     var body: some View {
         VStack(alignment: .leading, spacing: 12) {
@@ -258,139 +252,79 @@ struct ContentView: View {
         VStack(alignment: .leading, spacing: 0) {
             if controller.status != .stopped {
                 dashboardButton
+                baseURLRow
                 Divider()
                     .padding(.vertical, 8)
             } else {
                 Divider()
                     .padding(.vertical, 4)
             }
-            advancedSection
-                .padding(.bottom, 8)
-            controlButton(title: "Quit", tint: .secondary) {
-                controller.stop()
-                NSApplication.shared.terminate(nil)
+            HoverButton(
+                title: "Settings",
+                tint: .primary,
+                trailingSystemImage: "gear"
+            ) {
+                settingsWindowController.open(
+                    controller: controller,
+                    updater: updater,
+                    networkStatusService: networkStatusService,
+                    thunderboltBridgeService: thunderboltBridgeService,
+                    stateService: stateService
+                )
             }
-        }
-    }
-
-    private var advancedSection: some View {
-        VStack(alignment: .leading, spacing: 6) {
-            HStack {
-                Text("Advanced")
-                    .font(.caption)
-                    .foregroundColor(.secondary)
-                Spacer()
-                collapseButton(isExpanded: $showAdvanced)
+            HoverButton(
+                title: "Check for Updates",
+                tint: .primary,
+                trailingSystemImage: "arrow.triangle.2.circlepath"
+            ) {
+                updater.checkForUpdates()
             }
-            .animation(nil, value: showAdvanced)
-            if showAdvanced {
-                VStack(alignment: .leading, spacing: 8) {
-                    VStack(alignment: .leading, spacing: 4) {
-                        Text("Cluster Namespace")
-                            .font(.caption2)
-                            .foregroundColor(.secondary)
-                        HStack {
-                            TextField("optional", text: $pendingNamespace)
-                                .textFieldStyle(.roundedBorder)
-                                .font(.caption2)
-                                .onAppear {
-                                    pendingNamespace = controller.customNamespace
-                                }
-                            Button("Save & Restart") {
-                                controller.customNamespace = pendingNamespace
-                                if controller.status == .running || controller.status == .starting {
-                                    controller.restart()
-                                }
-                            }
-                            .font(.caption2)
-                            .disabled(pendingNamespace == controller.customNamespace)
-                        }
-                    }
-                    VStack(alignment: .leading, spacing: 4) {
-                        Text("HuggingFace Token")
-                            .font(.caption2)
-                            .foregroundColor(.secondary)
-                        HStack {
-                            SecureField("optional", text: $pendingHFToken)
-                                .textFieldStyle(.roundedBorder)
-                                .font(.caption2)
-                                .onAppear {
-                                    pendingHFToken = controller.hfToken
-                                }
-                            Button("Save & Restart") {
-                                controller.hfToken = pendingHFToken
-                                if controller.status == .running || controller.status == .starting {
-                                    controller.restart()
-                                }
-                            }
-                            .font(.caption2)
-                            .disabled(pendingHFToken == controller.hfToken)
-                        }
-                    }
-                    Divider()
-                    HStack {
-                        Toggle(
-                            "Enable Image Models (experimental)", isOn: $pendingEnableImageModels
-                        )
-                        .toggleStyle(.switch)
-                        .font(.caption2)
-                        .onAppear {
-                            pendingEnableImageModels = controller.enableImageModels
-                        }
-
-                        Spacer()
-
-                        Button("Save & Restart") {
-                            controller.enableImageModels = pendingEnableImageModels
-                            if controller.status == .running || controller.status == .starting {
-                                controller.restart()
-                            }
-                        }
-                        .font(.caption2)
-                        .disabled(pendingEnableImageModels == controller.enableImageModels)
-                    }
-                    HoverButton(title: "Check for Updates", small: true) {
-                        updater.checkForUpdates()
-                    }
-                    debugSection
-                    HoverButton(title: "Uninstall", tint: .red, small: true) {
-                        showUninstallConfirmationAlert()
-                    }
-                    .disabled(uninstallInProgress)
-                }
-                .transition(.opacity)
+            .padding(.bottom, 8)
+            HoverButton(title: "Quit", tint: .secondary) {
+                controller.stop()
+                NSApplication.shared.terminate(nil)
             }
         }
-        .animation(.easeInOut(duration: 0.25), value: showAdvanced)
-    }
-
-    private func controlButton(title: String, tint: Color = .primary, action: @escaping () -> Void)
-        -> some View
-    {
-        HoverButton(title: title, tint: tint, trailingSystemImage: nil, action: action)
     }
 
     private var dashboardButton: some View {
-        Button {
+        HoverButton(
+            title: "Web Dashboard",
+            tint: .primary,
+            trailingSystemImage: "arrow.up.right"
+        ) {
             guard let url = URL(string: "http://localhost:52415/") else { return }
             NSWorkspace.shared.open(url)
-        } label: {
-            HStack {
-                Image(systemName: "arrow.up.right.square")
+        }
+    }
+
+    private var baseURLRow: some View {
+        HStack(spacing: 6) {
+            Image(systemName: "link")
+                .imageScale(.small)
+                .foregroundColor(.secondary)
+            Text("localhost:52415/v1")
+                .font(.system(.caption, design: .monospaced))
+                .foregroundColor(.primary)
+            Spacer()
+            Button {
+                NSPasteboard.general.clearContents()
+                NSPasteboard.general.setString("http://localhost:52415/v1", forType: .string)
+                baseURLCopied = true
+                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                    baseURLCopied = false
+                }
+            } label: {
+                Image(systemName: baseURLCopied ? "checkmark" : "doc.on.doc")
                     .imageScale(.small)
-                Text("Dashboard")
-                    .fontWeight(.medium)
-                Spacer()
+                    .foregroundColor(baseURLCopied ? .green : .secondary)
+                    .contentTransition(.symbolEffect(.replace))
             }
-            .padding(.vertical, 8)
-            .padding(.horizontal, 10)
-            .background(
-                RoundedRectangle(cornerRadius: 8, style: .continuous)
-                    .fill(Color(red: 1.0, green: 0.87, blue: 0.0).opacity(0.2))
-            )
+            .buttonStyle(.plain)
+            .help("Copy API base URL")
         }
-        .buttonStyle(.plain)
-        .padding(.bottom, 4)
+        .padding(.vertical, 4)
+        .padding(.horizontal, 8)
     }
 
     private func collapseButton(isExpanded: Binding<Bool>) -> some View {
@@ -445,207 +379,6 @@ struct ContentView: View {
         }
     }
 
-    private var thunderboltStatusText: String {
-        switch networkStatusService.status.thunderboltBridgeState {
-        case .some(.disabled):
-            return "Thunderbolt Bridge: Disabled"
-        case .some(.deleted):
-            return "Thunderbolt Bridge: Deleted"
-        case .some(.enabled):
-            return "Thunderbolt Bridge: Enabled"
-        case nil:
-            return "Thunderbolt Bridge: Unknown"
-        }
-    }
-
-    private var thunderboltStatusColor: Color {
-        switch networkStatusService.status.thunderboltBridgeState {
-        case .some(.disabled), .some(.deleted):
-            return .green
-        case .some(.enabled):
-            return .red
-        case nil:
-            return .secondary
-        }
-    }
-
-    /// Shows TB bridge status for all nodes from exo cluster state
-    private var clusterThunderboltBridgeView: some View {
-        let bridgeStatuses = stateService.latestSnapshot?.nodeThunderboltBridge ?? [:]
-        let localNodeId = stateService.localNodeId
-        let nodeProfiles = stateService.latestSnapshot?.nodeProfiles ?? [:]
-
-        return VStack(alignment: .leading, spacing: 1) {
-            if bridgeStatuses.isEmpty {
-                Text("Cluster TB Bridge: No data")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-            } else {
-                Text("Cluster TB Bridge Status:")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-                ForEach(Array(bridgeStatuses.keys.sorted()), id: \.self) { nodeId in
-                    if let status = bridgeStatuses[nodeId] {
-                        let nodeName =
-                            nodeProfiles[nodeId]?.friendlyName ?? String(nodeId.prefix(8))
-                        let isLocal = nodeId == localNodeId
-                        let prefix = isLocal ? "  \(nodeName) (local):" : "  \(nodeName):"
-                        let statusText =
-                            !status.exists
-                            ? "N/A"
-                            : (status.enabled ? "Enabled" : "Disabled")
-                        let color: Color =
-                            !status.exists
-                            ? .secondary
-                            : (status.enabled ? .red : .green)
-                        Text("\(prefix) \(statusText)")
-                            .font(.caption2)
-                            .foregroundColor(color)
-                    }
-                }
-            }
-        }
-    }
-
-    private var interfaceIpList: some View {
-        let statuses = networkStatusService.status.interfaceStatuses
-        return VStack(alignment: .leading, spacing: 1) {
-            Text("Interfaces (en0–en7):")
-                .font(.caption2)
-                .foregroundColor(.secondary)
-            if statuses.isEmpty {
-                Text("  Unknown")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-            } else {
-                ForEach(statuses, id: \.interfaceName) { status in
-                    let ipText = status.ipAddress ?? "No IP"
-                    Text("  \(status.interfaceName): \(ipText)")
-                        .font(.caption2)
-                        .foregroundColor(status.ipAddress == nil ? .red : .green)
-                }
-            }
-        }
-    }
-
-    private var debugSection: some View {
-        VStack(alignment: .leading, spacing: 4) {
-            HoverButton(
-                title: "Debug Info",
-                tint: .primary,
-                trailingSystemImage: showDebugInfo ? "chevron.up" : "chevron.down",
-                small: true
-            ) {
-                showDebugInfo.toggle()
-            }
-            if showDebugInfo {
-                VStack(alignment: .leading, spacing: 4) {
-                    Text("Version: \(buildTag)")
-                        .font(.caption2)
-                        .foregroundColor(.secondary)
-                    Text("Commit: \(buildCommit)")
-                        .font(.caption2)
-                        .foregroundColor(.secondary)
-                    Text(thunderboltStatusText)
-                        .font(.caption2)
-                        .foregroundColor(thunderboltStatusColor)
-                    clusterThunderboltBridgeView
-                    interfaceIpList
-                    rdmaStatusView
-                    sendBugReportButton
-                        .padding(.top, 6)
-                }
-                .padding(.leading, 8)
-                .transition(.opacity)
-            }
-        }
-        .animation(.easeInOut(duration: 0.25), value: showDebugInfo)
-    }
-
-    private var rdmaStatusView: some View {
-        let rdmaStatuses = stateService.latestSnapshot?.nodeRdmaCtl ?? [:]
-        let localNodeId = stateService.localNodeId
-        let nodeProfiles = stateService.latestSnapshot?.nodeProfiles ?? [:]
-        let localDevices = networkStatusService.status.localRdmaDevices
-        let localPorts = networkStatusService.status.localRdmaActivePorts
-
-        return VStack(alignment: .leading, spacing: 1) {
-            if rdmaStatuses.isEmpty {
-                Text("Cluster RDMA: No data")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-            } else {
-                Text("Cluster RDMA Status:")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-                ForEach(Array(rdmaStatuses.keys.sorted()), id: \.self) { nodeId in
-                    if let status = rdmaStatuses[nodeId] {
-                        let nodeName =
-                            nodeProfiles[nodeId]?.friendlyName ?? String(nodeId.prefix(8))
-                        let isLocal = nodeId == localNodeId
-                        let prefix = isLocal ? "  \(nodeName) (local):" : "  \(nodeName):"
-                        let statusText = status.enabled ? "Enabled" : "Disabled"
-                        let color: Color = status.enabled ? .green : .orange
-                        Text("\(prefix) \(statusText)")
-                            .font(.caption2)
-                            .foregroundColor(color)
-                    }
-                }
-            }
-            if !localDevices.isEmpty {
-                Text("  Local Devices: \(localDevices.joined(separator: ", "))")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-            }
-            if !localPorts.isEmpty {
-                Text("  Local Active Ports:")
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-                ForEach(localPorts, id: \.device) { port in
-                    Text("    \(port.device) port \(port.port): \(port.state)")
-                        .font(.caption2)
-                        .foregroundColor(.green)
-                }
-            }
-        }
-    }
-
-    private var sendBugReportButton: some View {
-        VStack(alignment: .leading, spacing: 4) {
-            Button {
-                Task {
-                    await sendBugReport()
-                }
-            } label: {
-                HStack {
-                    if bugReportInFlight {
-                        ProgressView()
-                            .scaleEffect(0.6)
-                    }
-                    Text("Send Bug Report")
-                        .font(.caption)
-                        .fontWeight(.semibold)
-                    Spacer()
-                }
-                .padding(.vertical, 6)
-                .padding(.horizontal, 8)
-                .background(
-                    RoundedRectangle(cornerRadius: 6)
-                        .fill(Color.accentColor.opacity(0.12))
-                )
-            }
-            .buttonStyle(.plain)
-            .disabled(bugReportInFlight)
-
-            if let message = bugReportMessage {
-                Text(message)
-                    .font(.caption2)
-                    .foregroundColor(.secondary)
-                    .fixedSize(horizontal: false, vertical: true)
-            }
-        }
-    }
-
     private var processToggleBinding: Binding<Bool> {
         Binding(
             get: {
@@ -686,101 +419,6 @@ struct ContentView: View {
         )
     }
 
-    private func sendBugReport() async {
-        bugReportInFlight = true
-        bugReportMessage = "Collecting logs..."
-        let service = BugReportService()
-        do {
-            let outcome = try await service.sendReport(isManual: true)
-            bugReportMessage = outcome.message
-        } catch {
-            bugReportMessage = error.localizedDescription
-        }
-        bugReportInFlight = false
-    }
-
-    private func showUninstallConfirmationAlert() {
-        let alert = NSAlert()
-        alert.messageText = "Uninstall EXO"
-        alert.informativeText = """
-            This will remove EXO and all its system components:
-
-            • Network configuration daemon
-            • Launch at login registration
-            • EXO network location
-
-            The app will be moved to Trash.
-            """
-        alert.alertStyle = .warning
-        alert.addButton(withTitle: "Uninstall")
-        alert.addButton(withTitle: "Cancel")
-
-        // Style the Uninstall button as destructive
-        if let uninstallButton = alert.buttons.first {
-            uninstallButton.hasDestructiveAction = true
-        }
-
-        let response = alert.runModal()
-        if response == .alertFirstButtonReturn {
-            performUninstall()
-        }
-    }
-
-    private func performUninstall() {
-        uninstallInProgress = true
-
-        // Stop EXO process first
-        controller.cancelPendingLaunch()
-        controller.stop()
-        stateService.stopPolling()
-
-        // Run the privileged uninstall on a background thread
-        // Using .utility QoS to avoid priority inversion with NSAppleScript's subprocess
-        DispatchQueue.global(qos: .utility).async {
-            do {
-                // Remove network setup daemon and components (requires admin privileges)
-                try NetworkSetupHelper.uninstall()
-
-                DispatchQueue.main.async {
-                    // Unregister from launch at login
-                    LaunchAtLoginHelper.disable()
-
-                    // Move app to trash
-                    self.moveAppToTrash()
-
-                    // Quit the app
-                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
-                        NSApplication.shared.terminate(nil)
-                    }
-                }
-            } catch {
-                DispatchQueue.main.async {
-                    self.showErrorAlert(message: error.localizedDescription)
-                    self.uninstallInProgress = false
-                }
-            }
-        }
-    }
-
-    private func showErrorAlert(message: String) {
-        let alert = NSAlert()
-        alert.messageText = "Uninstall Failed"
-        alert.informativeText = message
-        alert.alertStyle = .critical
-        alert.addButton(withTitle: "OK")
-        alert.runModal()
-    }
-
-    private func moveAppToTrash() {
-        guard let appURL = Bundle.main.bundleURL as URL? else { return }
-        do {
-            try FileManager.default.trashItem(at: appURL, resultingItemURL: nil)
-        } catch {
-            // If we can't trash the app, that's OK - user can do it manually
-            // The important system components have already been cleaned up
-        }
-    }
-
     private var buildTag: String {
         Bundle.main.infoDictionary?["EXOBuildTag"] as? String ?? "unknown"
     }
diff --git a/app/EXO/EXO/EXOApp.swift b/app/EXO/EXO/EXOApp.swift
index 3aff58c3..bba26e24 100644
--- a/app/EXO/EXO/EXOApp.swift
+++ b/app/EXO/EXO/EXOApp.swift
@@ -21,7 +21,9 @@ struct EXOApp: App {
     @StateObject private var localNetworkChecker: LocalNetworkChecker
     @StateObject private var updater: SparkleUpdater
     @StateObject private var thunderboltBridgeService: ThunderboltBridgeService
+    @StateObject private var settingsWindowController: SettingsWindowController
     private let terminationObserver: TerminationObserver
+    private let firstLaunchPopout = FirstLaunchPopout()
     private let ciContext = CIContext(options: nil)
 
     init() {
@@ -43,12 +45,13 @@ struct EXOApp: App {
         _updater = StateObject(wrappedValue: updater)
         let thunderboltBridge = ThunderboltBridgeService(clusterStateService: service)
         _thunderboltBridgeService = StateObject(wrappedValue: thunderboltBridge)
+        _settingsWindowController = StateObject(wrappedValue: SettingsWindowController())
         enableLaunchAtLoginIfNeeded()
         // Install LaunchDaemon to disable Thunderbolt Bridge on startup (prevents network loops)
         NetworkSetupHelper.promptAndInstallIfNeeded()
         // Check local network access periodically (warning disappears when user grants permission)
         localNetwork.startPeriodicChecking(interval: 10)
-        controller.scheduleLaunch(after: 15)
+        controller.scheduleLaunch(after: 5)
         service.startPolling()
         networkStatus.startPolling()
     }
@@ -62,8 +65,19 @@ struct EXOApp: App {
                 .environmentObject(localNetworkChecker)
                 .environmentObject(updater)
                 .environmentObject(thunderboltBridgeService)
+                .environmentObject(settingsWindowController)
         } label: {
             menuBarIcon
+                .onReceive(controller.$isFirstLaunchReady) { ready in
+                    if ready {
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+                            self.firstLaunchPopout.onComplete = { [weak controller] in
+                                controller?.markOnboardingCompleted()
+                            }
+                            self.firstLaunchPopout.show()
+                        }
+                    }
+                }
         }
         .menuBarExtraStyle(.window)
     }
diff --git a/app/EXO/EXO/ExoProcessController.swift b/app/EXO/EXO/ExoProcessController.swift
index 7566674b..2350c118 100644
--- a/app/EXO/EXO/ExoProcessController.swift
+++ b/app/EXO/EXO/ExoProcessController.swift
@@ -5,6 +5,7 @@ import Foundation
 private let customNamespaceKey = "EXOCustomNamespace"
 private let hfTokenKey = "EXOHFToken"
 private let enableImageModelsKey = "EXOEnableImageModels"
+private let onboardingCompletedKey = "EXOOnboardingCompleted"
 
 @MainActor
 final class ExoProcessController: ObservableObject {
@@ -60,6 +61,9 @@ final class ExoProcessController: ObservableObject {
         }
     }
 
+    /// Fires once when EXO transitions to `.running` for the very first time (fresh install).
+    @Published private(set) var isFirstLaunchReady = false
+
     private var process: Process?
     private var runtimeDirectoryURL: URL?
     private var pendingLaunchTask: Task<Void, Never>?
@@ -113,6 +117,11 @@ final class ExoProcessController: ObservableObject {
             try child.run()
             process = child
             status = .running
+
+            // Show welcome popout if onboarding was never completed
+            if !UserDefaults.standard.bool(forKey: onboardingCompletedKey) {
+                isFirstLaunchReady = true
+            }
         } catch {
             process = nil
             status = .failed(message: "Launch error")
@@ -164,6 +173,17 @@ final class ExoProcessController: ObservableObject {
         launch()
     }
 
+    /// Mark onboarding as completed (user interacted with the welcome popout).
+    func markOnboardingCompleted() {
+        UserDefaults.standard.set(true, forKey: onboardingCompletedKey)
+    }
+
+    /// Reset onboarding so the welcome popout appears on next launch.
+    func resetOnboarding() {
+        UserDefaults.standard.removeObject(forKey: onboardingCompletedKey)
+        isFirstLaunchReady = false
+    }
+
     func scheduleLaunch(after seconds: TimeInterval) {
         cancelPendingLaunch()
         let start = max(1, Int(ceil(seconds)))
diff --git a/app/EXO/EXO/Views/FirstLaunchPopout.swift b/app/EXO/EXO/Views/FirstLaunchPopout.swift
new file mode 100644
index 00000000..1a10b3ac
--- /dev/null
+++ b/app/EXO/EXO/Views/FirstLaunchPopout.swift
@@ -0,0 +1,192 @@
+import AppKit
+import SwiftUI
+
+/// A popover callout anchored to the menu bar icon on first launch,
+/// pointing the user to the web dashboard with an arrow connecting to the icon.
+@MainActor
+final class FirstLaunchPopout {
+    private var popover: NSPopover?
+    private var countdownTask: Task<Void, Never>?
+    private static let dashboardURL = "http://localhost:52415/"
+
+    /// Called when the user completes onboarding (clicks Open Dashboard or dismisses).
+    var onComplete: (() -> Void)?
+
+    func show() {
+        guard popover == nil else { return }
+
+        // The status bar button may not exist yet on first launch; retry a few times.
+        showWithRetry(attemptsRemaining: 5)
+    }
+
+    private func showWithRetry(attemptsRemaining: Int) {
+        guard attemptsRemaining > 0 else { return }
+
+        guard let button = Self.findStatusItemButton() else {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+                self?.showWithRetry(attemptsRemaining: attemptsRemaining - 1)
+            }
+            return
+        }
+
+        let pop = NSPopover()
+        pop.behavior = .applicationDefined
+        pop.animates = true
+        pop.contentSize = NSSize(width: 280, height: 120)
+        pop.contentViewController = NSHostingController(
+            rootView: WelcomeCalloutView(
+                countdownDuration: 30,
+                onDismiss: { [weak self] in
+                    self?.onComplete?()
+                    self?.dismiss()
+                },
+                onOpen: { [weak self] in
+                    self?.openDashboard()
+                    self?.onComplete?()
+                    self?.dismiss()
+                }
+            )
+        )
+
+        self.popover = pop
+        pop.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
+
+        // Auto-open dashboard after 30s then dismiss
+        countdownTask = Task {
+            try? await Task.sleep(nanoseconds: 30_000_000_000)
+            if !Task.isCancelled {
+                openDashboard()
+                onComplete?()
+                dismiss()
+            }
+        }
+    }
+
+    func dismiss() {
+        countdownTask?.cancel()
+        countdownTask = nil
+        UserDefaults.standard.set(true, forKey: "EXOOnboardingCompleted")
+        guard let pop = popover else { return }
+        popover = nil
+        pop.performClose(nil)
+    }
+
+    private func openDashboard() {
+        guard let url = URL(string: Self.dashboardURL) else { return }
+        NSWorkspace.shared.open(url)
+    }
+
+    /// Finds the NSStatusBarButton created by SwiftUI's MenuBarExtra.
+    /// Walks the view hierarchy to find the actual button rather than the content view.
+    private static func findStatusItemButton() -> NSView? {
+        for window in NSApp.windows {
+            let className = NSStringFromClass(type(of: window))
+            if className.contains("NSStatusBarWindow") {
+                // Try to find the actual status bar button in the view hierarchy
+                if let content = window.contentView {
+                    if let button = findButton(in: content) {
+                        return button
+                    }
+                    return content
+                }
+            }
+        }
+        return nil
+    }
+
+    /// Recursively searches the view hierarchy for an NSStatusBarButton.
+    private static func findButton(in view: NSView) -> NSView? {
+        let className = NSStringFromClass(type(of: view))
+        if className.contains("StatusBarButton") {
+            return view
+        }
+        for subview in view.subviews {
+            if let found = findButton(in: subview) {
+                return found
+            }
+        }
+        return nil
+    }
+}
+
+/// Minimal welcome callout — friendly pointer, not a wall of text.
+/// Rendered inside the NSPopover which provides its own chrome and arrow.
+private struct WelcomeCalloutView: View {
+    let countdownDuration: Int
+    let onDismiss: () -> Void
+    let onOpen: () -> Void
+    @State private var countdown: Int
+    @State private var timerTask: Task<Void, Never>?
+
+    init(countdownDuration: Int, onDismiss: @escaping () -> Void, onOpen: @escaping () -> Void) {
+        self.countdownDuration = countdownDuration
+        self.onDismiss = onDismiss
+        self.onOpen = onOpen
+        self._countdown = State(initialValue: countdownDuration)
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            HStack(alignment: .top) {
+                Text("Welcome to EXO!")
+                    .font(.system(.headline, design: .rounded))
+                    .fontWeight(.semibold)
+                    .foregroundColor(.primary)
+                Spacer()
+                Button {
+                    onDismiss()
+                } label: {
+                    Image(systemName: "xmark.circle.fill")
+                        .font(.system(size: 14))
+                        .foregroundStyle(.tertiary)
+                }
+                .buttonStyle(.plain)
+            }
+
+            Text("Run your first model here:")
+                .font(.system(.subheadline, design: .default))
+                .foregroundColor(.secondary)
+
+            HStack {
+                Button {
+                    onOpen()
+                } label: {
+                    Label("Open Dashboard", systemImage: "arrow.up.right.square")
+                        .font(.system(.caption, design: .default))
+                        .fontWeight(.medium)
+                }
+                .buttonStyle(.borderedProminent)
+                .tint(.accentColor)
+                .controlSize(.small)
+
+                Spacer()
+
+                if countdown > 0 {
+                    Text("Auto-opens in \(countdown)s")
+                        .font(.system(.caption2, design: .default))
+                        .foregroundColor(.secondary.opacity(0.6))
+                        .monospacedDigit()
+                }
+            }
+        }
+        .padding(14)
+        .onAppear {
+            startCountdown()
+        }
+        .onDisappear {
+            timerTask?.cancel()
+            timerTask = nil
+        }
+    }
+
+    private func startCountdown() {
+        timerTask = Task {
+            while countdown > 0 {
+                try? await Task.sleep(nanoseconds: 1_000_000_000)
+                if !Task.isCancelled {
+                    countdown -= 1
+                }
+            }
+        }
+    }
+}
diff --git a/app/EXO/EXO/Views/SettingsView.swift b/app/EXO/EXO/Views/SettingsView.swift
new file mode 100644
index 00000000..ed221c89
--- /dev/null
+++ b/app/EXO/EXO/Views/SettingsView.swift
@@ -0,0 +1,478 @@
+import AppKit
+import SwiftUI
+
+/// Native macOS Settings window following Apple HIG.
+/// Organized into General, Model, Advanced, and About sections.
+struct SettingsView: View {
+    @EnvironmentObject private var controller: ExoProcessController
+    @EnvironmentObject private var updater: SparkleUpdater
+    @EnvironmentObject private var networkStatusService: NetworkStatusService
+    @EnvironmentObject private var thunderboltBridgeService: ThunderboltBridgeService
+    @EnvironmentObject private var stateService: ClusterStateService
+
+    @State private var pendingNamespace: String = ""
+    @State private var pendingHFToken: String = ""
+    @State private var pendingEnableImageModels = false
+    @State private var needsRestart = false
+    @State private var bugReportInFlight = false
+    @State private var bugReportMessage: String?
+    @State private var uninstallInProgress = false
+
+    var body: some View {
+        TabView {
+            generalTab
+                .tabItem {
+                    Label("General", systemImage: "gear")
+                }
+            modelTab
+                .tabItem {
+                    Label("Model", systemImage: "cube")
+                }
+            advancedTab
+                .tabItem {
+                    Label("Advanced", systemImage: "wrench.and.screwdriver")
+                }
+            aboutTab
+                .tabItem {
+                    Label("About", systemImage: "info.circle")
+                }
+        }
+        .frame(width: 450, height: 400)
+        .onAppear {
+            pendingNamespace = controller.customNamespace
+            pendingHFToken = controller.hfToken
+            pendingEnableImageModels = controller.enableImageModels
+            needsRestart = false
+        }
+    }
+
+    // MARK: - General Tab
+
+    private var generalTab: some View {
+        Form {
+            Section {
+                LabeledContent("Cluster Namespace") {
+                    TextField("default", text: $pendingNamespace)
+                        .textFieldStyle(.roundedBorder)
+                        .frame(width: 200)
+                }
+                Text("Nodes with the same namespace form a cluster. Leave empty for default.")
+                    .font(.caption)
+                    .foregroundColor(.secondary)
+            }
+
+            Section {
+                LabeledContent("HuggingFace Token") {
+                    SecureField("optional", text: $pendingHFToken)
+                        .textFieldStyle(.roundedBorder)
+                        .frame(width: 200)
+                }
+                Text("Required for gated models. Get yours at huggingface.co/settings/tokens")
+                    .font(.caption)
+                    .foregroundColor(.secondary)
+            }
+
+            Section {
+                HStack {
+                    Spacer()
+                    Button("Save & Restart") {
+                        applyGeneralSettings()
+                    }
+                    .disabled(!hasGeneralChanges)
+                }
+            }
+        }
+        .formStyle(.grouped)
+        .padding()
+    }
+
+    // MARK: - Model Tab
+
+    private var modelTab: some View {
+        Form {
+            Section {
+                Toggle("Enable Image Models (experimental)", isOn: $pendingEnableImageModels)
+                Text("Allow text-to-image and image-to-image models in the model picker.")
+                    .font(.caption)
+                    .foregroundColor(.secondary)
+            }
+
+            Section {
+                HStack {
+                    Spacer()
+                    Button("Save & Restart") {
+                        applyModelSettings()
+                    }
+                    .disabled(!hasModelChanges)
+                }
+            }
+        }
+        .formStyle(.grouped)
+        .padding()
+    }
+
+    // MARK: - Advanced Tab
+
+    private var advancedTab: some View {
+        Form {
+            Section("Onboarding") {
+                HStack {
+                    VStack(alignment: .leading) {
+                        Text("Reset Onboarding")
+                        Text("Opens the dashboard and resets the onboarding wizard.")
+                            .font(.caption)
+                            .foregroundColor(.secondary)
+                    }
+                    Spacer()
+                    Button("Reset") {
+                        guard let url = URL(string: "http://localhost:52415/?reset-onboarding")
+                        else { return }
+                        NSWorkspace.shared.open(url)
+                    }
+                }
+            }
+
+            Section("Debug Info") {
+                LabeledContent("Thunderbolt Bridge") {
+                    Text(thunderboltStatusText)
+                        .foregroundColor(thunderboltStatusColor)
+                }
+
+                VStack(alignment: .leading, spacing: 2) {
+                    clusterThunderboltBridgeView
+                }
+
+                VStack(alignment: .leading, spacing: 2) {
+                    interfaceIpList
+                }
+
+                VStack(alignment: .leading, spacing: 2) {
+                    rdmaStatusView
+                }
+
+                sendBugReportButton
+            }
+
+            Section("Danger Zone") {
+                Button(role: .destructive) {
+                    showUninstallConfirmationAlert()
+                } label: {
+                    HStack {
+                        Text("Uninstall EXO")
+                        Spacer()
+                        Image(systemName: "trash")
+                            .imageScale(.small)
+                    }
+                }
+                .disabled(uninstallInProgress)
+            }
+        }
+        .formStyle(.grouped)
+        .padding()
+    }
+
+    // MARK: - About Tab
+
+    private var aboutTab: some View {
+        Form {
+            Section {
+                LabeledContent("Version") {
+                    Text(buildTag)
+                        .textSelection(.enabled)
+                }
+                LabeledContent("Commit") {
+                    Text(buildCommit)
+                        .font(.system(.body, design: .monospaced))
+                        .textSelection(.enabled)
+                }
+            }
+
+            Section {
+                Button("Check for Updates") {
+                    updater.checkForUpdates()
+                }
+            }
+        }
+        .formStyle(.grouped)
+        .padding()
+    }
+
+    // MARK: - Debug Info Views (moved from ContentView)
+
+    private var thunderboltStatusText: String {
+        switch networkStatusService.status.thunderboltBridgeState {
+        case .some(.disabled):
+            return "Disabled"
+        case .some(.deleted):
+            return "Deleted"
+        case .some(.enabled):
+            return "Enabled"
+        case nil:
+            return "Unknown"
+        }
+    }
+
+    private var thunderboltStatusColor: Color {
+        switch networkStatusService.status.thunderboltBridgeState {
+        case .some(.disabled), .some(.deleted):
+            return .green
+        case .some(.enabled):
+            return .red
+        case nil:
+            return .secondary
+        }
+    }
+
+    private var clusterThunderboltBridgeView: some View {
+        let bridgeStatuses = stateService.latestSnapshot?.nodeThunderboltBridge ?? [:]
+        let localNodeId = stateService.localNodeId
+        let nodeProfiles = stateService.latestSnapshot?.nodeProfiles ?? [:]
+
+        return VStack(alignment: .leading, spacing: 1) {
+            if bridgeStatuses.isEmpty {
+                Text("Cluster TB Bridge: No data")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+            } else {
+                Text("Cluster TB Bridge Status:")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+                ForEach(Array(bridgeStatuses.keys.sorted()), id: \.self) { nodeId in
+                    if let status = bridgeStatuses[nodeId] {
+                        let nodeName =
+                            nodeProfiles[nodeId]?.friendlyName ?? String(nodeId.prefix(8))
+                        let isLocal = nodeId == localNodeId
+                        let prefix = isLocal ? "  \(nodeName) (local):" : "  \(nodeName):"
+                        let statusText =
+                            !status.exists
+                            ? "N/A"
+                            : (status.enabled ? "Enabled" : "Disabled")
+                        let color: Color =
+                            !status.exists
+                            ? .secondary
+                            : (status.enabled ? .red : .green)
+                        Text("\(prefix) \(statusText)")
+                            .font(.caption2)
+                            .foregroundColor(color)
+                    }
+                }
+            }
+        }
+    }
+
+    private var interfaceIpList: some View {
+        let statuses = networkStatusService.status.interfaceStatuses
+        return VStack(alignment: .leading, spacing: 1) {
+            Text("Interfaces (en0–en7):")
+                .font(.caption2)
+                .foregroundColor(.secondary)
+            if statuses.isEmpty {
+                Text("  Unknown")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+            } else {
+                ForEach(statuses, id: \.interfaceName) { status in
+                    let ipText = status.ipAddress ?? "No IP"
+                    Text("  \(status.interfaceName): \(ipText)")
+                        .font(.caption2)
+                        .foregroundColor(status.ipAddress == nil ? .red : .green)
+                }
+            }
+        }
+    }
+
+    private var rdmaStatusView: some View {
+        let rdmaStatuses = stateService.latestSnapshot?.nodeRdmaCtl ?? [:]
+        let localNodeId = stateService.localNodeId
+        let nodeProfiles = stateService.latestSnapshot?.nodeProfiles ?? [:]
+        let localDevices = networkStatusService.status.localRdmaDevices
+        let localPorts = networkStatusService.status.localRdmaActivePorts
+
+        return VStack(alignment: .leading, spacing: 1) {
+            if rdmaStatuses.isEmpty {
+                Text("Cluster RDMA: No data")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+            } else {
+                Text("Cluster RDMA Status:")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+                ForEach(Array(rdmaStatuses.keys.sorted()), id: \.self) { nodeId in
+                    if let status = rdmaStatuses[nodeId] {
+                        let nodeName =
+                            nodeProfiles[nodeId]?.friendlyName ?? String(nodeId.prefix(8))
+                        let isLocal = nodeId == localNodeId
+                        let prefix = isLocal ? "  \(nodeName) (local):" : "  \(nodeName):"
+                        let statusText = status.enabled ? "Enabled" : "Disabled"
+                        let color: Color = status.enabled ? .green : .orange
+                        Text("\(prefix) \(statusText)")
+                            .font(.caption2)
+                            .foregroundColor(color)
+                    }
+                }
+            }
+            if !localDevices.isEmpty {
+                Text("  Local Devices: \(localDevices.joined(separator: ", "))")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+            }
+            if !localPorts.isEmpty {
+                Text("  Local Active Ports:")
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+                ForEach(localPorts, id: \.device) { port in
+                    Text("    \(port.device) port \(port.port): \(port.state)")
+                        .font(.caption2)
+                        .foregroundColor(.green)
+                }
+            }
+        }
+    }
+
+    private var sendBugReportButton: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Button {
+                Task {
+                    await sendBugReport()
+                }
+            } label: {
+                HStack {
+                    if bugReportInFlight {
+                        ProgressView()
+                            .scaleEffect(0.6)
+                    }
+                    Text("Send Bug Report")
+                        .font(.caption)
+                        .fontWeight(.semibold)
+                    Spacer()
+                }
+            }
+            .disabled(bugReportInFlight)
+
+            if let message = bugReportMessage {
+                Text(message)
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+                    .fixedSize(horizontal: false, vertical: true)
+            }
+        }
+    }
+
+    // MARK: - Actions
+
+    private func sendBugReport() async {
+        bugReportInFlight = true
+        bugReportMessage = "Collecting logs..."
+        let service = BugReportService()
+        do {
+            let outcome = try await service.sendReport(isManual: true)
+            bugReportMessage = outcome.message
+        } catch {
+            bugReportMessage = error.localizedDescription
+        }
+        bugReportInFlight = false
+    }
+
+    private func showUninstallConfirmationAlert() {
+        let alert = NSAlert()
+        alert.messageText = "Uninstall EXO"
+        alert.informativeText = """
+            This will remove EXO and all its system components:
+
+            • Network configuration daemon
+            • Launch at login registration
+            • EXO network location
+
+            The app will be moved to Trash.
+            """
+        alert.alertStyle = .warning
+        alert.addButton(withTitle: "Uninstall")
+        alert.addButton(withTitle: "Cancel")
+
+        if let uninstallButton = alert.buttons.first {
+            uninstallButton.hasDestructiveAction = true
+        }
+
+        let response = alert.runModal()
+        if response == .alertFirstButtonReturn {
+            performUninstall()
+        }
+    }
+
+    private func performUninstall() {
+        uninstallInProgress = true
+
+        controller.cancelPendingLaunch()
+        controller.stop()
+        stateService.stopPolling()
+
+        DispatchQueue.global(qos: .utility).async {
+            do {
+                try NetworkSetupHelper.uninstall()
+
+                DispatchQueue.main.async {
+                    LaunchAtLoginHelper.disable()
+                    self.moveAppToTrash()
+
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                        NSApplication.shared.terminate(nil)
+                    }
+                }
+            } catch {
+                DispatchQueue.main.async {
+                    let errorAlert = NSAlert()
+                    errorAlert.messageText = "Uninstall Failed"
+                    errorAlert.informativeText = error.localizedDescription
+                    errorAlert.alertStyle = .critical
+                    errorAlert.addButton(withTitle: "OK")
+                    errorAlert.runModal()
+                    self.uninstallInProgress = false
+                }
+            }
+        }
+    }
+
+    private func moveAppToTrash() {
+        guard let appURL = Bundle.main.bundleURL as URL? else { return }
+        do {
+            try FileManager.default.trashItem(at: appURL, resultingItemURL: nil)
+        } catch {
+            // If we can't trash the app, that's OK - user can do it manually
+        }
+    }
+
+    // MARK: - Helpers
+
+    private var hasGeneralChanges: Bool {
+        pendingNamespace != controller.customNamespace || pendingHFToken != controller.hfToken
+    }
+
+    private var hasModelChanges: Bool {
+        pendingEnableImageModels != controller.enableImageModels
+    }
+
+    private func applyGeneralSettings() {
+        controller.customNamespace = pendingNamespace
+        controller.hfToken = pendingHFToken
+        restartIfRunning()
+    }
+
+    private func applyModelSettings() {
+        controller.enableImageModels = pendingEnableImageModels
+        restartIfRunning()
+    }
+
+    private func restartIfRunning() {
+        if controller.status == .running || controller.status == .starting {
+            controller.restart()
+        }
+    }
+
+    private var buildTag: String {
+        Bundle.main.infoDictionary?["EXOBuildTag"] as? String ?? "unknown"
+    }
+
+    private var buildCommit: String {
+        Bundle.main.infoDictionary?["EXOBuildCommit"] as? String ?? "unknown"
+    }
+}
diff --git a/app/EXO/EXO/Views/SettingsWindowController.swift b/app/EXO/EXO/Views/SettingsWindowController.swift
new file mode 100644
index 00000000..98517f92
--- /dev/null
+++ b/app/EXO/EXO/Views/SettingsWindowController.swift
@@ -0,0 +1,47 @@
+import AppKit
+import SwiftUI
+
+/// Manages a standalone native macOS Settings window.
+/// Ensures only one instance exists and brings it to front on repeated opens.
+@MainActor
+final class SettingsWindowController: ObservableObject {
+    private var window: NSWindow?
+
+    func open(
+        controller: ExoProcessController,
+        updater: SparkleUpdater,
+        networkStatusService: NetworkStatusService,
+        thunderboltBridgeService: ThunderboltBridgeService,
+        stateService: ClusterStateService
+    ) {
+        if let existing = window, existing.isVisible {
+            existing.makeKeyAndOrderFront(nil)
+            NSApp.activate(ignoringOtherApps: true)
+            return
+        }
+
+        let settingsView = SettingsView()
+            .environmentObject(controller)
+            .environmentObject(updater)
+            .environmentObject(networkStatusService)
+            .environmentObject(thunderboltBridgeService)
+            .environmentObject(stateService)
+
+        let hostingView = NSHostingView(rootView: settingsView)
+
+        let newWindow = NSWindow(
+            contentRect: NSRect(x: 0, y: 0, width: 450, height: 400),
+            styleMask: [.titled, .closable],
+            backing: .buffered,
+            defer: false
+        )
+        newWindow.title = "EXO Settings"
+        newWindow.contentView = hostingView
+        newWindow.center()
+        newWindow.isReleasedWhenClosed = false
+        newWindow.makeKeyAndOrderFront(nil)
+        NSApp.activate(ignoringOtherApps: true)
+
+        window = newWindow
+    }
+}
diff --git a/dashboard/src/app.css b/dashboard/src/app.css
index fc532578..3e951163 100644
--- a/dashboard/src/app.css
+++ b/dashboard/src/app.css
@@ -202,6 +202,15 @@
 	filter: drop-shadow(0 0 3px oklch(0.85 0.18 85 / 0.5));
 }
 
+/* Onboarding step 2: connection line between devices */
+.onboarding-connection-line {
+	stroke: oklch(0.85 0.18 85 / 0.5);
+	stroke-width: 1.5px;
+	stroke-dasharray: 6, 6;
+	animation: flowAnimation 1s linear infinite;
+	filter: drop-shadow(0 0 4px oklch(0.85 0.18 85 / 0.4));
+}
+
 .graph-link-active {
 	stroke: oklch(0.85 0.18 85 / 0.8);
 	stroke-width: 2px;
@@ -320,3 +329,31 @@ input:focus, textarea:focus {
 		transform: translate(400px, 400px);
 	}
 }
+
+/* Respect reduced motion preference */
+@media (prefers-reduced-motion: reduce) {
+	.shooting-star,
+	.shooting-star::before {
+		animation: none !important;
+		opacity: 0 !important;
+	}
+	.graph-link {
+		animation: none;
+	}
+	.status-pulse {
+		animation: none;
+	}
+	.cursor-blink {
+		animation: none;
+	}
+	.onboarding-connection-line {
+		animation: none;
+	}
+	*,
+	*::before,
+	*::after {
+		transition-duration: 0.01ms !important;
+		animation-duration: 0.01ms !important;
+		animation-iteration-count: 1 !important;
+	}
+}
diff --git a/dashboard/src/lib/components/ChatForm.svelte b/dashboard/src/lib/components/ChatForm.svelte
index 2eb7aa40..1bc554e8 100644
--- a/dashboard/src/lib/components/ChatForm.svelte
+++ b/dashboard/src/lib/components/ChatForm.svelte
@@ -28,6 +28,7 @@
     showModelSelector?: boolean;
     modelTasks?: Record<string, string[]>;
     modelCapabilities?: Record<string, string[]>;
+    onSend?: () => void;
   }
 
   let {
@@ -38,6 +39,7 @@
     showModelSelector = false,
     modelTasks = {},
     modelCapabilities = {},
+    onSend,
   }: Props = $props();
 
   let message = $state("");
@@ -305,6 +307,8 @@
       );
     }
 
+    onSend?.();
+
     // Refocus the textarea after sending
     setTimeout(() => textareaRef?.focus(), 10);
   }
diff --git a/dashboard/src/lib/components/ChatMessages.svelte b/dashboard/src/lib/components/ChatMessages.svelte
index ba5322a7..7c076459 100644
--- a/dashboard/src/lib/components/ChatMessages.svelte
+++ b/dashboard/src/lib/components/ChatMessages.svelte
@@ -802,8 +802,8 @@
       >
         AWAITING INPUT
       </p>
-      <p class="text-sm sm:text-xs text-exo-light-gray tracking-wider mt-1">
-        ENTER A QUERY TO BEGIN
+      <p class="text-xs text-white/30 tracking-wider mt-1.5 font-mono">
+        Type a message below &middot; Shift+Enter for newline
       </p>
     </div>
   {/if}
@@ -818,6 +818,7 @@
       onclick={scrollToBottom}
       class="sticky bottom-4 left-1/2 -translate-x-1/2 w-10 h-10 rounded-full bg-exo-dark-gray/90 border border-exo-medium-gray/50 flex items-center justify-center text-exo-light-gray hover:text-exo-yellow hover:border-exo-yellow/50 transition-all shadow-lg cursor-pointer z-10"
       title="Scroll to bottom"
+      aria-label="Scroll to bottom of messages"
     >
       <svg
         class="w-5 h-5"
diff --git a/dashboard/src/lib/components/ChatSidebar.svelte b/dashboard/src/lib/components/ChatSidebar.svelte
index 6a822ddf..e7dbe908 100644
--- a/dashboard/src/lib/components/ChatSidebar.svelte
+++ b/dashboard/src/lib/components/ChatSidebar.svelte
@@ -303,7 +303,7 @@
       <div class="py-2">
         <div class="px-4 py-2">
           <span
-            class="text-sm text-white/70 font-mono tracking-wider uppercase"
+            class="text-xs text-exo-light-gray font-mono tracking-wider uppercase"
           >
             {searchQuery ? "SEARCH RESULTS" : "CONVERSATIONS"}
           </span>
@@ -372,39 +372,37 @@
                 onkeydown={(e) =>
                   e.key === "Enter" &&
                   handleSelectConversation(conversation.id)}
-                class="group w-full flex items-center justify-between p-2 rounded mb-1 transition-all text-left cursor-pointer
+                class="group w-full flex items-center justify-between p-2.5 rounded-lg mb-1 transition-all text-left cursor-pointer
 									{activeId === conversation.id
-                  ? 'bg-transparent border border-exo-yellow/30'
-                  : 'hover:border-exo-yellow/20 border border-transparent'}"
+                  ? 'bg-exo-yellow/5 border border-exo-yellow/30'
+                  : 'hover:bg-white/[0.03] hover:border-white/10 border border-transparent'}"
               >
                 <div class="flex-1 min-w-0 pr-2">
                   <div
-                    class="text-sm truncate {activeId === conversation.id
+                    class="text-sm font-medium truncate {activeId ===
+                    conversation.id
                       ? 'text-exo-yellow'
-                      : 'text-white/90'}"
+                      : 'text-white'}"
                   >
                     {conversation.name}
                   </div>
-                  <div class="text-sm text-white/50 mt-0.5">
+                  <div class="text-xs text-white/60 mt-0.5">
                     {formatDate(conversation.updatedAt)}
                   </div>
-                  <div class="text-sm text-white/70 truncate">
+                  <div class="text-xs text-exo-light-gray truncate">
                     {info.modelLabel}
                   </div>
-                  <div class="text-xs text-white/60 font-mono">
-                    Strategy: <span class="text-white/80"
-                      >{info.strategyLabel}</span
-                    >
-                  </div>
                   {#if stats}
-                    <div class="text-xs text-white/60 font-mono mt-1">
-                      {#if stats.ttftMs}<span class="text-white/40">TTFT</span>
-                        {stats.ttftMs.toFixed(
-                          0,
-                        )}ms{/if}{#if stats.ttftMs && stats.tps}<span
-                          class="text-white/30 mx-1.5">•</span
-                        >{/if}{#if stats.tps}{stats.tps.toFixed(1)}
-                        <span class="text-white/40">tok/s</span>{/if}
+                    <div class="text-xs text-white/70 font-mono mt-1">
+                      {#if stats.ttftMs}<span class="text-white/50">TTFT</span>
+                        <span class="text-exo-yellow/80"
+                          >{stats.ttftMs.toFixed(0)}ms</span
+                        >{/if}{#if stats.ttftMs && stats.tps}<span
+                          class="text-white/30 mx-1.5">·</span
+                        >{/if}{#if stats.tps}<span class="text-exo-yellow/80"
+                          >{stats.tps.toFixed(1)}</span
+                        >
+                        <span class="text-white/50">tok/s</span>{/if}
                     </div>
                   {/if}
                 </div>
diff --git a/dashboard/src/lib/components/ConnectionBanner.svelte b/dashboard/src/lib/components/ConnectionBanner.svelte
new file mode 100644
index 00000000..3339cf24
--- /dev/null
+++ b/dashboard/src/lib/components/ConnectionBanner.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+  import { isConnected } from "$lib/stores/app.svelte";
+  import { slide } from "svelte/transition";
+
+  const connected = $derived(isConnected());
+</script>
+
+{#if !connected}
+  <div
+    transition:slide={{ duration: 200 }}
+    class="relative z-50 flex items-center justify-center gap-2 px-4 py-2 bg-red-950/80 border-b border-red-500/30"
+    role="alert"
+    aria-live="assertive"
+  >
+    <div class="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
+    <span class="text-xs font-mono text-red-300 tracking-wider uppercase">
+      Connection lost &mdash; Reconnecting to backend&hellip;
+    </span>
+  </div>
+{/if}
diff --git a/dashboard/src/lib/components/HeaderNav.svelte b/dashboard/src/lib/components/HeaderNav.svelte
index 5bdcf1ba..d78bc809 100644
--- a/dashboard/src/lib/components/HeaderNav.svelte
+++ b/dashboard/src/lib/components/HeaderNav.svelte
@@ -6,6 +6,10 @@
   export let showSidebarToggle = false;
   export let sidebarVisible = true;
   export let onToggleSidebar: (() => void) | null = null;
+  export let downloadProgress: {
+    count: number;
+    percentage: number;
+  } | null = null;
 
   function handleHome(): void {
     if (onHome) {
@@ -35,11 +39,15 @@
         onclick={handleToggleSidebar}
         class="p-2 rounded border border-exo-medium-gray/40 hover:border-exo-yellow/50 transition-colors cursor-pointer"
         title={sidebarVisible ? "Hide sidebar" : "Show sidebar"}
+        aria-label={sidebarVisible
+          ? "Hide conversation sidebar"
+          : "Show conversation sidebar"}
+        aria-pressed={sidebarVisible}
       >
         <svg
           class="w-5 h-5 {sidebarVisible
             ? 'text-exo-yellow'
-            : 'text-exo-medium-gray'}"
+            : 'text-exo-light-gray'}"
           fill="none"
           viewBox="0 0 24 24"
           stroke="currentColor"
@@ -80,8 +88,9 @@
   </button>
 
   <!-- Right: Home + Downloads -->
-  <div
+  <nav
     class="absolute right-6 top-1/2 -translate-y-1/2 flex items-center gap-4"
+    aria-label="Main navigation"
   >
     {#if showHome}
       <button
@@ -110,20 +119,56 @@
       class="text-sm text-exo-light-gray hover:text-exo-yellow transition-colors tracking-wider uppercase flex items-center gap-2 cursor-pointer"
       title="View downloads overview"
     >
-      <svg
-        class="w-4 h-4"
-        viewBox="0 0 24 24"
-        fill="none"
-        stroke="currentColor"
-        stroke-width="2"
-        stroke-linecap="round"
-        stroke-linejoin="round"
-      >
-        <path d="M12 3v12" />
-        <path d="M7 12l5 5 5-5" />
-        <path d="M5 21h14" />
-      </svg>
+      {#if downloadProgress}
+        <!-- Compact download progress indicator -->
+        <div class="relative w-4 h-4 flex-shrink-0">
+          <svg class="w-4 h-4 -rotate-90" viewBox="0 0 20 20">
+            <circle
+              cx="10"
+              cy="10"
+              r="8"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="2"
+              opacity="0.2"
+            />
+            <circle
+              cx="10"
+              cy="10"
+              r="8"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="2"
+              stroke-dasharray={2 * Math.PI * 8}
+              stroke-dashoffset={2 *
+                Math.PI *
+                8 *
+                (1 - downloadProgress.percentage / 100)}
+              class="text-blue-400 transition-all duration-300"
+            />
+          </svg>
+          <div
+            class="absolute inset-0 flex items-center justify-center text-[6px] font-mono text-blue-400"
+          >
+            {downloadProgress.count}
+          </div>
+        </div>
+      {:else}
+        <svg
+          class="w-4 h-4"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          stroke-width="2"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+        >
+          <path d="M12 3v12" />
+          <path d="M7 12l5 5 5-5" />
+          <path d="M5 21h14" />
+        </svg>
+      {/if}
       Downloads
     </a>
-  </div>
+  </nav>
 </header>
diff --git a/dashboard/src/lib/components/ModelCard.svelte b/dashboard/src/lib/components/ModelCard.svelte
index 561c325b..b432b7a8 100644
--- a/dashboard/src/lib/components/ModelCard.svelte
+++ b/dashboard/src/lib/components/ModelCard.svelte
@@ -567,11 +567,17 @@
     <div class="flex items-center gap-1.5 mb-2">
       <span
         class="px-1.5 py-0.5 text-xs font-mono tracking-wider uppercase bg-exo-medium-gray/30 text-exo-light-gray border border-exo-medium-gray/40"
+        title={sharding === "Pipeline"
+          ? "Pipeline: splits model into sequential stages across devices. Lower network overhead."
+          : "Tensor: splits each layer across devices. Best with high-bandwidth connections (Thunderbolt)."}
       >
         {sharding}
       </span>
       <span
         class="px-1.5 py-0.5 text-xs font-mono tracking-wider uppercase bg-exo-medium-gray/30 text-exo-light-gray border border-exo-medium-gray/40"
+        title={runtime === "MlxRing"
+          ? "Ring: standard networking. Works over any connection (Wi-Fi, Ethernet, Thunderbolt)."
+          : "RDMA: direct memory access over Thunderbolt. Significantly faster for multi-device inference."}
       >
         {runtime === "MlxRing"
           ? "MLX Ring"
@@ -581,6 +587,26 @@
       </span>
     </div>
 
+    <!-- Download Status -->
+    {#if isDownloading && progress}
+      <div class="mb-2 space-y-1">
+        <div class="flex items-center justify-between text-xs font-mono">
+          <span class="text-blue-400 tracking-wider uppercase">Downloading</span
+          >
+          <span class="text-white/60"
+            >{percentage.toFixed(1)}% &middot; {formatSpeed(progress.speed)}
+            &middot; {formatEta(progress.etaMs)}</span
+          >
+        </div>
+        <div class="h-1 bg-exo-medium-gray/30 rounded overflow-hidden">
+          <div
+            class="h-full bg-blue-500/70 transition-all duration-300"
+            style="width: {percentage}%"
+          ></div>
+        </div>
+      </div>
+    {/if}
+
     <!-- Mini Topology Preview -->
     {#if placementPreview().nodes.length > 0}
       {@const preview = placementPreview()}
diff --git a/dashboard/src/lib/components/ModelPickerModal.svelte b/dashboard/src/lib/components/ModelPickerModal.svelte
index 84a93ee7..cf21c727 100644
--- a/dashboard/src/lib/components/ModelPickerModal.svelte
+++ b/dashboard/src/lib/components/ModelPickerModal.svelte
@@ -512,6 +512,18 @@
     );
   });
 
+  // Split filtered groups into recommended (fits_now) and others for visual separation
+  const recommendedGroups = $derived(
+    filteredGroups.filter((g) =>
+      g.variants.some((v) => getModelFitStatus(v.id) === "fits_now"),
+    ),
+  );
+  const otherGroups = $derived(
+    filteredGroups.filter(
+      (g) => !g.variants.some((v) => getModelFitStatus(v.id) === "fits_now"),
+    ),
+  );
+
   function toggleGroupExpanded(groupId: string) {
     const next = new Set(expandedGroups);
     if (next.has(groupId)) {
@@ -840,7 +852,60 @@
             {/if}
           </div>
         {:else}
-          {#each filteredGroups as group}
+          <!-- Recommended for your cluster -->
+          {#if recommendedGroups.length > 0 && otherGroups.length > 0 && !searchQuery.trim()}
+            <div
+              class="sticky top-0 z-10 flex items-center gap-2 px-3 py-2 bg-green-950/60 border-b border-green-500/20 backdrop-blur-sm"
+            >
+              <svg
+                class="w-3.5 h-3.5 text-green-400 flex-shrink-0"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+                stroke-width="2"
+              >
+                <path
+                  stroke-linecap="round"
+                  stroke-linejoin="round"
+                  d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+                />
+              </svg>
+              <span
+                class="text-xs font-mono text-green-400 tracking-wider uppercase"
+                >Recommended for your cluster</span
+              >
+              <span class="text-xs font-mono text-green-400/50"
+                >— fits in available memory</span
+              >
+            </div>
+          {/if}
+          {#each recommendedGroups as group}
+            <ModelPickerGroup
+              {group}
+              isExpanded={expandedGroups.has(group.id)}
+              isFavorite={favorites.has(group.id)}
+              {selectedModelId}
+              {canModelFit}
+              {getModelFitStatus}
+              onToggleExpand={() => toggleGroupExpanded(group.id)}
+              onSelectModel={handleSelect}
+              {onToggleFavorite}
+              onShowInfo={(g) => (infoGroup = g)}
+              downloadStatusMap={getVariantDownloadMap(group)}
+            />
+          {/each}
+          <!-- Other models -->
+          {#if otherGroups.length > 0 && recommendedGroups.length > 0 && !searchQuery.trim()}
+            <div
+              class="sticky top-0 z-10 flex items-center gap-2 px-3 py-2 bg-exo-dark-gray/80 border-y border-exo-medium-gray/20 backdrop-blur-sm"
+            >
+              <span
+                class="text-xs font-mono text-white/40 tracking-wider uppercase"
+                >Other models</span
+              >
+            </div>
+          {/if}
+          {#each otherGroups as group}
             <ModelPickerGroup
               {group}
               isExpanded={expandedGroups.has(group.id)}
diff --git a/dashboard/src/lib/components/ToastContainer.svelte b/dashboard/src/lib/components/ToastContainer.svelte
new file mode 100644
index 00000000..6bcb77b8
--- /dev/null
+++ b/dashboard/src/lib/components/ToastContainer.svelte
@@ -0,0 +1,117 @@
+<script lang="ts">
+  import { toasts, dismissToast, type Toast } from "$lib/stores/toast.svelte";
+  import { fly, fade } from "svelte/transition";
+  import { flip } from "svelte/animate";
+
+  const items = $derived(toasts());
+
+  const typeStyles: Record<
+    Toast["type"],
+    { border: string; icon: string; iconColor: string }
+  > = {
+    success: {
+      border: "border-l-green-500",
+      icon: "M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
+      iconColor: "text-green-400",
+    },
+    error: {
+      border: "border-l-red-500",
+      icon: "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z",
+      iconColor: "text-red-400",
+    },
+    warning: {
+      border: "border-l-yellow-500",
+      icon: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z",
+      iconColor: "text-yellow-400",
+    },
+    info: {
+      border: "border-l-blue-500",
+      icon: "M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z",
+      iconColor: "text-blue-400",
+    },
+  };
+</script>
+
+{#if items.length > 0}
+  <div
+    class="fixed bottom-6 right-6 z-[9999] flex flex-col gap-2 pointer-events-none"
+    role="log"
+    aria-live="polite"
+    aria-label="Notifications"
+  >
+    {#each items as toast (toast.id)}
+      {@const style = typeStyles[toast.type]}
+      <div
+        class="pointer-events-auto max-w-sm w-80 bg-exo-dark-gray/95 backdrop-blur-sm border border-exo-medium-gray/60 border-l-[3px] {style.border} rounded shadow-lg shadow-black/40"
+        in:fly={{ x: 80, duration: 250 }}
+        out:fade={{ duration: 150 }}
+        animate:flip={{ duration: 200 }}
+        role="alert"
+      >
+        <div class="flex items-start gap-3 px-4 py-3">
+          <!-- Icon -->
+          <svg
+            class="w-5 h-5 flex-shrink-0 mt-0.5 {style.iconColor}"
+            fill="none"
+            viewBox="0 0 24 24"
+            stroke-width="1.5"
+            stroke="currentColor"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d={style.icon}
+            />
+          </svg>
+
+          <!-- Message -->
+          <p class="flex-1 text-sm text-white/90 font-mono leading-snug">
+            {toast.message}
+          </p>
+
+          <!-- Dismiss button -->
+          <button
+            onclick={() => dismissToast(toast.id)}
+            class="flex-shrink-0 p-0.5 text-white/40 hover:text-white/80 transition-colors cursor-pointer"
+            aria-label="Dismiss notification"
+          >
+            <svg
+              class="w-4 h-4"
+              fill="none"
+              viewBox="0 0 24 24"
+              stroke-width="2"
+              stroke="currentColor"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M6 18L18 6M6 6l12 12"
+              />
+            </svg>
+          </button>
+        </div>
+
+        <!-- Auto-dismiss progress bar -->
+        {#if toast.duration > 0}
+          <div class="h-0.5 bg-white/5 rounded-b overflow-hidden">
+            <div
+              class="h-full {style.border.replace('border-l-', 'bg-')}/60"
+              style="animation: shrink {toast.duration}ms linear forwards"
+            ></div>
+          </div>
+        {/if}
+      </div>
+    {/each}
+  </div>
+{/if}
+
+<style>
+  @keyframes shrink {
+    from {
+      width: 100%;
+    }
+    to {
+      width: 0%;
+    }
+  }
+</style>
diff --git a/dashboard/src/lib/stores/app.svelte.ts b/dashboard/src/lib/stores/app.svelte.ts
index ebb2d0df..5a5afc78 100644
--- a/dashboard/src/lib/stores/app.svelte.ts
+++ b/dashboard/src/lib/stores/app.svelte.ts
@@ -587,6 +587,12 @@ class AppStore {
   // Image editing state
   editingImage = $state<EditingImage | null>(null);
 
+  /** True when the backend is reachable. */
+  isConnected = $state<boolean>(true);
+  /** Number of consecutive fetch failures. */
+  private consecutiveFailures = 0;
+  private static readonly CONNECTION_LOST_THRESHOLD = 3;
+
   private fetchInterval: ReturnType<typeof setInterval> | null = null;
   private previewsInterval: ReturnType<typeof setInterval> | null = null;
   private lastConversationPersistTs = 0;
@@ -1290,7 +1296,19 @@ class AppStore {
       // Thunderbolt bridge status per node
       this.nodeThunderboltBridge = data.nodeThunderboltBridge ?? {};
       this.lastUpdate = Date.now();
+      // Connection recovered
+      if (!this.isConnected) {
+        this.isConnected = true;
+      }
+      this.consecutiveFailures = 0;
     } catch (error) {
+      this.consecutiveFailures++;
+      if (
+        this.consecutiveFailures >= AppStore.CONNECTION_LOST_THRESHOLD &&
+        this.isConnected
+      ) {
+        this.isConnected = false;
+      }
       console.error("Error fetching state:", error);
     }
   }
@@ -1817,7 +1835,7 @@ class AppStore {
           assistantMessage.id,
           (msg) => {
             msg.content =
-              "Error: No model available. Please launch an instance first.";
+              "No model is loaded yet. Select a model from the sidebar to get started — it will download and load automatically.";
           },
         );
         this.syncActiveMessagesIfNeeded(targetConversationId);
@@ -2255,7 +2273,7 @@ class AppStore {
       const modelToUse = this.getModelForRequest();
       if (!modelToUse) {
         throw new Error(
-          "No model selected and no running instances available. Please launch an instance first.",
+          "No model is loaded yet. Select a model from the sidebar to get started — it will download and load automatically.",
         );
       }
 
@@ -3144,6 +3162,9 @@ export const setChatSidebarVisible = (visible: boolean) =>
   appStore.setChatSidebarVisible(visible);
 export const refreshState = () => appStore.fetchState();
 
+// Connection status
+export const isConnected = () => appStore.isConnected;
+
 // Node identities (for OS version mismatch detection)
 export const nodeIdentities = () => appStore.nodeIdentities;
 
diff --git a/dashboard/src/lib/stores/toast.svelte.ts b/dashboard/src/lib/stores/toast.svelte.ts
new file mode 100644
index 00000000..e9e62019
--- /dev/null
+++ b/dashboard/src/lib/stores/toast.svelte.ts
@@ -0,0 +1,87 @@
+/**
+ * Toast notification store - Global notification system for the EXO dashboard.
+ *
+ * Usage:
+ *   import { addToast, dismissToast, toasts } from "$lib/stores/toast.svelte";
+ *   addToast({ type: "success", message: "Model launched" });
+ *   addToast({ type: "error", message: "Connection lost", persistent: true });
+ */
+
+type ToastType = "success" | "error" | "warning" | "info";
+
+export interface Toast {
+  id: string;
+  type: ToastType;
+  message: string;
+  /** Auto-dismiss after this many ms. 0 = persistent (must be dismissed manually). */
+  duration: number;
+  createdAt: number;
+}
+
+interface ToastInput {
+  type: ToastType;
+  message: string;
+  /** If true, toast stays until manually dismissed. Default: false. */
+  persistent?: boolean;
+  /** Auto-dismiss duration in ms. Default: 4000 for success/info, 6000 for error/warning. */
+  duration?: number;
+}
+
+const DEFAULT_DURATIONS: Record<ToastType, number> = {
+  success: 4000,
+  info: 4000,
+  warning: 6000,
+  error: 6000,
+};
+
+let toastList = $state<Toast[]>([]);
+const timers = new Map<string, ReturnType<typeof setTimeout>>();
+
+function generateId(): string {
+  return `toast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+}
+
+export function addToast(input: ToastInput): string {
+  const id = generateId();
+  const duration = input.persistent
+    ? 0
+    : (input.duration ?? DEFAULT_DURATIONS[input.type]);
+
+  const toast: Toast = {
+    id,
+    type: input.type,
+    message: input.message,
+    duration,
+    createdAt: Date.now(),
+  };
+
+  toastList = [...toastList, toast];
+
+  if (duration > 0) {
+    const timer = setTimeout(() => dismissToast(id), duration);
+    timers.set(id, timer);
+  }
+
+  return id;
+}
+
+export function dismissToast(id: string): void {
+  const timer = timers.get(id);
+  if (timer) {
+    clearTimeout(timer);
+    timers.delete(id);
+  }
+  toastList = toastList.filter((t) => t.id !== id);
+}
+
+/** Dismiss all toasts matching a message (useful for dedup). */
+export function dismissByMessage(message: string): void {
+  const matching = toastList.filter((t) => t.message === message);
+  for (const t of matching) {
+    dismissToast(t.id);
+  }
+}
+
+export function toasts(): Toast[] {
+  return toastList;
+}
diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte
index d249e9cf..295c9dc0 100644
--- a/dashboard/src/routes/+layout.svelte
+++ b/dashboard/src/routes/+layout.svelte
@@ -1,5 +1,7 @@
 <script lang="ts">
   import "../app.css";
+  import ToastContainer from "$lib/components/ToastContainer.svelte";
+  import ConnectionBanner from "$lib/components/ConnectionBanner.svelte";
 
   let { children } = $props();
 </script>
@@ -10,5 +12,7 @@
 </svelte:head>
 
 <div class="min-h-screen bg-background text-foreground">
+  <ConnectionBanner />
   {@render children?.()}
+  <ToastContainer />
 </div>
diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte
index 5f7dcf04..20f196ee 100644
--- a/dashboard/src/routes/+page.svelte
+++ b/dashboard/src/routes/+page.svelte
@@ -36,6 +36,7 @@
     createConversation,
     setSelectedChatModel,
     selectedChatModel,
+    sendMessage,
     debugMode,
     toggleDebugMode,
     topologyOnlyMode,
@@ -52,9 +53,11 @@
     type PlacementPreview,
     type MetaInstanceData,
   } from "$lib/stores/app.svelte";
+  import { addToast } from "$lib/stores/toast.svelte";
   import HeaderNav from "$lib/components/HeaderNav.svelte";
-  import { fade, fly } from "svelte/transition";
-  import { cubicInOut } from "svelte/easing";
+  import { fade, fly, slide } from "svelte/transition";
+  import { tweened } from "svelte/motion";
+  import { cubicInOut, cubicOut } from "svelte/easing";
   import { onMount } from "svelte";
 
   const chatStarted = $derived(hasStartedChat());
@@ -140,6 +143,26 @@
   const rdmaCtlData = $derived(nodeRdmaCtl());
   const nodeFilter = $derived(previewNodeFilter());
 
+  // Aggregate active download progress across all instances for header indicator
+  const activeDownloadSummary = $derived.by(() => {
+    let totalBytes = 0;
+    let downloadedBytes = 0;
+    let count = 0;
+    for (const [id, inst] of Object.entries(instanceData)) {
+      const status = getInstanceDownloadStatus(id, inst);
+      if (status.isDownloading && status.progress) {
+        count++;
+        totalBytes += status.progress.totalBytes || 0;
+        downloadedBytes += status.progress.downloadedBytes || 0;
+      }
+    }
+    if (count === 0) return null;
+    return {
+      count,
+      percentage: totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : 0,
+    };
+  });
+
   // Detect macOS version mismatches across cluster nodes
   const macosVersionMismatch = $derived.by(() => {
     if (!identitiesData) return null;
@@ -230,6 +253,171 @@
 
   let mounted = $state(false);
 
+  // ── Onboarding wizard state ──
+  const ONBOARDING_COMPLETE_KEY = "exo-onboarding-complete";
+  let onboardingStep = $state(0); // 0 = not in onboarding, 1-7 = wizard steps
+  let onboardingModelId = $state<string | null>(null); // model selected during onboarding
+  const showOnboarding = $derived(onboardingStep > 0);
+
+  // ── Step 2 animation state: "Add more devices, run bigger models" ──
+  let deviceAnimPhase = $state(0); // 0=waiting, 1=macbook, 2=studio joins, 3=connection+mid unlock, 4=big unlock
+  let showContinueStep2 = $state(false);
+  const studioX = tweened(540, { duration: 700, easing: cubicOut });
+  const studioOpacity = tweened(0, { duration: 700, easing: cubicOut });
+
+  $effect(() => {
+    if (onboardingStep === 2) {
+      deviceAnimPhase = 0;
+      showContinueStep2 = false;
+      studioX.set(540, { duration: 0 });
+      studioOpacity.set(0, { duration: 0 });
+
+      const t1 = setTimeout(() => {
+        deviceAnimPhase = 1;
+      }, 100);
+      const t2 = setTimeout(() => {
+        deviceAnimPhase = 2;
+        studioX.set(340);
+        studioOpacity.set(1);
+      }, 900);
+      const t3 = setTimeout(() => {
+        deviceAnimPhase = 3;
+      }, 1700);
+      const t4 = setTimeout(() => {
+        deviceAnimPhase = 4;
+      }, 2500);
+      const t5 = setTimeout(() => {
+        showContinueStep2 = true;
+      }, 3500);
+
+      return () => {
+        clearTimeout(t1);
+        clearTimeout(t2);
+        clearTimeout(t3);
+        clearTimeout(t4);
+        clearTimeout(t5);
+      };
+    }
+  });
+
+  // Recommended models for onboarding (sorted by fit, then size desc, limited to 6)
+  const onboardingModels = $derived.by(() => {
+    if (models.length === 0) return [];
+    return [...models]
+      .filter((m) => getModelMemoryFitStatus(m) !== "too_large")
+      .sort((a, b) => {
+        const aFit = hasEnoughMemory(a) ? 0 : 1;
+        const bFit = hasEnoughMemory(b) ? 0 : 1;
+        if (aFit !== bFit) return aFit - bFit;
+        return getModelSizeGB(b) - getModelSizeGB(a);
+      })
+      .slice(0, 6);
+  });
+
+  // Track onboarding instance status for auto-advancing steps.
+  // Handles cached models: if no download is needed, skip step 5 entirely.
+  $effect(() => {
+    if (onboardingStep === 5 && instanceCount > 0) {
+      let anyDownloading = false;
+      let anyReady = false;
+      for (const [id, inst] of Object.entries(instanceData)) {
+        const status = getInstanceDownloadStatus(id, inst);
+        if (status.isDownloading) {
+          anyDownloading = true;
+        }
+        if (
+          status.statusText === "READY" ||
+          status.statusText === "LOADED" ||
+          status.statusText === "RUNNING"
+        ) {
+          anyReady = true;
+        }
+      }
+      // Model already cached & ready — skip download AND loading steps
+      if (anyReady) {
+        onboardingStep = 7;
+      } else if (!anyDownloading) {
+        // Download finished (or was never needed) but not ready yet
+        onboardingStep = 6;
+      }
+    }
+  });
+
+  $effect(() => {
+    if (onboardingStep === 6 && instanceCount > 0) {
+      for (const [id, inst] of Object.entries(instanceData)) {
+        const status = getInstanceDownloadStatus(id, inst);
+        if (
+          status.statusText === "READY" ||
+          status.statusText === "LOADED" ||
+          status.statusText === "RUNNING"
+        ) {
+          onboardingStep = 7;
+          break;
+        }
+      }
+    }
+  });
+
+  function completeOnboarding() {
+    onboardingStep = 0;
+    try {
+      localStorage.setItem(ONBOARDING_COMPLETE_KEY, "true");
+    } catch {
+      // ignore
+    }
+  }
+
+  let onboardingError = $state<string | null>(null);
+
+  async function onboardingLaunchModel(modelId: string) {
+    onboardingModelId = modelId;
+    onboardingError = null;
+    selectPreviewModel(modelId);
+    onboardingStep = 5;
+    // Launch via API
+    try {
+      const placementResponse = await fetch(
+        `/instance/placement?model_id=${encodeURIComponent(modelId)}&sharding=${selectedSharding}&instance_meta=${selectedInstanceType}&min_nodes=1`,
+      );
+      if (!placementResponse.ok) {
+        const errorText = await placementResponse.text();
+        onboardingError = `Could not place model: ${errorText}`;
+        onboardingStep = 4;
+        return;
+      }
+      const placementData = await placementResponse.json();
+      const response = await fetch("/instance", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ instance: placementData }),
+      });
+      if (!response.ok) {
+        const errorText = await response.text();
+        onboardingError = `Failed to launch: ${errorText}`;
+        onboardingStep = 4;
+        return;
+      }
+      setSelectedChatModel(modelId);
+      recordRecentLaunch(modelId);
+    } catch (error) {
+      onboardingError = `Network error: ${error}`;
+      onboardingStep = 4;
+    }
+  }
+
+  // Helper to get onboarding download progress
+  const onboardingDownloadProgress = $derived.by(() => {
+    if (instanceCount === 0) return null;
+    for (const [id, inst] of Object.entries(instanceData)) {
+      const status = getInstanceDownloadStatus(id, inst);
+      if (status.isDownloading && status.progress) {
+        return status.progress;
+      }
+    }
+    return null;
+  });
+
   // Instance launch state
   let models = $state<
     Array<{
@@ -374,6 +562,9 @@
   // Model picker modal state
   let isModelPickerOpen = $state(false);
 
+  // Advanced options toggle (hides technical jargon for new users)
+  let showAdvancedOptions = $state(false);
+
   // Favorites state (reactive)
   const favoritesSet = $derived(getFavoritesSet());
 
@@ -703,6 +894,20 @@
   onMount(() => {
     mounted = true;
     fetchModels();
+
+    // Handle reset-onboarding query parameter (triggered from native Settings)
+    const params = new URLSearchParams(window.location.search);
+    if (params.has("reset-onboarding")) {
+      localStorage.removeItem(ONBOARDING_COMPLETE_KEY);
+      window.history.replaceState({}, "", window.location.pathname);
+      onboardingStep = 1;
+      return;
+    }
+
+    // Show onboarding wizard for first-time users
+    if (!localStorage.getItem(ONBOARDING_COMPLETE_KEY)) {
+      onboardingStep = 1;
+    }
   });
 
   async function fetchModels() {
@@ -782,7 +987,30 @@
           ? Array.from(nodeFilter)
           : undefined;
 
-      const response = await fetch("/meta_instance", {
+      if (preview?.instance) {
+        // Use the instance from the preview
+        instanceData = preview.instance;
+      } else {
+        // Fallback: GET placement from API
+        const placementResponse = await fetch(
+          `/instance/placement?model_id=${encodeURIComponent(modelId)}&sharding=${selectedSharding}&instance_meta=${selectedInstanceType}&min_nodes=${selectedMinNodes}`,
+        );
+
+        if (!placementResponse.ok) {
+          const errorText = await placementResponse.text();
+          console.error("Failed to get placement:", errorText);
+          addToast({
+            type: "error",
+            message: `Placement failed: ${errorText}`,
+          });
+          return;
+        }
+
+        instanceData = await placementResponse.json();
+      }
+
+      // POST the instance to create it
+      const response = await fetch("/instance", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({
@@ -796,8 +1024,13 @@
 
       if (!response.ok) {
         const errorText = await response.text();
-        console.error("Failed to create meta instance:", errorText);
+        console.error("Failed to launch instance:", errorText);
+        addToast({
+          type: "error",
+          message: `Failed to launch model: ${errorText}`,
+        });
       } else {
+        addToast({ type: "success", message: `Model launched successfully` });
         // Always auto-select the newly launched model so the user chats to what they just launched
         setSelectedChatModel(modelId);
 
@@ -819,7 +1052,11 @@
         setTimeout(scrollToBottom, 1000);
       }
     } catch (error) {
-      console.error("Error creating meta instance:", error);
+      console.error("Error launching instance:", error);
+      addToast({
+        type: "error",
+        message: "Failed to launch model. Check console for details.",
+      });
     } finally {
       launchingModelId = null;
     }
@@ -1428,6 +1665,7 @@
 
       if (!response.ok) {
         console.error("Failed to delete instance:", response.status);
+        addToast({ type: "error", message: "Failed to delete instance" });
       } else if (wasSelected) {
         // If we deleted the currently selected model, switch to another available model
         // Find another instance that isn't the one we just deleted
@@ -2416,368 +2654,1385 @@
   class="relative h-screen w-full flex flex-col bg-exo-dark-gray overflow-hidden"
 >
   <!-- Scanline overlay -->
-  <div
-    class="fixed inset-0 pointer-events-none z-50 scanlines opacity-20"
-  ></div>
-
-  <!-- Shooting Stars Background - one every ~15s -->
-  <div class="shooting-stars">
+  {#if !showOnboarding}
     <div
-      class="shooting-star"
-      style="top: 10%; left: 20%; --duration: 45s; --delay: 0s;"
+      class="fixed inset-0 pointer-events-none z-50 scanlines opacity-20"
     ></div>
-    <div
-      class="shooting-star"
-      style="top: 30%; left: 65%; --duration: 45s; --delay: 15s;"
-    ></div>
-    <div
-      class="shooting-star"
-      style="top: 50%; left: 40%; --duration: 45s; --delay: 30s;"
-    ></div>
-  </div>
-
-  {#if !topologyOnlyEnabled}
-    <HeaderNav
-      showHome={chatStarted}
-      onHome={handleGoHome}
-      showSidebarToggle={true}
-      {sidebarVisible}
-      onToggleSidebar={toggleChatSidebarVisible}
-    />
-  {/if}
-
-  <!-- Main Content -->
-  <main class="flex-1 flex overflow-hidden relative">
-    <!-- Left: Conversation History Sidebar (hidden in topology-only mode or when toggled off) -->
-    {#if !topologyOnlyEnabled && sidebarVisible}
-      <div class="w-80 flex-shrink-0 border-r border-exo-yellow/10">
-        <ChatSidebar class="h-full" />
-      </div>
-    {/if}
 
-    {#if topologyOnlyEnabled}
-      <!-- TOPOLOGY ONLY MODE: Full-screen topology -->
+    <!-- Shooting Stars Background - one every ~15s -->
+    <div class="shooting-stars">
+      <div
+        class="shooting-star"
+        style="top: 10%; left: 20%; --duration: 45s; --delay: 0s;"
+      ></div>
       <div
-        class="flex-1 flex flex-col min-h-0 min-w-0 p-4"
-        in:fade={{ duration: 300 }}
-      >
+        class="shooting-star"
+        style="top: 30%; left: 65%; --duration: 45s; --delay: 15s;"
+      ></div>
+      <div
+        class="shooting-star"
+        style="top: 50%; left: 40%; --duration: 45s; --delay: 30s;"
+      ></div>
+    </div>
+  {/if}
+
+  {#if showOnboarding}
+    <!-- ═══════════════════════════════════════════════════════ -->
+    <!-- FULL-SCREEN ONBOARDING WIZARD                          -->
+    <!-- ═══════════════════════════════════════════════════════ -->
+    <div class="flex-1 flex items-center justify-center relative z-10 bg-white">
+      {#if onboardingStep === 1}
+        <!-- Step 1: Welcome -->
         <div
-          class="flex-1 relative bg-exo-dark-gray/40 rounded-lg overflow-hidden"
+          class="text-center max-w-lg px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
         >
-          <TopologyGraph
-            class="w-full h-full"
-            highlightedNodes={highlightedNodes()}
-            filteredNodes={nodeFilter}
-            onNodeClick={togglePreviewNodeFilter}
-          />
-
-          {@render clusterWarnings()}
-
-          <!-- TB5 RDMA Available Info -->
-          {#if tb5WithoutRdma && !tb5InfoDismissed}
+          <div class="mb-8">
             <div
-              class="absolute left-4 flex items-center gap-2 px-3 py-2 rounded border border-blue-400/50 bg-blue-400/10 backdrop-blur-sm"
-              class:top-16={tbBridgeCycles.length > 0}
-              class:top-4={tbBridgeCycles.length === 0}
-              role="status"
+              class="text-5xl font-mono font-bold text-exo-yellow tracking-wider mb-4"
             >
-              <svg
-                class="w-5 h-5 text-blue-400 flex-shrink-0"
-                fill="none"
-                viewBox="0 0 24 24"
-                stroke="currentColor"
-                stroke-width="2"
-              >
-                <path
-                  stroke-linecap="round"
-                  stroke-linejoin="round"
-                  d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
-                />
-              </svg>
-              <span class="text-sm font-mono text-blue-200">
-                RDMA AVAILABLE
-              </span>
-              <button
-                type="button"
-                onclick={() => (tb5InfoDismissed = true)}
-                class="ml-1 text-blue-300/60 hover:text-blue-200 transition-colors cursor-pointer"
-                title="Dismiss"
-              >
-                <svg
-                  class="w-4 h-4"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                  stroke-width="2"
-                >
-                  <path
-                    stroke-linecap="round"
-                    stroke-linejoin="round"
-                    d="M6 18L18 6M6 6l12 12"
-                  />
-                </svg>
-              </button>
+              exo
             </div>
-          {/if}
-
-          <!-- Exit topology-only mode button -->
+            <h1
+              class="text-2xl font-sans font-light text-gray-900 mb-3 tracking-wide"
+            >
+              Welcome to exo
+            </h1>
+            <p class="text-base font-sans text-gray-500 leading-relaxed">
+              Run AI models locally, across all your devices.
+            </p>
+          </div>
           <button
             type="button"
-            onclick={toggleTopologyOnlyMode}
-            class="absolute bottom-4 right-4 p-2 rounded border border-exo-yellow/30 bg-exo-dark-gray/80 hover:border-exo-yellow/50 hover:bg-exo-dark-gray transition-colors cursor-pointer backdrop-blur-sm"
-            title="Exit topology only mode"
+            onclick={() => (onboardingStep = 2)}
+            class="inline-flex items-center gap-2 px-8 py-3 bg-exo-yellow text-exo-black font-sans text-sm font-semibold rounded-full hover:brightness-110 hover:shadow-[0_0_24px_rgba(255,215,0,0.2)] transition-all duration-200 cursor-pointer"
           >
+            Get Started
             <svg
-              class="w-5 h-5 text-exo-yellow"
+              class="w-4 h-4"
               fill="none"
               viewBox="0 0 24 24"
               stroke="currentColor"
-              stroke-width="2"
+              stroke-width="2.5"
             >
-              <circle cx="12" cy="5" r="2" fill="currentColor" />
-              <circle cx="5" cy="19" r="2" fill="currentColor" />
-              <circle cx="19" cy="19" r="2" fill="currentColor" />
-              <path stroke-linecap="round" d="M12 7v5m0 0l-5 5m5-5l5 5" />
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M13 7l5 5m0 0l-5 5m5-5H6"
+              />
             </svg>
           </button>
         </div>
-      </div>
-    {:else if !chatStarted}
-      <!-- WELCOME STATE: Topology + Instance Controls (no left sidebar for cleaner look) -->
-      <div
-        class="flex-1 flex overflow-visible relative"
-        in:fade={{ duration: 300 }}
-        out:fade={{ duration: 200 }}
-      >
-        <!-- Center: MAIN TOPOLOGY DISPLAY -->
-        <div class="flex-1 flex flex-col min-h-0 min-w-0 py-4">
-          <!-- Topology Container - Takes most of the space -->
-          <div
-            class="flex-1 relative bg-exo-dark-gray/40 mx-4 mb-4 rounded-lg overflow-hidden"
-          >
-            <!-- The main topology graph - full container -->
-            <TopologyGraph
-              class="w-full h-full"
-              highlightedNodes={highlightedNodes()}
-              filteredNodes={nodeFilter}
-              onNodeClick={togglePreviewNodeFilter}
-            />
-
-            {@render clusterWarnings()}
+      {:else if onboardingStep === 2}
+        <!-- Step 2: Add more devices, run bigger models -->
+        <div
+          class="flex flex-col items-center w-full max-w-2xl px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
+        >
+          <div class="text-center mb-4">
+            <h1
+              class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
+            >
+              Add more devices, run bigger models
+            </h1>
+          </div>
 
-            <!-- TB5 RDMA Available Info -->
-            {#if tb5WithoutRdma && !tb5InfoDismissed}
-              <div
-                class="absolute left-4 group"
-                class:top-16={tbBridgeCycles.length > 0}
-                class:top-4={tbBridgeCycles.length === 0}
-                role="status"
-              >
-                <div
-                  class="flex items-center gap-2 px-3 py-2 rounded border border-blue-400/50 bg-blue-400/10 backdrop-blur-sm"
+          <!-- Animation stage -->
+          <div class="relative w-full" style="height: 380px;">
+            <svg
+              viewBox="0 0 600 380"
+              class="w-full h-full"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <defs>
+                <filter
+                  id="onb-gold-glow"
+                  x="-50%"
+                  y="-50%"
+                  width="200%"
+                  height="200%"
                 >
-                  <svg
-                    class="w-5 h-5 text-blue-400 flex-shrink-0"
+                  <feGaussianBlur stdDeviation="4" result="blur" />
+                  <feFlood
+                    flood-color="#FFD700"
+                    flood-opacity="0.6"
+                    result="color"
+                  />
+                  <feComposite
+                    in="color"
+                    in2="blur"
+                    operator="in"
+                    result="glow"
+                  />
+                  <feMerge>
+                    <feMergeNode in="glow" />
+                    <feMergeNode in="SourceGraphic" />
+                  </feMerge>
+                </filter>
+                <filter
+                  id="onb-device-glow"
+                  x="-50%"
+                  y="-50%"
+                  width="200%"
+                  height="200%"
+                >
+                  <feGaussianBlur stdDeviation="4" result="blur" />
+                  <feFlood
+                    flood-color="#FFD700"
+                    flood-opacity="0.3"
+                    result="color"
+                  />
+                  <feComposite
+                    in="color"
+                    in2="blur"
+                    operator="in"
+                    result="glow"
+                  />
+                  <feMerge>
+                    <feMergeNode in="glow" />
+                    <feMergeNode in="SourceGraphic" />
+                  </feMerge>
+                </filter>
+              </defs>
+
+              <!-- MacBook Device (left side) -->
+              {#if deviceAnimPhase >= 1}
+                <g transform="translate(135, 30)" in:fade={{ duration: 600 }}>
+                  <!-- Screen bezel -->
+                  <rect
+                    x="0"
+                    y="0"
+                    width="84"
+                    height="56"
+                    rx="4"
                     fill="none"
-                    viewBox="0 0 24 24"
-                    stroke="currentColor"
-                    stroke-width="2"
+                    stroke="#FFD700"
+                    stroke-width="1.5"
+                    filter="url(#onb-device-glow)"
+                  />
+                  <!-- Screen interior -->
+                  <rect
+                    x="4"
+                    y="3"
+                    width="76"
+                    height="48"
+                    rx="2"
+                    fill="#0a0a0a"
+                  />
+                  <!-- Memory bar inside screen -->
+                  <rect
+                    x="10"
+                    y="36"
+                    width="30"
+                    height="8"
+                    rx="2"
+                    fill="#374151"
+                  />
+                  <rect
+                    x="10"
+                    y="36"
+                    width="18"
+                    height="8"
+                    rx="2"
+                    fill="rgba(255,215,0,0.4)"
+                  />
+                  <!-- Keyboard base -->
+                  <path
+                    d="M -6 60 L 90 60 L 84 70 L 0 70 Z"
+                    fill="none"
+                    stroke="#FFD700"
+                    stroke-width="1.5"
+                  />
+                  <!-- Label -->
+                  <text
+                    x="42"
+                    y="92"
+                    text-anchor="middle"
+                    fill="rgba(0,0,0,0.7)"
+                    style="font-size: 13px; font-family: system-ui, sans-serif;"
                   >
-                    <path
-                      stroke-linecap="round"
-                      stroke-linejoin="round"
-                      d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
-                    />
-                  </svg>
-                  <span class="text-sm font-mono text-blue-200">
-                    RDMA AVAILABLE
-                  </span>
-                  <button
-                    type="button"
-                    onclick={() => (tb5InfoDismissed = true)}
-                    class="ml-1 text-blue-300/60 hover:text-blue-200 transition-colors cursor-pointer"
-                    title="Dismiss"
+                    MacBook Pro
+                  </text>
+                  <text
+                    x="42"
+                    y="108"
+                    text-anchor="middle"
+                    fill="rgba(0,0,0,0.35)"
+                    style="font-size: 11px; font-family: 'SF Mono', monospace;"
                   >
-                    <svg
-                      class="w-4 h-4"
-                      fill="none"
-                      viewBox="0 0 24 24"
-                      stroke="currentColor"
-                      stroke-width="2"
-                    >
-                      <path
-                        stroke-linecap="round"
-                        stroke-linejoin="round"
-                        d="M6 18L18 6M6 6l12 12"
-                      />
-                    </svg>
-                  </button>
-                </div>
+                    36 GB
+                  </text>
+                </g>
+              {/if}
 
-                <!-- Tooltip on hover -->
-                <div
-                  class="absolute top-full left-0 mt-2 w-80 p-3 rounded border border-blue-400/30 bg-exo-dark-gray/95 backdrop-blur-sm opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 shadow-lg"
+              <!-- Mac Studio Device (right side) - tweened fly-in -->
+              <g transform="translate({$studioX}, 30)" opacity={$studioOpacity}>
+                <!-- Studio box -->
+                <rect
+                  x="0"
+                  y="0"
+                  width="78"
+                  height="62"
+                  rx="6"
+                  fill="none"
+                  stroke="#FFD700"
+                  stroke-width="1.5"
+                  filter="url(#onb-device-glow)"
+                />
+                <!-- Inner face -->
+                <rect
+                  x="5"
+                  y="5"
+                  width="68"
+                  height="52"
+                  rx="4"
+                  fill="#0a0a0a"
+                />
+                <!-- Memory bar inside -->
+                <rect
+                  x="12"
+                  y="40"
+                  width="54"
+                  height="8"
+                  rx="2"
+                  fill="#374151"
+                />
+                <rect
+                  x="12"
+                  y="40"
+                  width="42"
+                  height="8"
+                  rx="2"
+                  fill="rgba(255,215,0,0.4)"
+                />
+                <!-- Front circle detail -->
+                <circle
+                  cx="39"
+                  cy="56"
+                  r="3.5"
+                  fill="none"
+                  stroke="rgba(255,215,0,0.4)"
+                  stroke-width="1"
+                />
+                <!-- Label -->
+                <text
+                  x="39"
+                  y="86"
+                  text-anchor="middle"
+                  fill="rgba(0,0,0,0.7)"
+                  style="font-size: 13px; font-family: system-ui, sans-serif;"
                 >
-                  <p class="text-xs text-white/80 mb-2">
-                    Thunderbolt 5 hardware detected on multiple nodes. Enable
-                    RDMA for significantly faster inter-node communication.
-                  </p>
-                  <p class="text-xs text-white/60 mb-1.5">
-                    <span class="text-blue-300">To enable:</span>
-                  </p>
-                  <ol
-                    class="text-xs text-white/60 list-decimal list-inside space-y-0.5 mb-1.5"
-                  >
-                    <li>Connect nodes with TB5 cables</li>
-                    <li>Boot to Recovery (hold power 10s → Options)</li>
-                    <li>
-                      Run
-                      <code class="text-blue-300 bg-blue-400/10 px-1 rounded"
-                        >rdma_ctl enable</code
-                      >
-                    </li>
-                    <li>Reboot</li>
-                  </ol>
-                  <p class="text-xs text-white/40">
-                    Requires macOS 26.2+, TB5 cables, and matching OS versions.
-                  </p>
-                </div>
-              </div>
-            {/if}
+                  Mac Studio
+                </text>
+                <text
+                  x="39"
+                  y="102"
+                  text-anchor="middle"
+                  fill="rgba(0,0,0,0.35)"
+                  style="font-size: 11px; font-family: 'SF Mono', monospace;"
+                >
+                  192 GB
+                </text>
+              </g>
+
+              <!-- Connection line between devices -->
+              {#if deviceAnimPhase >= 3}
+                <line
+                  x1="223"
+                  y1="62"
+                  x2="340"
+                  y2="62"
+                  class="onboarding-connection-line"
+                  in:fade={{ duration: 400 }}
+                />
+              {/if}
 
-            <!-- Node Filter Indicator (top-right corner) -->
-            {#if isFilterActive()}
-              <button
-                onclick={clearPreviewNodeFilter}
-                class="absolute top-2 right-2 flex items-center gap-1.5 px-2 py-1 bg-exo-dark-gray/80 border border-exo-yellow/40 rounded text-exo-yellow hover:border-exo-yellow/60 transition-colors cursor-pointer backdrop-blur-sm"
-                title="Clear filter"
-              >
-                <span class="text-[10px] font-mono tracking-wider">
-                  FILTER: {nodeFilter.size}
-                </span>
-                <svg
-                  class="w-3 h-3"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                  stroke-width="2"
+              <!-- Combined memory label -->
+              {#if deviceAnimPhase >= 3}
+                <text
+                  x="282"
+                  y="50"
+                  text-anchor="middle"
+                  fill="rgba(255,215,0,0.7)"
+                  style="font-size: 11px; font-family: 'SF Mono', monospace;"
+                  in:fade={{ duration: 400 }}
                 >
-                  <path
-                    stroke-linecap="round"
-                    stroke-linejoin="round"
-                    d="M6 18L18 6M6 6l12 12"
+                  228 GB combined
+                </text>
+              {/if}
+
+              <!-- Model cards row -->
+              <g transform="translate(0, 200)">
+                <!-- Arrows from devices to models -->
+                {#if deviceAnimPhase >= 3}
+                  <line
+                    x1="177"
+                    y1="-50"
+                    x2="177"
+                    y2="-8"
+                    stroke="rgba(255,215,0,0.15)"
+                    stroke-width="1"
+                    stroke-dasharray="4,4"
+                    in:fade={{ duration: 300 }}
                   />
-                </svg>
-              </button>
-            {/if}
-          </div>
+                  <line
+                    x1="379"
+                    y1="-50"
+                    x2="379"
+                    y2="-8"
+                    stroke="rgba(255,215,0,0.15)"
+                    stroke-width="1"
+                    stroke-dasharray="4,4"
+                    in:fade={{ duration: 300 }}
+                  />
+                {/if}
 
-          <!-- Chat Input - Below topology -->
-          <div class="px-4 pt-6 pb-8">
-            <div class="max-w-3xl mx-auto">
-              <ChatForm
-                placeholder="Ask anything"
-                showHelperText={false}
-                showModelSelector={true}
-                modelTasks={modelTasks()}
-                modelCapabilities={modelCapabilities()}
-              />
-            </div>
-          </div>
-        </div>
+                <!-- Small model: 8B (visible from phase 1) -->
+                {#if deviceAnimPhase >= 1}
+                  <g transform="translate(55, 0)" in:fade={{ duration: 500 }}>
+                    <rect
+                      x="0"
+                      y="0"
+                      width="110"
+                      height="48"
+                      rx="8"
+                      fill="rgba(0,0,0,0.04)"
+                      stroke="rgba(0,0,0,0.1)"
+                      stroke-width="1"
+                    />
+                    <text
+                      x="55"
+                      y="20"
+                      text-anchor="middle"
+                      fill="rgba(0,0,0,0.7)"
+                      style="font-size: 13px; font-family: system-ui, sans-serif;"
+                    >
+                      Qwen3 8B
+                    </text>
+                    <text
+                      x="55"
+                      y="37"
+                      text-anchor="middle"
+                      fill="rgba(0,0,0,0.35)"
+                      style="font-size: 11px; font-family: 'SF Mono', monospace;"
+                    >
+                      4 GB
+                    </text>
+                  </g>
+                {/if}
 
-        <!-- Right Sidebar: Instance Controls (wider on welcome page for better visibility) -->
-        <aside
-          class="w-80 border-l border-exo-yellow/10 bg-exo-dark-gray flex flex-col flex-shrink-0"
-        >
-          <!-- Running Instances Panel (only shown when instances exist) - Scrollable -->
-          {#if instanceCount > 0}
-            <div class="p-4 flex-shrink-0">
-              <!-- Panel Header -->
-              <div class="flex items-center gap-2 mb-4">
-                <div
-                  class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
-                ></div>
-                <h3
-                  class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
-                >
-                  Instances
-                </h3>
-                <div
-                  class="flex-1 h-px bg-gradient-to-r from-exo-yellow/30 to-transparent"
-                ></div>
-              </div>
+                <!-- Medium model: 30B (locked → unlocks at phase 3) -->
+                {#if deviceAnimPhase >= 1}
+                  <g
+                    transform="translate(185, 0)"
+                    in:fade={{ duration: 500, delay: 200 }}
+                  >
+                    <rect
+                      x="0"
+                      y="0"
+                      width="110"
+                      height="48"
+                      rx="8"
+                      fill={deviceAnimPhase >= 3
+                        ? "rgba(255,215,0,0.06)"
+                        : "rgba(0,0,0,0.03)"}
+                      stroke={deviceAnimPhase >= 3
+                        ? "rgba(255,215,0,0.35)"
+                        : "rgba(0,0,0,0.08)"}
+                      stroke-width="1"
+                      filter={deviceAnimPhase >= 3
+                        ? "url(#onb-gold-glow)"
+                        : "none"}
+                      style="transition: fill 500ms, stroke 500ms, filter 500ms;"
+                    />
+                    {#if deviceAnimPhase < 3}
+                      <g transform="translate(48, 12)">
+                        <rect
+                          x="0"
+                          y="9"
+                          width="14"
+                          height="11"
+                          rx="2"
+                          fill="none"
+                          stroke="rgba(0,0,0,0.2)"
+                          stroke-width="1"
+                        />
+                        <path
+                          d="M 2 9 V 6 C 2 3 4.5 1 7 1 C 9.5 1 12 3 12 6 V 9"
+                          fill="none"
+                          stroke="rgba(0,0,0,0.2)"
+                          stroke-width="1"
+                        />
+                      </g>
+                    {:else}
+                      <text
+                        x="55"
+                        y="20"
+                        text-anchor="middle"
+                        fill="#FFD700"
+                        style="font-size: 13px; font-family: system-ui, sans-serif;"
+                      >
+                        Qwen3 30B
+                      </text>
+                      <text
+                        x="55"
+                        y="37"
+                        text-anchor="middle"
+                        fill="rgba(255,215,0,0.6)"
+                        style="font-size: 11px; font-family: 'SF Mono', monospace;"
+                      >
+                        16 GB
+                      </text>
+                    {/if}
+                  </g>
+                {/if}
 
-              <div
-                bind:this={instancesContainerRef}
-                class="max-h-72 xl:max-h-96 space-y-3 overflow-y-auto overflow-x-hidden py-px"
-              >
-                {#each unifiedDisplayItems as item (item.id)}
-                  {@const id = item.id}
-                  {@const instance = item.instance}
-                  {@const downloadInfo = instance
-                    ? getInstanceDownloadStatus(item.instanceId ?? id, instance)
-                    : getMetaInstancePlacingStatus(id)}
-                  {@const metaData = item.isMetaInstance
-                    ? metaInstancesData[id]
-                    : null}
-                  {@const retryError =
-                    metaData?.lastFailureError && !downloadInfo.isFailed
-                      ? metaData.consecutiveFailures > 0
-                        ? `(${((metaData.consecutiveFailures - 1) % 3) + 1}/3) ${metaData.lastFailureError}`
-                        : metaData.lastFailureError
-                      : null}
-                  {@const statusText = downloadInfo.statusText}
-                  {@const isDownloading = downloadInfo.isDownloading}
-                  {@const isFailed =
-                    statusText === "FAILED" ||
-                    statusText === "PLACEMENT FAILED"}
-                  {@const isLoading =
-                    statusText.startsWith("LOADING") ||
-                    statusText.startsWith("WARMING UP") ||
-                    statusText === "WAITING" ||
-                    statusText === "PLACING" ||
-                    statusText.startsWith("RETRYING")}
-                  {@const isReady =
-                    statusText === "READY" || statusText === "LOADED"}
-                  {@const isRunning = statusText === "RUNNING"}
-                  <!-- Instance Card -->
-                  {@const instanceModelId = item.modelId}
-                  {@const instanceInfo = instance
-                    ? getInstanceInfo(instance)
-                    : {
-                        instanceType:
-                          item.instanceMeta === "MlxRing"
-                            ? "MLX Ring"
-                            : item.instanceMeta === "MlxJaccl"
-                              ? "MLX RDMA"
-                              : "Unknown",
-                        sharding: item.sharding ?? "Unknown",
-                        nodeNames: [] as string[],
-                        nodeIds: [] as string[],
-                        nodeCount: 0,
-                      }}
-                  {@const instanceConnections = instance
-                    ? getInstanceConnections(instance)
-                    : []}
+                <!-- Large model: 72B (locked → unlocks at phase 4) -->
+                {#if deviceAnimPhase >= 1}
+                  <g
+                    transform="translate(315, 0)"
+                    in:fade={{ duration: 500, delay: 400 }}
+                  >
+                    <rect
+                      x="0"
+                      y="0"
+                      width="110"
+                      height="48"
+                      rx="8"
+                      fill={deviceAnimPhase >= 4
+                        ? "rgba(255,215,0,0.06)"
+                        : "rgba(0,0,0,0.03)"}
+                      stroke={deviceAnimPhase >= 4
+                        ? "rgba(255,215,0,0.35)"
+                        : "rgba(0,0,0,0.08)"}
+                      stroke-width="1"
+                      filter={deviceAnimPhase >= 4
+                        ? "url(#onb-gold-glow)"
+                        : "none"}
+                      style="transition: fill 500ms, stroke 500ms, filter 500ms;"
+                    />
+                    {#if deviceAnimPhase < 4}
+                      <g transform="translate(48, 12)">
+                        <rect
+                          x="0"
+                          y="9"
+                          width="14"
+                          height="11"
+                          rx="2"
+                          fill="none"
+                          stroke="rgba(0,0,0,0.2)"
+                          stroke-width="1"
+                        />
+                        <path
+                          d="M 2 9 V 6 C 2 3 4.5 1 7 1 C 9.5 1 12 3 12 6 V 9"
+                          fill="none"
+                          stroke="rgba(0,0,0,0.2)"
+                          stroke-width="1"
+                        />
+                      </g>
+                    {:else}
+                      <text
+                        x="55"
+                        y="20"
+                        text-anchor="middle"
+                        fill="#FFD700"
+                        style="font-size: 13px; font-family: system-ui, sans-serif;"
+                      >
+                        Llama 72B
+                      </text>
+                      <text
+                        x="55"
+                        y="37"
+                        text-anchor="middle"
+                        fill="rgba(255,215,0,0.6)"
+                        style="font-size: 11px; font-family: 'SF Mono', monospace;"
+                      >
+                        36 GB
+                      </text>
+                    {/if}
+                  </g>
+                {/if}
+
+                <!-- Extra large model: 405B (locked → unlocks at phase 4 with extra glow) -->
+                {#if deviceAnimPhase >= 1}
+                  <g
+                    transform="translate(445, 0)"
+                    in:fade={{ duration: 500, delay: 600 }}
+                  >
+                    <rect
+                      x="0"
+                      y="0"
+                      width="110"
+                      height="48"
+                      rx="8"
+                      fill={deviceAnimPhase >= 4
+                        ? "rgba(255,215,0,0.08)"
+                        : "rgba(0,0,0,0.03)"}
+                      stroke={deviceAnimPhase >= 4
+                        ? "rgba(255,215,0,0.45)"
+                        : "rgba(0,0,0,0.08)"}
+                      stroke-width={deviceAnimPhase >= 4 ? "1.5" : "1"}
+                      filter={deviceAnimPhase >= 4
+                        ? "url(#onb-gold-glow)"
+                        : "none"}
+                      style="transition: fill 700ms, stroke 700ms, filter 700ms, stroke-width 700ms;"
+                    />
+                    {#if deviceAnimPhase < 4}
+                      <g transform="translate(48, 12)">
+                        <rect
+                          x="0"
+                          y="9"
+                          width="14"
+                          height="11"
+                          rx="2"
+                          fill="none"
+                          stroke="rgba(0,0,0,0.2)"
+                          stroke-width="1"
+                        />
+                        <path
+                          d="M 2 9 V 6 C 2 3 4.5 1 7 1 C 9.5 1 12 3 12 6 V 9"
+                          fill="none"
+                          stroke="rgba(0,0,0,0.2)"
+                          stroke-width="1"
+                        />
+                      </g>
+                    {:else}
+                      <text
+                        x="55"
+                        y="20"
+                        text-anchor="middle"
+                        fill="#FFD700"
+                        style="font-size: 13px; font-family: system-ui, sans-serif; font-weight: 600;"
+                      >
+                        Llama 405B
+                      </text>
+                      <text
+                        x="55"
+                        y="37"
+                        text-anchor="middle"
+                        fill="rgba(255,215,0,0.6)"
+                        style="font-size: 11px; font-family: 'SF Mono', monospace;"
+                      >
+                        203 GB
+                      </text>
+                    {/if}
+                  </g>
+                {/if}
+
+                <!-- "Models you can run" label -->
+                {#if deviceAnimPhase >= 1}
+                  <text
+                    x="300"
+                    y="72"
+                    text-anchor="middle"
+                    fill="rgba(0,0,0,0.3)"
+                    style="font-size: 11px; font-family: system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.12em;"
+                    in:fade={{ duration: 400 }}
+                  >
+                    Models you can run
+                  </text>
+                {/if}
+              </g>
+            </svg>
+          </div>
+
+          <!-- Continue button -->
+          {#if showContinueStep2}
+            <button
+              type="button"
+              onclick={() => (onboardingStep = 3)}
+              class="inline-flex items-center gap-2 px-8 py-3 mt-2 bg-exo-yellow text-exo-black font-sans text-sm font-semibold rounded-full hover:brightness-110 hover:shadow-[0_0_24px_rgba(255,215,0,0.2)] transition-all duration-200 cursor-pointer"
+              in:fade={{ duration: 400 }}
+            >
+              Continue
+              <svg
+                class="w-4 h-4"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+                stroke-width="2.5"
+              >
+                <path
+                  stroke-linecap="round"
+                  stroke-linejoin="round"
+                  d="M13 7l5 5m0 0l-5 5m5-5H6"
+                />
+              </svg>
+            </button>
+          {/if}
+        </div>
+      {:else if onboardingStep === 3}
+        <!-- Step 3: Your Devices -->
+        <div
+          class="flex flex-col items-center w-full max-w-4xl px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
+        >
+          <div class="text-center mb-6">
+            <h1
+              class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
+            >
+              Your devices
+            </h1>
+            <p class="text-sm font-sans text-gray-500">
+              {nodeCount} device{nodeCount !== 1 ? "s" : ""} connected
+              {#if clusterTotalMemoryGB() > 0}
+                · {clusterTotalMemoryGB().toFixed(0)} GB total memory
+              {/if}
+            </p>
+          </div>
+          <div
+            class="w-full h-80 bg-exo-dark-gray rounded-lg overflow-hidden border border-gray-200 mb-8"
+          >
+            <TopologyGraph
+              class="w-full h-full"
+              highlightedNodes={highlightedNodes()}
+              filteredNodes={nodeFilter}
+              onNodeClick={togglePreviewNodeFilter}
+            />
+          </div>
+          <p
+            class="text-sm font-sans text-gray-400 mt-2 max-w-md text-center leading-relaxed"
+          >
+            Install exo on more devices on your network to combine their power —
+            they connect automatically.
+          </p>
+          <button
+            type="button"
+            onclick={() => (onboardingStep = 4)}
+            class="inline-flex items-center gap-2 px-8 py-3 mt-6 bg-exo-yellow text-exo-black font-sans text-sm font-semibold rounded-full hover:brightness-110 hover:shadow-[0_0_24px_rgba(255,215,0,0.2)] transition-all duration-200 cursor-pointer"
+          >
+            Continue
+            <svg
+              class="w-4 h-4"
+              fill="none"
+              viewBox="0 0 24 24"
+              stroke="currentColor"
+              stroke-width="2.5"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M13 7l5 5m0 0l-5 5m5-5H6"
+              />
+            </svg>
+          </button>
+        </div>
+      {:else if onboardingStep === 4}
+        <!-- Step 4: Choose a Model -->
+        <div
+          class="flex flex-col items-center w-full max-w-2xl px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
+        >
+          <div class="text-center mb-8">
+            <h1
+              class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
+            >
+              Choose a model
+            </h1>
+            <p class="text-sm font-sans text-gray-500">
+              Pick a model to download and run locally.
+            </p>
+          </div>
+
+          {#if onboardingError}
+            <div
+              class="w-full mb-6 px-4 py-3 rounded-lg border border-red-200 bg-red-50 text-sm font-mono text-red-600"
+              in:fade={{ duration: 200 }}
+            >
+              {onboardingError}
+            </div>
+          {/if}
+
+          {#if onboardingModels.length === 0}
+            <div class="text-center py-8">
+              <div class="text-sm text-gray-400 font-sans animate-pulse">
+                Loading models...
+              </div>
+            </div>
+          {:else}
+            <div class="w-full space-y-3 mb-8">
+              {#each onboardingModels as model}
+                {@const sizeGB = getModelSizeGB(model)}
+                {@const fitsNow = hasEnoughMemory(model)}
+                {@const tags = modelTags()[model.id] || []}
+                <button
+                  type="button"
+                  onclick={() => onboardingLaunchModel(model.id)}
+                  class="w-full flex items-center justify-between gap-4 px-5 py-4 rounded-xl border transition-all duration-200 cursor-pointer {fitsNow
+                    ? 'border-gray-200 bg-gray-50 hover:border-exo-yellow/50 hover:bg-yellow-50/50'
+                    : 'border-gray-200 bg-gray-50/50 hover:border-gray-300 opacity-60'}"
+                >
+                  <div class="flex flex-col items-start gap-1 min-w-0">
+                    <div class="flex items-center gap-2">
+                      <span
+                        class="text-sm font-sans font-medium text-gray-900 truncate"
+                        >{model.name || model.id}</span
+                      >
+                      {#each tags as tag}
+                        <span
+                          class="text-[10px] font-sans font-medium px-1.5 py-0.5 rounded-full bg-exo-yellow/10 text-exo-yellow/80"
+                          >{tag}</span
+                        >
+                      {/each}
+                    </div>
+                    <span class="text-xs font-mono text-gray-400 truncate"
+                      >{model.id}</span
+                    >
+                  </div>
+                  <div class="flex items-center gap-3 flex-shrink-0">
+                    <span class="text-xs font-mono text-gray-500"
+                      >{sizeGB >= 1 ? sizeGB.toFixed(0) : sizeGB.toFixed(1)} GB</span
+                    >
+                    <svg
+                      class="w-4 h-4 text-gray-400"
+                      fill="none"
+                      viewBox="0 0 24 24"
+                      stroke="currentColor"
+                      stroke-width="2"
+                    >
+                      <path
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                        d="M9 5l7 7-7 7"
+                      />
+                    </svg>
+                  </div>
+                </button>
+              {/each}
+            </div>
+          {/if}
+
+          <button
+            type="button"
+            onclick={() => {
+              isModelPickerOpen = true;
+            }}
+            class="text-sm font-sans text-gray-400 hover:text-exo-yellow transition-colors cursor-pointer underline underline-offset-4 decoration-gray-300 hover:decoration-exo-yellow/30"
+          >
+            Browse all models
+          </button>
+        </div>
+      {:else if onboardingStep === 5}
+        <!-- Step 5: Downloading -->
+        <div
+          class="text-center max-w-lg px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
+        >
+          <div class="mb-8">
+            <h1
+              class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
+            >
+              Downloading
+            </h1>
+            <p class="text-sm text-gray-500">
+              {#if onboardingModelId}
+                <span class="text-exo-yellow">{onboardingModelId}</span>
+              {/if}
+            </p>
+          </div>
+
+          {#if onboardingDownloadProgress}
+            <div class="w-full max-w-md mx-auto space-y-4">
+              <div
+                class="relative h-2 bg-gray-200 rounded-full overflow-hidden"
+              >
+                <div
+                  class="absolute inset-y-0 left-0 bg-gradient-to-r from-exo-yellow to-amber-400 rounded-full transition-all duration-500"
+                  style="width: {onboardingDownloadProgress.percentage}%"
+                ></div>
+              </div>
+              <div class="flex justify-between text-xs font-mono text-gray-500">
+                <span>{onboardingDownloadProgress.percentage.toFixed(1)}%</span>
+                <span
+                  >{formatBytes(onboardingDownloadProgress.downloadedBytes)} /
+                  {formatBytes(onboardingDownloadProgress.totalBytes)}</span
+                >
+              </div>
+              <div class="flex justify-between text-xs font-mono text-gray-400">
+                <span>{formatSpeed(onboardingDownloadProgress.speed)}</span>
+                <span>ETA: {formatEta(onboardingDownloadProgress.etaMs)}</span>
+              </div>
+            </div>
+          {:else}
+            <div class="w-full max-w-md mx-auto">
+              <div
+                class="relative h-2 bg-gray-200 rounded-full overflow-hidden"
+              >
+                <div
+                  class="absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-exo-yellow to-amber-400 rounded-full animate-pulse"
+                ></div>
+              </div>
+              <p class="text-xs font-mono text-gray-400 mt-4">
+                Preparing download...
+              </p>
+            </div>
+          {/if}
+
+          <p class="text-xs font-sans text-gray-300 mt-8">
+            This may take a few minutes depending on your connection.
+          </p>
+        </div>
+      {:else if onboardingStep === 6}
+        <!-- Step 6: Loading into memory -->
+        <div
+          class="text-center max-w-lg px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
+        >
+          <div class="mb-8">
+            <h1
+              class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
+            >
+              Loading into memory
+            </h1>
+            <p class="text-sm text-gray-500">
+              {#if onboardingModelId}
+                <span class="text-exo-yellow">{onboardingModelId}</span>
+              {/if}
+            </p>
+          </div>
+
+          <div class="flex justify-center mb-6">
+            <div
+              class="w-12 h-12 border-2 border-exo-yellow/30 border-t-exo-yellow rounded-full animate-spin"
+            ></div>
+          </div>
+
+          <p class="text-sm text-gray-400 font-sans">Almost ready...</p>
+        </div>
+      {:else if onboardingStep === 7}
+        <!-- Step 7: Chat — centered input auto-appears, first message transitions to dashboard -->
+        <div
+          class="flex flex-col items-center justify-center w-full max-w-2xl px-8"
+          in:fade={{ duration: 400 }}
+          out:fade={{ duration: 200 }}
+        >
+          <!-- Subtle branding -->
+          <div
+            class="text-2xl font-mono text-gray-200 font-bold tracking-wider mb-8"
+          >
+            exo
+          </div>
+
+          <!-- Model name -->
+          {#if onboardingModelId}
+            <p class="text-sm text-gray-400 font-sans mb-4">
+              {onboardingModelId.split("/").pop() ?? onboardingModelId}
+            </p>
+          {/if}
+
+          <!-- Centered ChatForm — first message completes onboarding -->
+          <div class="w-full bg-exo-dark-gray rounded-xl p-4">
+            <ChatForm
+              placeholder="Ask anything"
+              autofocus={true}
+              showHelperText={false}
+              showModelSelector={false}
+              modelTasks={modelTasks()}
+              modelCapabilities={modelCapabilities()}
+              onSend={completeOnboarding}
+            />
+          </div>
+
+          <!-- Suggestion chips -->
+          <div class="flex flex-wrap justify-center gap-2.5 mt-8">
+            {#each ["Write a poem about the ocean", "Explain quantum computing simply", "Help me debug my code", "Tell me a creative story"] as chip}
+              <button
+                type="button"
+                onclick={() => {
+                  sendMessage(chip);
+                  completeOnboarding();
+                }}
+                class="px-4 py-2 rounded-full border border-gray-200 bg-gray-50 text-[13px] font-sans text-gray-500 hover:bg-gray-100 hover:text-gray-700 hover:border-gray-300 transition-all duration-200 cursor-pointer"
+              >
+                {chip}
+              </button>
+            {/each}
+          </div>
+        </div>
+      {/if}
+    </div>
+
+    <!-- Model Picker Modal (available during onboarding step 4) -->
+    {#if onboardingStep === 4}
+      <ModelPickerModal
+        isOpen={isModelPickerOpen}
+        {models}
+        {selectedModelId}
+        favorites={favoritesSet}
+        {recentModelIds}
+        hasRecents={showRecentsTab}
+        existingModelIds={new Set(models.map((m) => m.id))}
+        canModelFit={(modelId) => {
+          const model = models.find((m) => m.id === modelId);
+          return model ? hasEnoughMemory(model) : false;
+        }}
+        getModelFitStatus={(modelId): ModelMemoryFitStatus => {
+          const model = models.find((m) => m.id === modelId);
+          return model ? getModelMemoryFitStatus(model) : "too_large";
+        }}
+        onSelect={(modelId) => {
+          isModelPickerOpen = false;
+          onboardingLaunchModel(modelId);
+        }}
+        onClose={() => (isModelPickerOpen = false)}
+        onToggleFavorite={toggleFavorite}
+        onAddModel={addModelFromPicker}
+        onDeleteModel={deleteCustomModel}
+        totalMemoryGB={clusterMemory().total / (1024 * 1024 * 1024)}
+        usedMemoryGB={clusterMemory().used / (1024 * 1024 * 1024)}
+        {downloadsData}
+        topologyNodes={data?.nodes}
+      />
+    {/if}
+  {:else}
+    <!-- ═══════════════════════════════════════════════════════ -->
+    <!-- MAIN DASHBOARD (shown after onboarding)                -->
+    <!-- ═══════════════════════════════════════════════════════ -->
+    {#if !topologyOnlyEnabled}
+      <HeaderNav
+        showHome={chatStarted}
+        onHome={handleGoHome}
+        showSidebarToggle={true}
+        {sidebarVisible}
+        onToggleSidebar={toggleChatSidebarVisible}
+        downloadProgress={activeDownloadSummary}
+      />
+    {/if}
+
+    <!-- Main Content -->
+    <main class="flex-1 flex overflow-hidden relative">
+      <!-- Left: Conversation History Sidebar (hidden in topology-only mode or when toggled off) -->
+      {#if !topologyOnlyEnabled && sidebarVisible}
+        <div
+          class="w-80 flex-shrink-0 border-r border-exo-yellow/10"
+          role="complementary"
+          aria-label="Conversation history"
+        >
+          <ChatSidebar class="h-full" />
+        </div>
+      {/if}
+
+      {#if topologyOnlyEnabled}
+        <!-- TOPOLOGY ONLY MODE: Full-screen topology -->
+        <div
+          class="flex-1 flex flex-col min-h-0 min-w-0 p-4"
+          in:fade={{ duration: 300 }}
+        >
+          <div
+            class="flex-1 relative bg-exo-dark-gray/40 rounded-lg overflow-hidden"
+          >
+            <TopologyGraph
+              class="w-full h-full"
+              highlightedNodes={highlightedNodes()}
+              filteredNodes={nodeFilter}
+              onNodeClick={togglePreviewNodeFilter}
+            />
+
+            {@render clusterWarnings()}
+
+            <!-- TB5 RDMA Available Info -->
+            {#if tb5WithoutRdma && !tb5InfoDismissed}
+              <div
+                class="absolute left-4 flex items-center gap-2 px-3 py-2 rounded border border-blue-400/50 bg-blue-400/10 backdrop-blur-sm"
+                class:top-16={tbBridgeCycles.length > 0}
+                class:top-4={tbBridgeCycles.length === 0}
+                role="status"
+              >
+                <svg
+                  class="w-5 h-5 text-blue-400 flex-shrink-0"
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke="currentColor"
+                  stroke-width="2"
+                >
+                  <path
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+                  />
+                </svg>
+                <span class="text-sm font-mono text-blue-200">
+                  RDMA AVAILABLE
+                </span>
+                <button
+                  type="button"
+                  onclick={() => (tb5InfoDismissed = true)}
+                  class="ml-1 text-blue-300/60 hover:text-blue-200 transition-colors cursor-pointer"
+                  title="Dismiss"
+                  aria-label="Dismiss RDMA available notification"
+                >
+                  <svg
+                    class="w-4 h-4"
+                    fill="none"
+                    viewBox="0 0 24 24"
+                    stroke="currentColor"
+                    stroke-width="2"
+                  >
+                    <path
+                      stroke-linecap="round"
+                      stroke-linejoin="round"
+                      d="M6 18L18 6M6 6l12 12"
+                    />
+                  </svg>
+                </button>
+              </div>
+            {/if}
+
+            <!-- Exit topology-only mode button -->
+            <button
+              type="button"
+              onclick={toggleTopologyOnlyMode}
+              class="absolute bottom-4 right-4 p-2 rounded border border-exo-yellow/30 bg-exo-dark-gray/80 hover:border-exo-yellow/50 hover:bg-exo-dark-gray transition-colors cursor-pointer backdrop-blur-sm"
+              title="Exit topology only mode"
+              aria-label="Exit topology only mode"
+            >
+              <svg
+                class="w-5 h-5 text-exo-yellow"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+                stroke-width="2"
+              >
+                <circle cx="12" cy="5" r="2" fill="currentColor" />
+                <circle cx="5" cy="19" r="2" fill="currentColor" />
+                <circle cx="19" cy="19" r="2" fill="currentColor" />
+                <path stroke-linecap="round" d="M12 7v5m0 0l-5 5m5-5l5 5" />
+              </svg>
+            </button>
+          </div>
+        </div>
+      {:else if !chatStarted}
+        <!-- WELCOME STATE: Topology + Instance Controls (no left sidebar for cleaner look) -->
+        <div
+          class="flex-1 flex overflow-visible relative"
+          in:fade={{ duration: 300 }}
+          out:fade={{ duration: 200 }}
+        >
+          <!-- Center: MAIN TOPOLOGY DISPLAY -->
+          <div class="flex-1 flex flex-col min-h-0 min-w-0 py-4">
+            <!-- Topology Container - Takes most of the space -->
+            <div
+              class="flex-1 relative bg-exo-dark-gray/40 mx-4 mb-4 rounded-lg overflow-hidden"
+            >
+              <!-- The main topology graph - full container -->
+              <TopologyGraph
+                class="w-full h-full"
+                highlightedNodes={highlightedNodes()}
+                filteredNodes={nodeFilter}
+                onNodeClick={togglePreviewNodeFilter}
+              />
+
+              <!-- Initial loading state before first data fetch -->
+              {#if !update}
+                <div
+                  class="absolute inset-0 flex items-center justify-center bg-exo-dark-gray/80"
+                  in:fade={{ duration: 200 }}
+                  out:fade={{ duration: 300 }}
+                >
+                  <div class="text-center">
+                    <div
+                      class="w-8 h-8 border-2 border-exo-yellow/30 border-t-exo-yellow rounded-full animate-spin mx-auto mb-4"
+                    ></div>
+                    <p
+                      class="text-xs font-mono text-white/40 tracking-wider uppercase"
+                    >
+                      Connecting to cluster&hellip;
+                    </p>
+                  </div>
+                </div>
+              {/if}
+
+              <!-- Welcome overlay - shown when no instances are running -->
+              {#if instanceCount === 0 && update}
+                <div
+                  class="absolute inset-0 flex items-center justify-center pointer-events-none"
+                  in:fade={{ duration: 400, delay: 200 }}
+                >
+                  <div class="text-center pointer-events-auto max-w-lg px-6">
+                    <div class="mb-6">
+                      <div
+                        class="text-2xl font-mono text-exo-yellow font-bold tracking-wide mb-3 glow-text"
+                      >
+                        exo
+                      </div>
+                      <p
+                        class="text-sm font-sans text-white/50 leading-relaxed mb-1"
+                      >
+                        {#if data && Object.keys(data.nodes).length > 1}
+                          {Object.keys(data.nodes).length} devices connected. Choose
+                          a model to start running AI across your cluster.
+                        {:else if data && Object.keys(data.nodes).length === 1}
+                          Your device is ready. Choose a model to start running
+                          AI locally.
+                        {:else}
+                          Waiting for devices to connect&hellip;
+                        {/if}
+                      </p>
+                    </div>
+
+                    <button
+                      type="button"
+                      onclick={() => (isModelPickerOpen = true)}
+                      class="inline-flex items-center gap-2 px-6 py-3 bg-exo-yellow text-exo-black font-sans text-sm font-semibold rounded-full hover:brightness-110 hover:shadow-[0_0_24px_rgba(255,215,0,0.2)] transition-all duration-200 cursor-pointer mb-4"
+                    >
+                      <svg
+                        class="w-5 h-5"
+                        fill="none"
+                        viewBox="0 0 24 24"
+                        stroke="currentColor"
+                        stroke-width="2"
+                      >
+                        <path
+                          stroke-linecap="round"
+                          stroke-linejoin="round"
+                          d="M12 4v16m8-8H4"
+                        />
+                      </svg>
+                      Choose a Model
+                    </button>
+
+                    <!-- Quick hints -->
+                    <div
+                      class="flex items-center justify-center gap-4 text-xs text-white/30 font-mono"
+                    >
+                      <span>models download automatically</span>
+                      <span class="text-white/15">&bull;</span>
+                      <a
+                        href="/#/downloads"
+                        class="hover:text-exo-yellow/60 transition-colors"
+                        >view downloads</a
+                      >
+                    </div>
+                  </div>
+                </div>
+              {/if}
+
+              {@render clusterWarnings()}
+
+              <!-- TB5 RDMA Available Info -->
+              {#if tb5WithoutRdma && !tb5InfoDismissed}
+                <div
+                  class="absolute left-4 group"
+                  class:top-16={tbBridgeCycles.length > 0}
+                  class:top-4={tbBridgeCycles.length === 0}
+                  role="status"
+                >
+                  <div
+                    class="flex items-center gap-2 px-3 py-2 rounded border border-blue-400/50 bg-blue-400/10 backdrop-blur-sm"
+                  >
+                    <svg
+                      class="w-5 h-5 text-blue-400 flex-shrink-0"
+                      fill="none"
+                      viewBox="0 0 24 24"
+                      stroke="currentColor"
+                      stroke-width="2"
+                    >
+                      <path
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                        d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+                      />
+                    </svg>
+                    <span class="text-sm font-mono text-blue-200">
+                      RDMA AVAILABLE
+                    </span>
+                    <button
+                      type="button"
+                      onclick={() => (tb5InfoDismissed = true)}
+                      class="ml-1 text-blue-300/60 hover:text-blue-200 transition-colors cursor-pointer"
+                      title="Dismiss"
+                    >
+                      <svg
+                        class="w-4 h-4"
+                        fill="none"
+                        viewBox="0 0 24 24"
+                        stroke="currentColor"
+                        stroke-width="2"
+                      >
+                        <path
+                          stroke-linecap="round"
+                          stroke-linejoin="round"
+                          d="M6 18L18 6M6 6l12 12"
+                        />
+                      </svg>
+                    </button>
+                  </div>
+
+                  <!-- Tooltip on hover -->
                   <div
-                    class="relative group cursor-pointer"
-                    role="button"
-                    tabindex="0"
-                    onmouseenter={() =>
-                      (hoveredInstanceId = item.instanceId ?? id)}
-                    onmouseleave={() => (hoveredInstanceId = null)}
-                    onclick={() => {
-                      if (
-                        instanceModelId &&
-                        instanceModelId !== "Unknown" &&
-                        instanceModelId !== "Unknown Model"
-                      ) {
-                        setSelectedChatModel(instanceModelId);
-                      }
-                    }}
-                    onkeydown={(e) => {
-                      if (e.key === "Enter" || e.key === " ") {
+                    class="absolute top-full left-0 mt-2 w-80 p-3 rounded border border-blue-400/30 bg-exo-dark-gray/95 backdrop-blur-sm opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 shadow-lg"
+                  >
+                    <p class="text-xs text-white/80 mb-2">
+                      Thunderbolt 5 hardware detected on multiple nodes. Enable
+                      RDMA for significantly faster inter-node communication.
+                    </p>
+                    <p class="text-xs text-white/60 mb-1.5">
+                      <span class="text-blue-300">To enable:</span>
+                    </p>
+                    <ol
+                      class="text-xs text-white/60 list-decimal list-inside space-y-0.5 mb-1.5"
+                    >
+                      <li>Connect nodes with TB5 cables</li>
+                      <li>Boot to Recovery (hold power 10s → Options)</li>
+                      <li>
+                        Run
+                        <code class="text-blue-300 bg-blue-400/10 px-1 rounded"
+                          >rdma_ctl enable</code
+                        >
+                      </li>
+                      <li>Reboot</li>
+                    </ol>
+                    <p class="text-xs text-white/40">
+                      Requires macOS 26.2+, TB5 cables, and matching OS
+                      versions.
+                    </p>
+                  </div>
+                </div>
+              {/if}
+
+              <!-- Node Filter Indicator (top-right corner) -->
+              {#if isFilterActive()}
+                <button
+                  onclick={clearPreviewNodeFilter}
+                  class="absolute top-2 right-2 flex items-center gap-1.5 px-2 py-1 bg-exo-dark-gray/80 border border-exo-yellow/40 rounded text-exo-yellow hover:border-exo-yellow/60 transition-colors cursor-pointer backdrop-blur-sm"
+                  title="Clear filter"
+                >
+                  <span class="text-[10px] font-mono tracking-wider">
+                    FILTER: {nodeFilter.size}
+                  </span>
+                  <svg
+                    class="w-3 h-3"
+                    fill="none"
+                    viewBox="0 0 24 24"
+                    stroke="currentColor"
+                    stroke-width="2"
+                  >
+                    <path
+                      stroke-linecap="round"
+                      stroke-linejoin="round"
+                      d="M6 18L18 6M6 6l12 12"
+                    />
+                  </svg>
+                </button>
+              {/if}
+            </div>
+
+            <!-- Chat Input - Below topology -->
+            <div class="px-4 pt-6 pb-8">
+              <div class="max-w-3xl mx-auto">
+                {#if instanceCount === 0}
+                  <!-- No model loaded prompt -->
+                  <div class="text-center mb-6">
+                    <p class="text-sm text-white/60 font-mono">
+                      No model loaded yet. Select a model to get started.
+                    </p>
+                  </div>
+                {/if}
+                <ChatForm
+                  placeholder={instanceCount === 0
+                    ? "Choose a model above to start chatting"
+                    : "Ask anything"}
+                  showHelperText={false}
+                  showModelSelector={true}
+                  modelTasks={modelTasks()}
+                  modelCapabilities={modelCapabilities()}
+                />
+              </div>
+            </div>
+          </div>
+
+          <!-- Right Sidebar: Instance Controls (wider on welcome page for better visibility) -->
+          <aside
+            class="w-80 border-l border-exo-yellow/10 bg-exo-dark-gray flex flex-col flex-shrink-0"
+            aria-label="Instance controls"
+          >
+            <!-- Running Instances Panel (only shown when instances exist) - Scrollable -->
+            {#if instanceCount > 0}
+              <div class="p-4 flex-shrink-0">
+                <!-- Panel Header -->
+                <div class="flex items-center gap-2 mb-4">
+                  <div
+                    class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
+                  ></div>
+                  <h3
+                    class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
+                  >
+                    Instances
+                  </h3>
+                  <div
+                    class="flex-1 h-px bg-gradient-to-r from-exo-yellow/30 to-transparent"
+                  ></div>
+                </div>
+
+                <div
+                  bind:this={instancesContainerRef}
+                  class="max-h-72 xl:max-h-96 space-y-3 overflow-y-auto overflow-x-hidden py-px"
+                >
+                  {#each unifiedDisplayItems as item (item.id)}
+                    {@const id = item.id}
+                    {@const instance = item.instance}
+                    {@const downloadInfo = instance
+                      ? getInstanceDownloadStatus(
+                          item.instanceId ?? id,
+                          instance,
+                        )
+                      : getMetaInstancePlacingStatus(id)}
+                    {@const metaData = item.isMetaInstance
+                      ? metaInstancesData[id]
+                      : null}
+                    {@const retryError =
+                      metaData?.lastFailureError && !downloadInfo.isFailed
+                        ? metaData.consecutiveFailures > 0
+                          ? `(${((metaData.consecutiveFailures - 1) % 3) + 1}/3) ${metaData.lastFailureError}`
+                          : metaData.lastFailureError
+                        : null}
+                    {@const statusText = downloadInfo.statusText}
+                    {@const isDownloading = downloadInfo.isDownloading}
+                    {@const isFailed =
+                      statusText === "FAILED" ||
+                      statusText === "PLACEMENT FAILED"}
+                    {@const isLoading =
+                      statusText.startsWith("LOADING") ||
+                      statusText.startsWith("WARMING UP") ||
+                      statusText === "WAITING" ||
+                      statusText === "PLACING" ||
+                      statusText.startsWith("RETRYING")}
+                    {@const isReady =
+                      statusText === "READY" || statusText === "LOADED"}
+                    {@const isRunning = statusText === "RUNNING"}
+                    <!-- Instance Card -->
+                    {@const instanceModelId = item.modelId}
+                    {@const instanceInfo = instance
+                      ? getInstanceInfo(instance)
+                      : {
+                          instanceType:
+                            item.instanceMeta === "MlxRing"
+                              ? "MLX Ring"
+                              : item.instanceMeta === "MlxJaccl"
+                                ? "MLX RDMA"
+                                : "Unknown",
+                          sharding: item.sharding ?? "Unknown",
+                          nodeNames: [] as string[],
+                          nodeIds: [] as string[],
+                          nodeCount: 0,
+                        }}
+                    {@const instanceConnections = instance
+                      ? getInstanceConnections(instance)
+                      : []}
+                    <div
+                      class="relative group cursor-pointer"
+                      role="button"
+                      tabindex="0"
+                      transition:slide={{ duration: 250, easing: cubicOut }}
+                      onmouseenter={() => (hoveredInstanceId = id)}
+                      onmouseleave={() => (hoveredInstanceId = null)}
+                      onclick={() => {
                         if (
                           instanceModelId &&
                           instanceModelId !== "Unknown" &&
@@ -2785,871 +4040,929 @@
                         ) {
                           setSelectedChatModel(instanceModelId);
                         }
-                      }
-                    }}
-                  >
-                    <!-- Corner accents -->
-                    <div
-                      class="absolute -top-px -left-px w-2 h-2 border-l border-t {isDownloading
-                        ? 'border-blue-500/50'
-                        : isFailed
-                          ? 'border-red-500/50'
-                          : isLoading
-                            ? 'border-yellow-500/50'
-                            : isReady
-                              ? 'border-green-500/50'
-                              : 'border-teal-500/50'}"
-                    ></div>
-                    <div
-                      class="absolute -top-px -right-px w-2 h-2 border-r border-t {isDownloading
-                        ? 'border-blue-500/50'
-                        : isFailed
-                          ? 'border-red-500/50'
-                          : isLoading
-                            ? 'border-yellow-500/50'
-                            : isReady
-                              ? 'border-green-500/50'
-                              : 'border-teal-500/50'}"
-                    ></div>
-                    <div
-                      class="absolute -bottom-px -left-px w-2 h-2 border-l border-b {isDownloading
-                        ? 'border-blue-500/50'
-                        : isFailed
-                          ? 'border-red-500/50'
-                          : isLoading
-                            ? 'border-yellow-500/50'
-                            : isReady
-                              ? 'border-green-500/50'
-                              : 'border-teal-500/50'}"
-                    ></div>
-                    <div
-                      class="absolute -bottom-px -right-px w-2 h-2 border-r border-b {isDownloading
-                        ? 'border-blue-500/50'
-                        : isFailed
-                          ? 'border-red-500/50'
-                          : isLoading
-                            ? 'border-yellow-500/50'
-                            : isReady
-                              ? 'border-green-500/50'
-                              : 'border-teal-500/50'}"
-                    ></div>
-
-                    <div
-                      class="bg-exo-dark-gray/60 border border-l-2 {isDownloading
-                        ? 'border-blue-500/30 border-l-blue-400'
-                        : isFailed
-                          ? 'border-red-500/30 border-l-red-400'
-                          : isLoading
-                            ? 'border-exo-yellow/30 border-l-yellow-400'
-                            : isReady
-                              ? 'border-green-500/30 border-l-green-400'
-                              : 'border-teal-500/30 border-l-teal-400'} p-3"
+                      }}
+                      onkeydown={(e) => {
+                        if (e.key === "Enter" || e.key === " ") {
+                          if (
+                            instanceModelId &&
+                            instanceModelId !== "Unknown" &&
+                            instanceModelId !== "Unknown Model"
+                          ) {
+                            setSelectedChatModel(instanceModelId);
+                          }
+                        }
+                      }}
                     >
-                      <div class="flex justify-between items-start mb-2 pl-2">
-                        <div class="flex items-center gap-2">
-                          <div
-                            class="w-1.5 h-1.5 {isDownloading
-                              ? 'bg-blue-400 animate-pulse'
-                              : isFailed
-                                ? 'bg-red-400'
-                                : isLoading
-                                  ? 'bg-yellow-400 animate-pulse'
-                                  : isReady
-                                    ? 'bg-green-400'
-                                    : 'bg-teal-400'} rounded-full shadow-[0_0_6px_currentColor]"
-                          ></div>
-                          <span
-                            class="text-exo-light-gray font-mono text-sm tracking-wider"
-                            >{id.slice(0, 8).toUpperCase()}</span
+                      <!-- Corner accents -->
+                      <div
+                        class="absolute -top-px -left-px w-2 h-2 border-l border-t {isDownloading
+                          ? 'border-blue-500/50'
+                          : isFailed
+                            ? 'border-red-500/50'
+                            : isLoading
+                              ? 'border-yellow-500/50'
+                              : isReady
+                                ? 'border-green-500/50'
+                                : 'border-teal-500/50'}"
+                      ></div>
+                      <div
+                        class="absolute -top-px -right-px w-2 h-2 border-r border-t {isDownloading
+                          ? 'border-blue-500/50'
+                          : isFailed
+                            ? 'border-red-500/50'
+                            : isLoading
+                              ? 'border-yellow-500/50'
+                              : isReady
+                                ? 'border-green-500/50'
+                                : 'border-teal-500/50'}"
+                      ></div>
+                      <div
+                        class="absolute -bottom-px -left-px w-2 h-2 border-l border-b {isDownloading
+                          ? 'border-blue-500/50'
+                          : isFailed
+                            ? 'border-red-500/50'
+                            : isLoading
+                              ? 'border-yellow-500/50'
+                              : isReady
+                                ? 'border-green-500/50'
+                                : 'border-teal-500/50'}"
+                      ></div>
+                      <div
+                        class="absolute -bottom-px -right-px w-2 h-2 border-r border-b {isDownloading
+                          ? 'border-blue-500/50'
+                          : isFailed
+                            ? 'border-red-500/50'
+                            : isLoading
+                              ? 'border-yellow-500/50'
+                              : isReady
+                                ? 'border-green-500/50'
+                                : 'border-teal-500/50'}"
+                      ></div>
+
+                      <div
+                        class="bg-exo-dark-gray/60 border border-l-2 transition-all duration-200 group-hover:bg-exo-dark-gray/80 {isDownloading
+                          ? 'border-blue-500/30 border-l-blue-400 group-hover:border-blue-500/50'
+                          : isFailed
+                            ? 'border-red-500/30 border-l-red-400 group-hover:border-red-500/50'
+                            : isLoading
+                              ? 'border-exo-yellow/30 border-l-yellow-400 group-hover:border-exo-yellow/50'
+                              : isReady
+                                ? 'border-green-500/30 border-l-green-400 group-hover:border-green-500/50'
+                                : 'border-teal-500/30 border-l-teal-400 group-hover:border-teal-500/50'} p-3"
+                      >
+                        <div class="flex justify-between items-start mb-2 pl-2">
+                          <div class="flex items-center gap-2">
+                            <div
+                              class="w-1.5 h-1.5 {isDownloading
+                                ? 'bg-blue-400 animate-pulse'
+                                : isFailed
+                                  ? 'bg-red-400'
+                                  : isLoading
+                                    ? 'bg-yellow-400 animate-pulse'
+                                    : isReady
+                                      ? 'bg-green-400'
+                                      : 'bg-teal-400'} rounded-full shadow-[0_0_6px_currentColor]"
+                            ></div>
+                            <span
+                              class="text-exo-light-gray font-mono text-sm tracking-wider"
+                              >{id.slice(0, 8).toUpperCase()}</span
+                            >
+                          </div>
+                          <button
+                            onclick={() =>
+                              item.isMetaInstance
+                                ? deleteMetaInstance(id)
+                                : deleteInstance(id)}
+                            class="text-xs px-2 py-1 font-mono tracking-wider uppercase border border-red-500/30 text-red-400 hover:bg-red-500/20 hover:text-red-400 hover:border-red-500/50 transition-all duration-200 cursor-pointer"
                           >
+                            DELETE
+                          </button>
                         </div>
-                        <button
-                          onclick={() =>
-                            item.isMetaInstance
-                              ? deleteMetaInstance(id)
-                              : deleteInstance(id)}
-                          class="text-xs px-2 py-1 font-mono tracking-wider uppercase border border-red-500/30 text-red-400 hover:bg-red-500/20 hover:text-red-400 hover:border-red-500/50 transition-all duration-200 cursor-pointer"
-                        >
-                          DELETE
-                        </button>
-                      </div>
-                      <div class="pl-2">
-                        <div
-                          class="text-exo-yellow text-xs font-mono tracking-wide truncate"
-                        >
-                          {instanceModelId}
-                        </div>
-                        <div class="text-white/60 text-xs font-mono">
-                          Strategy: <span class="text-white/80"
-                            >{instanceInfo.sharding} ({instanceInfo.instanceType})</span
+                        <div class="pl-2">
+                          <div
+                            class="text-exo-yellow text-xs font-mono tracking-wide truncate"
                           >
-                        </div>
-                        {#if instanceModelId && instanceModelId !== "Unknown" && instanceModelId !== "Unknown Model"}
-                          <a
-                            class="inline-flex items-center gap-1 text-[11px] text-white/60 hover:text-exo-yellow transition-colors mt-1"
-                            href={`https://huggingface.co/${instanceModelId}`}
-                            target="_blank"
-                            rel="noreferrer noopener"
-                            aria-label="View model on Hugging Face"
+                            {instanceModelId}
+                          </div>
+                          <div
+                            class="flex items-center gap-2 text-white/60 text-xs font-mono"
                           >
-                            <span>Hugging Face</span>
-                            <svg
-                              class="w-3.5 h-3.5"
-                              viewBox="0 0 24 24"
-                              fill="none"
-                              stroke="currentColor"
-                              stroke-width="2"
-                              stroke-linecap="round"
-                              stroke-linejoin="round"
+                            <span
+                              >{instanceInfo.sharding} &middot; {instanceInfo.instanceType}</span
                             >
-                              <path d="M14 3h7v7" />
-                              <path d="M10 14l11-11" />
-                              <path
-                                d="M21 14v6a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1h6"
-                              />
-                            </svg>
-                          </a>
-                        {/if}
-                        {#if instanceInfo.nodeNames.length > 0}
-                          <div class="text-white/60 text-xs font-mono">
-                            {instanceInfo.nodeNames.join(", ")}
+                            <span
+                              class="px-1.5 py-0.5 text-[10px] tracking-wider uppercase rounded transition-all duration-300 {isDownloading
+                                ? 'bg-blue-500/15 text-blue-400'
+                                : isFailed
+                                  ? 'bg-red-500/15 text-red-400'
+                                  : isLoading
+                                    ? 'bg-yellow-500/15 text-yellow-400'
+                                    : isReady
+                                      ? 'bg-green-500/15 text-green-400'
+                                      : 'bg-teal-500/15 text-teal-400'}"
+                            >
+                              {statusText}
+                            </span>
                           </div>
-                        {/if}
-                        {#if debugEnabled && instanceConnections.length > 0}
-                          <div class="mt-2 space-y-1">
-                            {#each instanceConnections as conn}
+                          {#if instanceModelId && instanceModelId !== "Unknown" && instanceModelId !== "Unknown Model"}
+                            <a
+                              class="inline-flex items-center gap-1 text-[11px] text-white/60 hover:text-exo-yellow transition-colors mt-1"
+                              href={`https://huggingface.co/${instanceModelId}`}
+                              target="_blank"
+                              rel="noreferrer noopener"
+                              aria-label="View model on Hugging Face"
+                            >
+                              <span>Hugging Face</span>
+                              <svg
+                                class="w-3.5 h-3.5"
+                                viewBox="0 0 24 24"
+                                fill="none"
+                                stroke="currentColor"
+                                stroke-width="2"
+                                stroke-linecap="round"
+                                stroke-linejoin="round"
+                              >
+                                <path d="M14 3h7v7" />
+                                <path d="M10 14l11-11" />
+                                <path
+                                  d="M21 14v6a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1h6"
+                                />
+                              </svg>
+                            </a>
+                          {/if}
+                          {#if instanceInfo.nodeNames.length > 0}
+                            <div class="text-white/60 text-xs font-mono">
+                              {instanceInfo.nodeNames.join(", ")}
+                            </div>
+                          {/if}
+                          {#if debugEnabled && instanceConnections.length > 0}
+                            <div class="mt-2 space-y-1">
+                              {#each instanceConnections as conn}
+                                <div
+                                  class="text-[11px] leading-snug font-mono text-white/70"
+                                >
+                                  <span
+                                    >{conn.from} -> {conn.to}: {conn.ip}</span
+                                  >
+                                  <span
+                                    class={conn.missingIface
+                                      ? "text-red-400"
+                                      : "text-white/60"}
+                                  >
+                                    ({conn.ifaceLabel})</span
+                                  >
+                                </div>
+                              {/each}
+                            </div>
+                          {/if}
+
+                          <!-- Download Progress -->
+                          {#if downloadInfo.isDownloading && downloadInfo.progress}
+                            <div class="mt-2 space-y-1">
+                              <div
+                                class="flex justify-between text-xs font-mono"
+                              >
+                                <span class="text-blue-400"
+                                  >{downloadInfo.progress.percentage.toFixed(
+                                    1,
+                                  )}%</span
+                                >
+                                <span class="text-exo-light-gray"
+                                  >{formatBytes(
+                                    downloadInfo.progress.downloadedBytes,
+                                  )}/{formatBytes(
+                                    downloadInfo.progress.totalBytes,
+                                  )}</span
+                                >
+                              </div>
+                              <div
+                                class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
+                              >
+                                <div
+                                  class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
+                                  style="width: {downloadInfo.progress
+                                    .percentage}%"
+                                ></div>
+                              </div>
                               <div
-                                class="text-[11px] leading-snug font-mono text-white/70"
+                                class="flex justify-between text-xs font-mono text-exo-light-gray"
                               >
-                                <span>{conn.from} -> {conn.to}: {conn.ip}</span>
                                 <span
-                                  class={conn.missingIface
-                                    ? "text-red-400"
-                                    : "text-white/60"}
+                                  >{formatSpeed(
+                                    downloadInfo.progress.speed,
+                                  )}</span
+                                >
+                                <span
+                                  >ETA: {formatEta(
+                                    downloadInfo.progress.etaMs,
+                                  )}</span
                                 >
-                                  ({conn.ifaceLabel})</span
+                                <span
+                                  >{downloadInfo.progress
+                                    .completedFiles}/{downloadInfo.progress
+                                    .totalFiles} files</span
                                 >
                               </div>
-                            {/each}
-                          </div>
-                        {/if}
-
-                        <!-- Download Progress -->
-                        {#if downloadInfo.isDownloading && downloadInfo.progress}
-                          <div class="mt-2 space-y-1">
-                            <div class="flex justify-between text-xs font-mono">
-                              <span class="text-blue-400"
-                                >{downloadInfo.progress.percentage.toFixed(
-                                  1,
-                                )}%</span
-                              >
-                              <span class="text-exo-light-gray"
-                                >{formatBytes(
-                                  downloadInfo.progress.downloadedBytes,
-                                )}/{formatBytes(
-                                  downloadInfo.progress.totalBytes,
-                                )}</span
-                              >
                             </div>
-                            <div
-                              class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
-                            >
+                            {#if downloadInfo.perNode.length > 0}
                               <div
-                                class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
-                                style="width: {downloadInfo.progress
-                                  .percentage}%"
-                              ></div>
-                            </div>
-                            <div
-                              class="flex justify-between text-xs font-mono text-exo-light-gray"
-                            >
-                              <span
-                                >{formatSpeed(
-                                  downloadInfo.progress.speed,
-                                )}</span
-                              >
-                              <span
-                                >ETA: {formatEta(
-                                  downloadInfo.progress.etaMs,
-                                )}</span
-                              >
-                              <span
-                                >{downloadInfo.progress
-                                  .completedFiles}/{downloadInfo.progress
-                                  .totalFiles} files</span
+                                class="mt-2 space-y-2 max-h-48 overflow-y-auto pr-1"
                               >
-                            </div>
-                          </div>
-                          {#if downloadInfo.perNode.length > 0}
-                            <div
-                              class="mt-2 space-y-2 max-h-48 overflow-y-auto pr-1"
-                            >
-                              {#each downloadInfo.perNode as nodeProg}
-                                {@const nodePercent = Math.min(
-                                  100,
-                                  Math.max(0, nodeProg.progress.percentage),
-                                )}
-                                {@const isExpanded =
-                                  instanceDownloadExpandedNodes.has(
-                                    nodeProg.nodeId,
+                                {#each downloadInfo.perNode as nodeProg}
+                                  {@const nodePercent = Math.min(
+                                    100,
+                                    Math.max(0, nodeProg.progress.percentage),
                                   )}
-                                <div
-                                  class="rounded border border-exo-medium-gray/40 bg-exo-black/30 p-2"
-                                >
-                                  <button
-                                    type="button"
-                                    class="w-full text-left space-y-1.5"
-                                    onclick={() =>
-                                      toggleInstanceDownloadDetails(
-                                        nodeProg.nodeId,
-                                      )}
+                                  {@const isExpanded =
+                                    instanceDownloadExpandedNodes.has(
+                                      nodeProg.nodeId,
+                                    )}
+                                  <div
+                                    class="rounded border border-exo-medium-gray/40 bg-exo-black/30 p-2"
                                   >
-                                    <div
-                                      class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
+                                    <button
+                                      type="button"
+                                      class="w-full text-left space-y-1.5"
+                                      onclick={() =>
+                                        toggleInstanceDownloadDetails(
+                                          nodeProg.nodeId,
+                                        )}
                                     >
-                                      <span class="text-white/80 truncate pr-2"
-                                        >{nodeProg.nodeName}</span
-                                      >
-                                      <span
-                                        class="flex items-center gap-1 text-blue-300"
+                                      <div
+                                        class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
                                       >
-                                        {nodePercent.toFixed(1)}%
-                                        <svg
-                                          class="w-3 h-3 text-exo-light-gray"
-                                          viewBox="0 0 20 20"
-                                          fill="none"
-                                          stroke="currentColor"
-                                          stroke-width="2"
+                                        <span
+                                          class="text-white/80 truncate pr-2"
+                                          >{nodeProg.nodeName}</span
                                         >
-                                          <path
-                                            d="M6 8l4 4 4-4"
-                                            class={isExpanded
-                                              ? "transform rotate-180 origin-center transition-transform duration-150"
-                                              : "transition-transform duration-150"}
-                                          ></path>
-                                        </svg>
-                                      </span>
-                                    </div>
-                                    <div
-                                      class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
-                                    >
+                                        <span
+                                          class="flex items-center gap-1 text-blue-300"
+                                        >
+                                          {nodePercent.toFixed(1)}%
+                                          <svg
+                                            class="w-3 h-3 text-exo-light-gray"
+                                            viewBox="0 0 20 20"
+                                            fill="none"
+                                            stroke="currentColor"
+                                            stroke-width="2"
+                                          >
+                                            <path
+                                              d="M6 8l4 4 4-4"
+                                              class={isExpanded
+                                                ? "transform rotate-180 origin-center transition-transform duration-150"
+                                                : "transition-transform duration-150"}
+                                            ></path>
+                                          </svg>
+                                        </span>
+                                      </div>
                                       <div
-                                        class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
-                                        style="width: {nodePercent.toFixed(1)}%"
-                                      ></div>
-                                    </div>
-                                    <div
-                                      class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
-                                    >
-                                      <span
-                                        >{formatBytes(
-                                          nodeProg.progress.downloadedBytes,
-                                        )} / {formatBytes(
-                                          nodeProg.progress.totalBytes,
-                                        )}</span
-                                      >
-                                      <span
-                                        >{formatSpeed(nodeProg.progress.speed)} •
-                                        ETA {formatEta(
-                                          nodeProg.progress.etaMs,
-                                        )}</span
+                                        class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
                                       >
-                                    </div>
-                                  </button>
-
-                                  {#if isExpanded}
-                                    <div class="mt-2 space-y-1.5">
-                                      {#if nodeProg.progress.files.length === 0}
                                         <div
-                                          class="text-[11px] font-mono text-exo-light-gray/70"
+                                          class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
+                                          style="width: {nodePercent.toFixed(
+                                            1,
+                                          )}%"
+                                        ></div>
+                                      </div>
+                                      <div
+                                        class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
+                                      >
+                                        <span
+                                          >{formatBytes(
+                                            nodeProg.progress.downloadedBytes,
+                                          )} / {formatBytes(
+                                            nodeProg.progress.totalBytes,
+                                          )}</span
                                         >
-                                          No file details reported.
-                                        </div>
-                                      {:else}
-                                        {#each nodeProg.progress.files as f}
-                                          {@const filePercent = Math.min(
-                                            100,
-                                            Math.max(0, f.percentage ?? 0),
-                                          )}
-                                          {@const isFileComplete =
-                                            filePercent >= 100}
+                                        <span
+                                          >{formatSpeed(
+                                            nodeProg.progress.speed,
+                                          )} • ETA {formatEta(
+                                            nodeProg.progress.etaMs,
+                                          )}</span
+                                        >
+                                      </div>
+                                    </button>
+
+                                    {#if isExpanded}
+                                      <div class="mt-2 space-y-1.5">
+                                        {#if nodeProg.progress.files.length === 0}
                                           <div
-                                            class="rounded border border-exo-medium-gray/30 bg-exo-black/40 p-2"
+                                            class="text-[11px] font-mono text-exo-light-gray/70"
                                           >
+                                            No file details reported.
+                                          </div>
+                                        {:else}
+                                          {#each nodeProg.progress.files as f}
+                                            {@const filePercent = Math.min(
+                                              100,
+                                              Math.max(0, f.percentage ?? 0),
+                                            )}
+                                            {@const isFileComplete =
+                                              filePercent >= 100}
                                             <div
-                                              class="flex items-center justify-between text-[10px] font-mono text-exo-light-gray/90"
+                                              class="rounded border border-exo-medium-gray/30 bg-exo-black/40 p-2"
                                             >
-                                              <span class="truncate pr-2"
-                                                >{f.name}</span
-                                              >
-                                              <span
-                                                class={isFileComplete
-                                                  ? "text-green-400"
-                                                  : "text-white/80"}
-                                                >{filePercent.toFixed(1)}%</span
+                                              <div
+                                                class="flex items-center justify-between text-[10px] font-mono text-exo-light-gray/90"
                                               >
-                                            </div>
-                                            <div
-                                              class="relative h-1 bg-exo-black/60 rounded-sm overflow-hidden mt-1"
-                                            >
+                                                <span class="truncate pr-2"
+                                                  >{f.name}</span
+                                                >
+                                                <span
+                                                  class={isFileComplete
+                                                    ? "text-green-400"
+                                                    : "text-white/80"}
+                                                  >{filePercent.toFixed(
+                                                    1,
+                                                  )}%</span
+                                                >
+                                              </div>
                                               <div
-                                                class="absolute inset-y-0 left-0 bg-gradient-to-r {isFileComplete
-                                                  ? 'from-green-500 to-green-400'
-                                                  : 'from-exo-yellow to-exo-yellow/70'} transition-all duration-300"
-                                                style="width: {filePercent.toFixed(
-                                                  1,
-                                                )}%"
-                                              ></div>
-                                            </div>
-                                            <div
-                                              class="flex items-center justify-between text-[10px] text-exo-light-gray/70 mt-0.5"
-                                            >
-                                              <span
-                                                >{formatBytes(
-                                                  f.downloadedBytes,
-                                                )} / {formatBytes(
-                                                  f.totalBytes,
-                                                )}</span
+                                                class="relative h-1 bg-exo-black/60 rounded-sm overflow-hidden mt-1"
                                               >
-                                              <span
-                                                >{formatSpeed(f.speed)} • ETA {formatEta(
-                                                  f.etaMs,
-                                                )}</span
+                                                <div
+                                                  class="absolute inset-y-0 left-0 bg-gradient-to-r {isFileComplete
+                                                    ? 'from-green-500 to-green-400'
+                                                    : 'from-exo-yellow to-exo-yellow/70'} transition-all duration-300"
+                                                  style="width: {filePercent.toFixed(
+                                                    1,
+                                                  )}%"
+                                                ></div>
+                                              </div>
+                                              <div
+                                                class="flex items-center justify-between text-[10px] text-exo-light-gray/70 mt-0.5"
                                               >
+                                                <span
+                                                  >{formatBytes(
+                                                    f.downloadedBytes,
+                                                  )} / {formatBytes(
+                                                    f.totalBytes,
+                                                  )}</span
+                                                >
+                                                <span
+                                                  >{formatSpeed(f.speed)} • ETA {formatEta(
+                                                    f.etaMs,
+                                                  )}</span
+                                                >
+                                              </div>
                                             </div>
-                                          </div>
-                                        {/each}
-                                      {/if}
-                                    </div>
-                                  {/if}
-                                </div>
-                              {/each}
-                            </div>
-                          {/if}
-                          <div
-                            class="text-xs text-blue-400 font-mono tracking-wider mt-1"
-                          >
-                            DOWNLOADING
-                          </div>
-                        {:else}
-                          <div
-                            class="text-xs {getStatusColor(
-                              downloadInfo.statusText,
-                            )} font-mono tracking-wider mt-1"
-                          >
-                            {downloadInfo.statusText}
-                          </div>
-                          {#if downloadInfo.isFailed && downloadInfo.errorMessage}
-                            <div
-                              class="text-xs text-red-400/80 font-mono mt-1 break-words"
-                            >
-                              {downloadInfo.errorMessage}
-                            </div>
-                          {:else if retryError}
-                            <div
-                              class="text-xs text-orange-400/80 font-mono mt-1 break-words"
-                            >
-                              Retrying after error: {retryError}
+                                          {/each}
+                                        {/if}
+                                      </div>
+                                    {/if}
+                                  </div>
+                                {/each}
+                              </div>
+                            {/if}
+                            <div class="mt-2 space-y-1">
+                              <div
+                                class="text-xs text-blue-400 font-mono tracking-wider"
+                              >
+                                DOWNLOADING
+                              </div>
+                              <p
+                                class="text-[11px] text-white/50 leading-relaxed"
+                              >
+                                Downloading model files. This runs locally on
+                                your device and needs to finish before you can
+                                chat.
+                              </p>
                             </div>
-                          {/if}
-                          {#if downloadInfo.perNodeStatus.length > 1 && (statusText.startsWith("LOADING") || statusText.startsWith("WARMING UP") || statusText === "WAITING" || statusText === "INITIALIZING")}
-                            <div class="mt-1.5 space-y-0.5">
-                              {#each downloadInfo.perNodeStatus as node}
-                                <div
-                                  class="flex items-center justify-between text-[10px] font-mono"
+                          {:else}
+                            <div class="mt-1 space-y-1">
+                              <div
+                                class="text-xs {getStatusColor(
+                                  downloadInfo.statusText,
+                                )} font-mono tracking-wider"
+                              >
+                                {downloadInfo.statusText}
+                              </div>
+                              {#if isLoading}
+                                <p
+                                  class="text-[11px] text-white/50 leading-relaxed"
                                 >
-                                  <span class="text-white/60 truncate pr-2"
-                                    >{node.nodeName}</span
-                                  >
-                                  <span
-                                    class={getStatusColor(
-                                      node.status.toUpperCase(),
-                                    )}>{node.status}</span
-                                  >
-                                </div>
-                              {/each}
+                                  Loading model into memory for fast
+                                  inference...
+                                </p>
+                              {:else if isReady || isRunning}
+                                <p
+                                  class="text-[11px] text-green-400/70 leading-relaxed"
+                                >
+                                  Ready to chat! Type a message below.
+                                </p>
+                              {/if}
                             </div>
+                            {#if downloadInfo.isFailed && downloadInfo.errorMessage}
+                              <div
+                                class="text-xs text-red-400/80 font-mono mt-1 break-words"
+                              >
+                                {downloadInfo.errorMessage}
+                              </div>
+                            {:else if retryError}
+                              <div
+                                class="text-xs text-orange-400/80 font-mono mt-1 break-words"
+                              >
+                                Retrying after error: {retryError}
+                              </div>
+                            {/if}
+                            {#if downloadInfo.perNodeStatus.length > 1 && (statusText.startsWith("LOADING") || statusText.startsWith("WARMING UP") || statusText === "WAITING" || statusText === "INITIALIZING")}
+                              <div class="mt-1.5 space-y-0.5">
+                                {#each downloadInfo.perNodeStatus as node}
+                                  <div
+                                    class="flex items-center justify-between text-[10px] font-mono"
+                                  >
+                                    <span class="text-white/60 truncate pr-2"
+                                      >{node.nodeName}</span
+                                    >
+                                    <span
+                                      class={getStatusColor(
+                                        node.status.toUpperCase(),
+                                      )}>{node.status}</span
+                                    >
+                                  </div>
+                                {/each}
+                              </div>
+                            {/if}
                           {/if}
-                        {/if}
+                        </div>
                       </div>
                     </div>
-                  </div>
-                {/each}
+                  {/each}
+                </div>
+              </div>
+            {/if}
+
+            <!-- Models Panel - Scrollable -->
+            <div class="p-4 flex-1 overflow-y-auto">
+              <!-- Panel Header -->
+              <div class="flex items-center gap-2 mb-3 flex-shrink-0">
+                <div
+                  class="w-2 h-2 border border-exo-yellow/60 rotate-45"
+                ></div>
+                <h3
+                  class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
+                >
+                  Load Model
+                </h3>
+                <div
+                  class="flex-1 h-px bg-gradient-to-r from-exo-yellow/30 to-transparent"
+                ></div>
+                <span class="text-sm text-white/70 font-mono"
+                  >{models.length} models</span
+                >
               </div>
-            </div>
-          {/if}
-
-          <!-- Models Panel - Scrollable -->
-          <div class="p-4 flex-1 overflow-y-auto">
-            <!-- Panel Header -->
-            <div class="flex items-center gap-2 mb-3 flex-shrink-0">
-              <div class="w-2 h-2 border border-exo-yellow/60 rotate-45"></div>
-              <h3
-                class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
-              >
-                Launch Instance
-              </h3>
-              <div
-                class="flex-1 h-px bg-gradient-to-r from-exo-yellow/30 to-transparent"
-              ></div>
-              <span class="text-sm text-white/70 font-mono"
-                >{models.length} models</span
-              >
-            </div>
 
-            <!-- Model Picker Button -->
-            <div class="flex-shrink-0 mb-3">
-              <button
-                type="button"
-                onclick={() => (isModelPickerOpen = true)}
-                class="w-full bg-exo-medium-gray/50 border border-exo-yellow/30 rounded pl-3 pr-8 py-2.5 text-sm font-mono text-left tracking-wide cursor-pointer transition-all duration-200 hover:border-exo-yellow/50 focus:outline-none focus:border-exo-yellow/70 relative"
-              >
-                {#if selectedModelId}
-                  {@const foundModel = models.find(
-                    (m) => m.id === selectedModelId,
-                  )}
-                  {#if foundModel}
-                    {@const sizeGB = getModelSizeGB(foundModel)}
-                    <span
-                      class="flex items-center justify-between gap-2 w-full pr-4"
-                    >
+              <!-- Model Picker Button -->
+              <div class="flex-shrink-0 mb-3">
+                <button
+                  type="button"
+                  onclick={() => (isModelPickerOpen = true)}
+                  class="w-full bg-exo-medium-gray/50 border border-exo-yellow/30 rounded pl-3 pr-8 py-2.5 text-sm font-mono text-left tracking-wide cursor-pointer transition-all duration-200 hover:border-exo-yellow/50 focus:outline-none focus:border-exo-yellow/70 relative"
+                >
+                  {#if selectedModelId}
+                    {@const foundModel = models.find(
+                      (m) => m.id === selectedModelId,
+                    )}
+                    {#if foundModel}
+                      {@const sizeGB = getModelSizeGB(foundModel)}
                       <span
-                        class="flex items-center gap-2 text-exo-light-gray truncate"
+                        class="flex items-center justify-between gap-2 w-full pr-4"
                       >
-                        <span class="truncate"
-                          >{foundModel.name || foundModel.id}</span
+                        <span
+                          class="flex items-center gap-2 text-exo-light-gray truncate"
+                        >
+                          <span class="truncate"
+                            >{foundModel.name || foundModel.id}</span
+                          >
+                        </span>
+                        <span class="text-white/50 text-xs flex-shrink-0"
+                          >{sizeGB >= 1
+                            ? sizeGB.toFixed(0)
+                            : sizeGB.toFixed(1)}GB</span
                         >
                       </span>
-                      <span class="text-white/50 text-xs flex-shrink-0"
-                        >{sizeGB >= 1
-                          ? sizeGB.toFixed(0)
-                          : sizeGB.toFixed(1)}GB</span
-                      >
-                    </span>
+                    {:else}
+                      <span class="text-exo-light-gray">{selectedModelId}</span>
+                    {/if}
                   {:else}
-                    <span class="text-exo-light-gray">{selectedModelId}</span>
+                    <span class="text-white/50">— SELECT MODEL —</span>
                   {/if}
-                {:else}
-                  <span class="text-white/50">— SELECT MODEL —</span>
-                {/if}
-                <div
-                  class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none"
+                  <div
+                    class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none"
+                  >
+                    <svg
+                      class="w-4 h-4 text-exo-yellow/60"
+                      fill="none"
+                      viewBox="0 0 24 24"
+                      stroke="currentColor"
+                    >
+                      <path
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                        stroke-width="2"
+                        d="M19 9l-7 7-7-7"
+                      />
+                    </svg>
+                  </div>
+                </button>
+              </div>
+
+              <!-- Advanced Options Toggle -->
+              <div class="flex-shrink-0 mb-4">
+                <button
+                  type="button"
+                  onclick={() => (showAdvancedOptions = !showAdvancedOptions)}
+                  class="flex items-center gap-2 text-xs text-white/50 hover:text-white/70 font-mono tracking-wider uppercase transition-colors cursor-pointer py-1"
+                  aria-expanded={showAdvancedOptions}
                 >
                   <svg
-                    class="w-4 h-4 text-exo-yellow/60"
+                    class="w-3 h-3 transition-transform duration-200 {showAdvancedOptions
+                      ? 'rotate-90'
+                      : ''}"
                     fill="none"
                     viewBox="0 0 24 24"
                     stroke="currentColor"
+                    stroke-width="2"
                   >
                     <path
                       stroke-linecap="round"
                       stroke-linejoin="round"
-                      stroke-width="2"
-                      d="M19 9l-7 7-7-7"
+                      d="M9 5l7 7-7 7"
                     />
                   </svg>
-                </div>
-              </button>
-            </div>
-
-            <!-- Configuration Options -->
-            <div class="flex-shrink-0 mb-4 space-y-3">
-              <!-- Sharding -->
-              <div>
-                <div class="text-xs text-white/70 font-mono mb-2">
-                  Sharding:
-                </div>
-                <div class="flex gap-2">
-                  <button
-                    onclick={() => {
-                      selectedSharding = "Pipeline";
-                      saveLaunchDefaults();
-                    }}
-                    class="flex items-center gap-2 py-2 px-4 text-sm font-mono border rounded transition-all duration-200 cursor-pointer {selectedSharding ===
-                    'Pipeline'
-                      ? 'bg-transparent text-exo-yellow border-exo-yellow'
-                      : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
-                  >
-                    <span
-                      class="w-4 h-4 rounded-full border-2 flex items-center justify-center {selectedSharding ===
-                      'Pipeline'
-                        ? 'border-exo-yellow'
-                        : 'border-exo-medium-gray'}"
-                    >
-                      {#if selectedSharding === "Pipeline"}
-                        <span class="w-2 h-2 rounded-full bg-exo-yellow"></span>
-                      {/if}
-                    </span>
-                    Pipeline
-                  </button>
-                  <button
-                    onclick={() => {
-                      selectedSharding = "Tensor";
-                      saveLaunchDefaults();
-                    }}
-                    class="flex items-center gap-2 py-2 px-4 text-sm font-mono border rounded transition-all duration-200 cursor-pointer {selectedSharding ===
-                    'Tensor'
-                      ? 'bg-transparent text-exo-yellow border-exo-yellow'
-                      : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
-                  >
-                    <span
-                      class="w-4 h-4 rounded-full border-2 flex items-center justify-center {selectedSharding ===
-                      'Tensor'
-                        ? 'border-exo-yellow'
-                        : 'border-exo-medium-gray'}"
-                    >
-                      {#if selectedSharding === "Tensor"}
-                        <span class="w-2 h-2 rounded-full bg-exo-yellow"></span>
-                      {/if}
-                    </span>
-                    Tensor
-                  </button>
-                </div>
-              </div>
-
-              <!-- Instance Type -->
-              <div>
-                <div class="text-xs text-white/70 font-mono mb-2">
-                  Instance Type:
-                </div>
-                <div class="flex gap-2">
-                  <button
-                    onclick={() => {
-                      selectedInstanceType = "MlxRing";
-                      saveLaunchDefaults();
-                    }}
-                    class="flex items-center gap-2 py-2 px-4 text-sm font-mono border rounded transition-all duration-200 cursor-pointer {selectedInstanceType ===
-                    'MlxRing'
-                      ? 'bg-transparent text-exo-yellow border-exo-yellow'
-                      : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
-                  >
-                    <span
-                      class="w-4 h-4 rounded-full border-2 flex items-center justify-center {selectedInstanceType ===
-                      'MlxRing'
-                        ? 'border-exo-yellow'
-                        : 'border-exo-medium-gray'}"
-                    >
-                      {#if selectedInstanceType === "MlxRing"}
-                        <span class="w-2 h-2 rounded-full bg-exo-yellow"></span>
-                      {/if}
-                    </span>
-                    MLX Ring
-                  </button>
-                  <button
-                    onclick={() => {
-                      selectedInstanceType = "MlxJaccl";
-                      saveLaunchDefaults();
-                    }}
-                    class="flex items-center gap-2 py-2 px-4 text-sm font-mono border rounded transition-all duration-200 cursor-pointer {selectedInstanceType ===
-                    'MlxJaccl'
-                      ? 'bg-transparent text-exo-yellow border-exo-yellow'
-                      : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
-                  >
-                    <span
-                      class="w-4 h-4 rounded-full border-2 flex items-center justify-center {selectedInstanceType ===
-                      'MlxJaccl'
-                        ? 'border-exo-yellow'
-                        : 'border-exo-medium-gray'}"
-                    >
-                      {#if selectedInstanceType === "MlxJaccl"}
-                        <span class="w-2 h-2 rounded-full bg-exo-yellow"></span>
-                      {/if}
-                    </span>
-                    MLX RDMA
-                  </button>
-                </div>
-              </div>
+                  Advanced Options
+                </button>
+
+                {#if showAdvancedOptions}
+                  <div class="mt-3 space-y-3 pl-1" in:fade={{ duration: 150 }}>
+                    <!-- Sharding Strategy -->
+                    <div>
+                      <div class="text-xs text-white/50 font-mono mb-2">
+                        Sharding Strategy:
+                      </div>
+                      <div class="flex gap-2">
+                        <button
+                          onclick={() => {
+                            selectedSharding = "Pipeline";
+                            saveLaunchDefaults();
+                          }}
+                          class="flex items-center gap-2 py-1.5 px-3 text-xs font-mono border rounded transition-all duration-200 cursor-pointer {selectedSharding ===
+                          'Pipeline'
+                            ? 'bg-transparent text-exo-yellow border-exo-yellow'
+                            : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
+                        >
+                          <span
+                            class="w-3 h-3 rounded-full border-2 flex items-center justify-center {selectedSharding ===
+                            'Pipeline'
+                              ? 'border-exo-yellow'
+                              : 'border-exo-medium-gray'}"
+                          >
+                            {#if selectedSharding === "Pipeline"}
+                              <span
+                                class="w-1.5 h-1.5 rounded-full bg-exo-yellow"
+                              ></span>
+                            {/if}
+                          </span>
+                          Pipeline
+                        </button>
+                        <button
+                          onclick={() => {
+                            selectedSharding = "Tensor";
+                            saveLaunchDefaults();
+                          }}
+                          class="flex items-center gap-2 py-1.5 px-3 text-xs font-mono border rounded transition-all duration-200 cursor-pointer {selectedSharding ===
+                          'Tensor'
+                            ? 'bg-transparent text-exo-yellow border-exo-yellow'
+                            : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
+                        >
+                          <span
+                            class="w-3 h-3 rounded-full border-2 flex items-center justify-center {selectedSharding ===
+                            'Tensor'
+                              ? 'border-exo-yellow'
+                              : 'border-exo-medium-gray'}"
+                          >
+                            {#if selectedSharding === "Tensor"}
+                              <span
+                                class="w-1.5 h-1.5 rounded-full bg-exo-yellow"
+                              ></span>
+                            {/if}
+                          </span>
+                          Tensor
+                        </button>
+                      </div>
+                    </div>
 
-              <!-- Minimum Nodes (discrete slider with drag support) -->
-              <div>
-                <div class="text-xs text-white/70 font-mono mb-2">
-                  Minimum Nodes:
-                </div>
-                <!-- Discrete slider track with drag support -->
-                <!-- svelte-ignore a11y_no_static_element_interactions -->
-                <div
-                  bind:this={sliderTrackElement}
-                  class="relative h-16 cursor-pointer select-none px-2 pr-6"
-                  onmousedown={handleSliderMouseDown}
-                  ontouchstart={handleSliderTouchStart}
-                >
-                  <!-- Track background - extends full width to align with edge dots -->
-                  <div
-                    class="absolute top-6 left-0 right-0 h-2 bg-exo-medium-gray/50 rounded-full"
-                  ></div>
-                  <!-- Active track (fills up to selected) -->
-                  {#if availableMinNodes > 1}
-                    <div
-                      class="absolute top-6 left-0 h-2 bg-white/30 rounded-full transition-all pointer-events-none"
-                      style="width: {((selectedMinNodes - 1) /
-                        (availableMinNodes - 1)) *
-                        100}%"
-                    ></div>
-                  {/if}
-                  <!-- Dots and labels for each node count -->
-                  {#each Array.from({ length: availableMinNodes }, (_, i) => i + 1) as n}
-                    {@const isValid = validMinNodeCounts().has(n)}
-                    {@const isSelected = selectedMinNodes === n}
-                    {@const position =
-                      availableMinNodes > 1
-                        ? ((n - 1) / (availableMinNodes - 1)) * 100
-                        : 50}
-                    <div
-                      class="absolute flex flex-col items-center pointer-events-none"
-                      style="left: {position}%; top: 0; transform: translateX(-50%);"
-                    >
-                      <!-- Dot -->
-                      <span
-                        class="rounded-full transition-all {isSelected
-                          ? 'w-6 h-6 bg-exo-yellow shadow-[0_0_10px_rgba(255,215,0,0.6)]'
-                          : isValid
-                            ? 'w-4 h-4 bg-exo-light-gray/70 mt-1'
-                            : 'w-3 h-3 bg-exo-medium-gray/50 mt-1.5'}"
-                      ></span>
-                      <!-- Number label below dot -->
-                      <span
-                        class="text-sm font-mono mt-1.5 tabular-nums transition-colors {isSelected
-                          ? 'text-exo-yellow font-bold'
-                          : isValid
-                            ? 'text-white/70'
-                            : 'text-white/30'}">{n}</span
-                      >
+                    <!-- Interconnect -->
+                    <div>
+                      <div class="text-xs text-white/50 font-mono mb-2">
+                        Interconnect:
+                      </div>
+                      <div class="flex gap-2">
+                        <button
+                          onclick={() => {
+                            selectedInstanceType = "MlxRing";
+                            saveLaunchDefaults();
+                          }}
+                          class="flex items-center gap-2 py-1.5 px-3 text-xs font-mono border rounded transition-all duration-200 cursor-pointer {selectedInstanceType ===
+                          'MlxRing'
+                            ? 'bg-transparent text-exo-yellow border-exo-yellow'
+                            : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
+                        >
+                          <span
+                            class="w-3 h-3 rounded-full border-2 flex items-center justify-center {selectedInstanceType ===
+                            'MlxRing'
+                              ? 'border-exo-yellow'
+                              : 'border-exo-medium-gray'}"
+                          >
+                            {#if selectedInstanceType === "MlxRing"}
+                              <span
+                                class="w-1.5 h-1.5 rounded-full bg-exo-yellow"
+                              ></span>
+                            {/if}
+                          </span>
+                          Standard
+                        </button>
+                        <button
+                          onclick={() => {
+                            selectedInstanceType = "MlxIbv";
+                            saveLaunchDefaults();
+                          }}
+                          class="flex items-center gap-2 py-1.5 px-3 text-xs font-mono border rounded transition-all duration-200 cursor-pointer {selectedInstanceType ===
+                          'MlxIbv'
+                            ? 'bg-transparent text-exo-yellow border-exo-yellow'
+                            : 'bg-transparent text-white/70 border-exo-medium-gray/50 hover:border-exo-yellow/50'}"
+                        >
+                          <span
+                            class="w-3 h-3 rounded-full border-2 flex items-center justify-center {selectedInstanceType ===
+                            'MlxIbv'
+                              ? 'border-exo-yellow'
+                              : 'border-exo-medium-gray'}"
+                          >
+                            {#if selectedInstanceType === "MlxIbv"}
+                              <span
+                                class="w-1.5 h-1.5 rounded-full bg-exo-yellow"
+                              ></span>
+                            {/if}
+                          </span>
+                          RDMA (Fast)
+                        </button>
+                      </div>
                     </div>
-                  {/each}
-                </div>
-              </div>
-            </div>
 
-            <!-- Selected Model Preview -->
-            <div class="space-y-3">
-              {#if models.length === 0}
-                <div class="text-center py-8">
-                  <div
-                    class="text-xs text-white/70 font-mono tracking-wider uppercase"
-                  >
-                    Loading models...
-                  </div>
-                </div>
-              {:else if loadingPreviews}
-                <div class="text-center py-8">
-                  <div
-                    class="text-xs text-exo-yellow font-mono tracking-wider uppercase animate-pulse"
-                  >
-                    Loading preview...
-                  </div>
-                </div>
-              {:else}
-                {@const selectedModel = models.find(
-                  (m) => m.id === selectedModelId,
-                )}
-                {@const allPreviews = filteredPreviews()}
-                {#if selectedModel && allPreviews.length > 0}
-                  {@const downloadStatus = getModelDownloadStatus(
-                    selectedModel.id,
-                  )}
-                  {@const tags = modelTags()[selectedModel.id] || []}
-                  <div class="space-y-3">
-                    {#each allPreviews as apiPreview, i}
+                    <!-- Minimum Devices -->
+                    <div>
+                      <div class="text-xs text-white/50 font-mono mb-2">
+                        Minimum Devices:
+                      </div>
+                      <!-- Discrete slider track with drag support -->
+                      <!-- svelte-ignore a11y_no_static_element_interactions -->
                       <div
-                        role="group"
-                        onmouseenter={() => {
-                          if (apiPreview.memory_delta_by_node) {
-                            hoveredPreviewNodes = new Set(
-                              Object.entries(apiPreview.memory_delta_by_node)
-                                .filter(([, delta]) => (delta ?? 0) > 0)
-                                .map(([nodeId]) => nodeId),
-                            );
-                          }
-                        }}
-                        onmouseleave={() => (hoveredPreviewNodes = new Set())}
+                        bind:this={sliderTrackElement}
+                        class="relative h-16 cursor-pointer select-none px-2 pr-6"
+                        onmousedown={handleSliderMouseDown}
+                        ontouchstart={handleSliderTouchStart}
                       >
-                        <ModelCard
-                          model={selectedModel}
-                          isLaunching={launchingModelId === selectedModel.id}
-                          {downloadStatus}
-                          nodes={data?.nodes ?? {}}
-                          sharding={apiPreview.sharding}
-                          runtime={apiPreview.instance_meta}
-                          onLaunch={() =>
-                            launchInstance(selectedModel.id, apiPreview)}
-                          {tags}
-                          {apiPreview}
-                          modelIdOverride={apiPreview.model_id}
-                        />
+                        <!-- Track background -->
+                        <div
+                          class="absolute top-6 left-0 right-0 h-2 bg-exo-medium-gray/50 rounded-full"
+                        ></div>
+                        <!-- Active track (fills up to selected) -->
+                        {#if availableMinNodes > 1}
+                          <div
+                            class="absolute top-6 left-0 h-2 bg-white/30 rounded-full transition-all pointer-events-none"
+                            style="width: {((selectedMinNodes - 1) /
+                              (availableMinNodes - 1)) *
+                              100}%"
+                          ></div>
+                        {/if}
+                        <!-- Dots and labels for each device count -->
+                        {#each Array.from({ length: availableMinNodes }, (_, i) => i + 1) as n}
+                          {@const isValid = validMinNodeCounts().has(n)}
+                          {@const isSelected = selectedMinNodes === n}
+                          {@const position =
+                            availableMinNodes > 1
+                              ? ((n - 1) / (availableMinNodes - 1)) * 100
+                              : 50}
+                          <div
+                            class="absolute flex flex-col items-center pointer-events-none"
+                            style="left: {position}%; top: 0; transform: translateX(-50%);"
+                          >
+                            <span
+                              class="rounded-full transition-all {isSelected
+                                ? 'w-6 h-6 bg-exo-yellow shadow-[0_0_10px_rgba(255,215,0,0.6)]'
+                                : isValid
+                                  ? 'w-4 h-4 bg-exo-light-gray/70 mt-1'
+                                  : 'w-3 h-3 bg-exo-medium-gray/50 mt-1.5'}"
+                            ></span>
+                            <span
+                              class="text-sm font-mono mt-1.5 tabular-nums transition-colors {isSelected
+                                ? 'text-exo-yellow font-bold'
+                                : isValid
+                                  ? 'text-white/70'
+                                  : 'text-white/30'}">{n}</span
+                            >
+                          </div>
+                        {/each}
                       </div>
-                    {/each}
-                  </div>
-                {:else if selectedModel}
-                  <div class="text-center py-4">
-                    <div class="text-xs text-white/50 font-mono">
-                      No valid configurations for current settings
                     </div>
                   </div>
                 {/if}
-              {/if}
-            </div>
-          </div>
-        </aside>
-      </div>
-    {:else}
-      <!-- CHAT STATE: Chat + Mini-Map -->
-      <div class="flex-1 flex overflow-hidden">
-        <!-- Chat Area -->
-        <div
-          class="flex-1 flex flex-col min-w-0 overflow-hidden"
-          in:fade={{ duration: 300, delay: 100 }}
-        >
-          <div
-            class="flex-1 overflow-y-auto px-8 py-6"
-            bind:this={chatScrollRef}
-          >
-            <div class="max-w-7xl mx-auto">
-              <ChatMessages scrollParent={chatScrollRef} />
-            </div>
-          </div>
-
-          <div
-            class="flex-shrink-0 px-8 pb-6 pt-4 bg-gradient-to-t from-exo-black via-exo-black to-transparent"
-          >
-            <div class="max-w-7xl mx-auto">
-              <ChatForm
-                placeholder="Ask anything"
-                showModelSelector={true}
-                modelTasks={modelTasks()}
-                modelCapabilities={modelCapabilities()}
-              />
-            </div>
-          </div>
-        </div>
-
-        <!-- Right: Mini-Map Sidebar -->
-        {#if minimized}
-          <aside
-            class="w-80 border-l border-exo-yellow/20 bg-exo-dark-gray flex flex-col flex-shrink-0 overflow-y-auto"
-            in:fly={{ x: 100, duration: 400, easing: cubicInOut }}
-          >
-            <!-- Topology Section - clickable to go back to main view -->
-            <button
-              class="p-4 border-b border-exo-medium-gray/30 w-full text-left cursor-pointer hover:bg-exo-medium-gray/10 transition-colors"
-              onclick={handleGoHome}
-              title="Click to return to main topology view"
-            >
-              <div class="flex items-center justify-between mb-3">
-                <div
-                  class="text-xs text-exo-yellow tracking-[0.2em] uppercase flex items-center gap-2"
-                >
-                  <span
-                    class="w-1.5 h-1.5 bg-exo-yellow rounded-full status-pulse"
-                  ></span>
-                  TOPOLOGY
-                </div>
-                <span class="text-xs text-white/70 tabular-nums"
-                  >{nodeCount} {nodeCount === 1 ? "NODE" : "NODES"}</span
-                >
-              </div>
-
-              <div
-                class="relative aspect-square bg-exo-dark-gray rounded-lg overflow-hidden"
-              >
-                <TopologyGraph
-                  highlightedNodes={highlightedNodes()}
-                  filteredNodes={nodeFilter}
-                  onNodeClick={togglePreviewNodeFilter}
-                />
-
-                {@render clusterWarningsCompact()}
               </div>
-            </button>
 
-            <!-- Instances Section (only shown when instances exist) -->
-            {#if instanceCount > 0}
-              <div class="p-4 flex-1">
-                <!-- Panel Header -->
-                <div class="flex items-center gap-2 mb-4">
-                  <div
-                    class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
-                  ></div>
-                  <h3
-                    class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
-                  >
-                    Instances
-                  </h3>
-                  <div
-                    class="flex-1 h-px bg-gradient-to-r from-exo-yellow/30 to-transparent"
-                  ></div>
-                </div>
-                <div
-                  class="space-y-3 max-h-72 xl:max-h-96 overflow-y-auto overflow-x-hidden py-px pr-1"
-                >
-                  {#each unifiedDisplayItems as item (item.id)}
-                    {@const id = item.id}
-                    {@const instance = item.instance}
-                    {@const downloadInfo = instance
-                      ? getInstanceDownloadStatus(
-                          item.instanceId ?? id,
-                          instance,
-                        )
-                      : getMetaInstancePlacingStatus(id)}
-                    {@const metaData = item.isMetaInstance
-                      ? metaInstancesData[id]
-                      : null}
-                    {@const retryError =
-                      metaData?.lastFailureError && !downloadInfo.isFailed
-                        ? metaData.consecutiveFailures > 0
-                          ? `(${((metaData.consecutiveFailures - 1) % 3) + 1}/3) ${metaData.lastFailureError}`
-                          : metaData.lastFailureError
-                        : null}
-                    {@const statusText = downloadInfo.statusText}
-                    {@const isDownloading = downloadInfo.isDownloading}
-                    {@const isFailed =
-                      statusText === "FAILED" ||
-                      statusText === "PLACEMENT FAILED"}
-                    {@const isLoading =
-                      statusText.startsWith("LOADING") ||
-                      statusText.startsWith("WARMING UP") ||
-                      statusText === "WAITING" ||
-                      statusText === "PLACING" ||
-                      statusText.startsWith("RETRYING")}
-                    {@const isReady =
-                      statusText === "READY" || statusText === "LOADED"}
-                    {@const isRunning = statusText === "RUNNING"}
-                    <!-- Instance Card -->
-                    {@const instanceModelId = item.modelId}
-                    {@const instanceInfo = instance
-                      ? getInstanceInfo(instance)
-                      : {
-                          instanceType:
-                            item.instanceMeta === "MlxRing"
-                              ? "MLX Ring"
-                              : item.instanceMeta === "MlxJaccl"
-                                ? "MLX RDMA"
-                                : "Unknown",
-                          sharding: item.sharding ?? "Unknown",
-                          nodeNames: [] as string[],
-                          nodeIds: [] as string[],
-                          nodeCount: 0,
-                        }}
-                    {@const instanceConnections = instance
-                      ? getInstanceConnections(instance)
-                      : []}
+              <!-- Selected Model Preview -->
+              <div class="space-y-3">
+                {#if models.length === 0}
+                  <div class="text-center py-8">
+                    <div
+                      class="text-xs text-white/70 font-mono tracking-wider uppercase"
+                    >
+                      Loading models...
+                    </div>
+                  </div>
+                {:else if loadingPreviews}
+                  <div class="text-center py-8">
+                    <div
+                      class="text-xs text-exo-yellow font-mono tracking-wider uppercase animate-pulse"
+                    >
+                      Loading preview...
+                    </div>
+                  </div>
+                {:else}
+                  {@const selectedModel = models.find(
+                    (m) => m.id === selectedModelId,
+                  )}
+                  {@const allPreviews = filteredPreviews()}
+                  {#if selectedModel && allPreviews.length > 0}
+                    {@const downloadStatus = getModelDownloadStatus(
+                      selectedModel.id,
+                    )}
+                    {@const tags = modelTags()[selectedModel.id] || []}
+                    <div class="space-y-3">
+                      {#each allPreviews as apiPreview, i}
+                        <div
+                          role="group"
+                          onmouseenter={() => {
+                            if (apiPreview.memory_delta_by_node) {
+                              hoveredPreviewNodes = new Set(
+                                Object.entries(apiPreview.memory_delta_by_node)
+                                  .filter(([, delta]) => (delta ?? 0) > 0)
+                                  .map(([nodeId]) => nodeId),
+                              );
+                            }
+                          }}
+                          onmouseleave={() => (hoveredPreviewNodes = new Set())}
+                        >
+                          <ModelCard
+                            model={selectedModel}
+                            isLaunching={launchingModelId === selectedModel.id}
+                            {downloadStatus}
+                            nodes={data?.nodes ?? {}}
+                            sharding={apiPreview.sharding}
+                            runtime={apiPreview.instance_meta}
+                            onLaunch={() =>
+                              launchInstance(selectedModel.id, apiPreview)}
+                            {tags}
+                            {apiPreview}
+                            modelIdOverride={apiPreview.model_id}
+                          />
+                        </div>
+                      {/each}
+                    </div>
+                  {:else if selectedModel}
+                    <div class="text-center py-4">
+                      <div class="text-xs text-white/50 font-mono">
+                        No valid configurations for current settings
+                      </div>
+                    </div>
+                  {/if}
+                {/if}
+              </div>
+            </div>
+          </aside>
+        </div>
+      {:else}
+        <!-- CHAT STATE: Chat + Mini-Map -->
+        <div class="flex-1 flex overflow-hidden">
+          <!-- Chat Area -->
+          <div
+            class="flex-1 flex flex-col min-w-0 overflow-hidden"
+            in:fade={{ duration: 300, delay: 100 }}
+          >
+            <div
+              class="flex-1 overflow-y-auto px-8 py-6"
+              bind:this={chatScrollRef}
+              role="log"
+              aria-live="polite"
+              aria-label="Chat messages"
+            >
+              <div class="max-w-7xl mx-auto">
+                <ChatMessages scrollParent={chatScrollRef} />
+              </div>
+            </div>
+
+            <div
+              class="flex-shrink-0 px-8 pb-6 pt-4 bg-gradient-to-t from-exo-black via-exo-black to-transparent"
+            >
+              <div class="max-w-7xl mx-auto">
+                <ChatForm
+                  placeholder="Ask anything"
+                  showModelSelector={true}
+                  modelTasks={modelTasks()}
+                  modelCapabilities={modelCapabilities()}
+                />
+              </div>
+            </div>
+          </div>
+
+          <!-- Right: Mini-Map Sidebar -->
+          {#if minimized}
+            <aside
+              class="w-80 border-l border-exo-yellow/20 bg-exo-dark-gray flex flex-col flex-shrink-0 overflow-y-auto"
+              in:fly={{ x: 100, duration: 400, easing: cubicInOut }}
+              aria-label="Cluster topology"
+            >
+              <!-- Topology Section - clickable to go back to main view -->
+              <button
+                class="p-4 border-b border-exo-medium-gray/30 w-full text-left cursor-pointer hover:bg-exo-medium-gray/10 transition-colors"
+                onclick={handleGoHome}
+                title="Click to return to main topology view"
+              >
+                <div class="flex items-center justify-between mb-3">
+                  <div
+                    class="text-xs text-exo-yellow tracking-[0.2em] uppercase flex items-center gap-2"
+                  >
+                    <span
+                      class="w-1.5 h-1.5 bg-exo-yellow rounded-full status-pulse"
+                    ></span>
+                    TOPOLOGY
+                  </div>
+                  <span class="text-xs text-white/70 tabular-nums"
+                    >{nodeCount} {nodeCount === 1 ? "NODE" : "NODES"}</span
+                  >
+                </div>
+
+                <div
+                  class="relative aspect-square bg-exo-dark-gray rounded-lg overflow-hidden"
+                >
+                  <TopologyGraph
+                    highlightedNodes={highlightedNodes()}
+                    filteredNodes={nodeFilter}
+                    onNodeClick={togglePreviewNodeFilter}
+                  />
+
+                  {@render clusterWarningsCompact()}
+                </div>
+              </button>
+
+              <!-- Instances Section (only shown when instances exist) -->
+              {#if instanceCount > 0}
+                <div class="p-4 flex-1">
+                  <!-- Panel Header -->
+                  <div class="flex items-center gap-2 mb-4">
                     <div
-                      class="relative group cursor-pointer"
-                      role="button"
-                      tabindex="0"
-                      onmouseenter={() =>
-                        (hoveredInstanceId = item.instanceId ?? id)}
-                      onmouseleave={() => (hoveredInstanceId = null)}
-                      onclick={() => {
-                        if (
-                          instanceModelId &&
-                          instanceModelId !== "Unknown" &&
-                          instanceModelId !== "Unknown Model"
-                        ) {
-                          setSelectedChatModel(instanceModelId);
-                        }
-                      }}
-                      onkeydown={(e) => {
-                        if (e.key === "Enter" || e.key === " ") {
+                      class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
+                    ></div>
+                    <h3
+                      class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
+                    >
+                      Instances
+                    </h3>
+                    <div
+                      class="flex-1 h-px bg-gradient-to-r from-exo-yellow/30 to-transparent"
+                    ></div>
+                  </div>
+                  <div
+                    class="space-y-3 max-h-72 xl:max-h-96 overflow-y-auto overflow-x-hidden py-px pr-1"
+                  >
+                    {#each Object.entries(instanceData) as [id, instance]}
+                      {@const downloadInfo = getInstanceDownloadStatus(
+                        id,
+                        instance,
+                      )}
+                      {@const statusText = downloadInfo.statusText}
+                      {@const isDownloading = downloadInfo.isDownloading}
+                      {@const isFailed = statusText === "FAILED"}
+                      {@const isLoading =
+                        statusText === "LOADING" ||
+                        statusText === "WARMING UP" ||
+                        statusText === "WAITING"}
+                      {@const isReady =
+                        statusText === "READY" || statusText === "LOADED"}
+                      {@const isRunning = statusText === "RUNNING"}
+                      <!-- Instance Card -->
+                      {@const instanceModelId = getInstanceModelId(instance)}
+                      {@const instanceInfo = getInstanceInfo(instance)}
+                      {@const instanceConnections =
+                        getInstanceConnections(instance)}
+                      <div
+                        class="relative group cursor-pointer"
+                        role="button"
+                        tabindex="0"
+                        onmouseenter={() => (hoveredInstanceId = id)}
+                        onmouseleave={() => (hoveredInstanceId = null)}
+                        onclick={() => {
                           if (
                             instanceModelId &&
                             instanceModelId !== "Unknown" &&
@@ -3657,439 +4970,466 @@
                           ) {
                             setSelectedChatModel(instanceModelId);
                           }
-                        }
-                      }}
-                    >
-                      <!-- Corner accents -->
-                      <div
-                        class="absolute -top-px -left-px w-2 h-2 border-l border-t {isDownloading
-                          ? 'border-blue-500/50'
-                          : isFailed
-                            ? 'border-red-500/50'
-                            : isLoading
-                              ? 'border-yellow-500/50'
-                              : isReady
-                                ? 'border-green-500/50'
-                                : 'border-teal-500/50'}"
-                      ></div>
-                      <div
-                        class="absolute -top-px -right-px w-2 h-2 border-r border-t {isDownloading
-                          ? 'border-blue-500/50'
-                          : isFailed
-                            ? 'border-red-500/50'
-                            : isLoading
-                              ? 'border-yellow-500/50'
-                              : isReady
-                                ? 'border-green-500/50'
-                                : 'border-teal-500/50'}"
-                      ></div>
-                      <div
-                        class="absolute -bottom-px -left-px w-2 h-2 border-l border-b {isDownloading
-                          ? 'border-blue-500/50'
-                          : isFailed
-                            ? 'border-red-500/50'
-                            : isLoading
-                              ? 'border-yellow-500/50'
-                              : isReady
-                                ? 'border-green-500/50'
-                                : 'border-teal-500/50'}"
-                      ></div>
-                      <div
-                        class="absolute -bottom-px -right-px w-2 h-2 border-r border-b {isDownloading
-                          ? 'border-blue-500/50'
-                          : isFailed
-                            ? 'border-red-500/50'
-                            : isLoading
-                              ? 'border-yellow-500/50'
-                              : isReady
-                                ? 'border-green-500/50'
-                                : 'border-teal-500/50'}"
-                      ></div>
-
-                      <div
-                        class="bg-exo-dark-gray/60 border border-l-2 {isDownloading
-                          ? 'border-blue-500/30 border-l-blue-400'
-                          : isFailed
-                            ? 'border-red-500/30 border-l-red-400'
-                            : isLoading
-                              ? 'border-exo-yellow/30 border-l-yellow-400'
-                              : isReady
-                                ? 'border-green-500/30 border-l-green-400'
-                                : 'border-teal-500/30 border-l-teal-400'} p-3"
+                        }}
+                        onkeydown={(e) => {
+                          if (e.key === "Enter" || e.key === " ") {
+                            if (
+                              instanceModelId &&
+                              instanceModelId !== "Unknown" &&
+                              instanceModelId !== "Unknown Model"
+                            ) {
+                              setSelectedChatModel(instanceModelId);
+                            }
+                          }
+                        }}
                       >
-                        <div class="flex justify-between items-start mb-2 pl-2">
-                          <div class="flex items-center gap-2">
-                            <div
-                              class="w-1.5 h-1.5 {isDownloading
-                                ? 'bg-blue-400 animate-pulse'
-                                : isFailed
-                                  ? 'bg-red-400'
-                                  : isLoading
-                                    ? 'bg-yellow-400 animate-pulse'
-                                    : isReady
-                                      ? 'bg-green-400'
-                                      : 'bg-teal-400'} rounded-full shadow-[0_0_6px_currentColor]"
-                            ></div>
-                            <span
-                              class="text-exo-light-gray font-mono text-sm tracking-wider"
-                              >{id.slice(0, 8).toUpperCase()}</span
-                            >
-                          </div>
-                          <button
-                            onclick={() =>
-                              item.isMetaInstance
-                                ? deleteMetaInstance(id)
-                                : deleteInstance(id)}
-                            class="text-xs px-2 py-1 font-mono tracking-wider uppercase border border-red-500/30 text-red-400 hover:bg-red-500/20 hover:text-red-400 hover:border-red-500/50 transition-all duration-200 cursor-pointer"
-                          >
-                            DELETE
-                          </button>
-                        </div>
-                        <div class="pl-2">
+                        <!-- Corner accents -->
+                        <div
+                          class="absolute -top-px -left-px w-2 h-2 border-l border-t {isDownloading
+                            ? 'border-blue-500/50'
+                            : isFailed
+                              ? 'border-red-500/50'
+                              : isLoading
+                                ? 'border-yellow-500/50'
+                                : isReady
+                                  ? 'border-green-500/50'
+                                  : 'border-teal-500/50'}"
+                        ></div>
+                        <div
+                          class="absolute -top-px -right-px w-2 h-2 border-r border-t {isDownloading
+                            ? 'border-blue-500/50'
+                            : isFailed
+                              ? 'border-red-500/50'
+                              : isLoading
+                                ? 'border-yellow-500/50'
+                                : isReady
+                                  ? 'border-green-500/50'
+                                  : 'border-teal-500/50'}"
+                        ></div>
+                        <div
+                          class="absolute -bottom-px -left-px w-2 h-2 border-l border-b {isDownloading
+                            ? 'border-blue-500/50'
+                            : isFailed
+                              ? 'border-red-500/50'
+                              : isLoading
+                                ? 'border-yellow-500/50'
+                                : isReady
+                                  ? 'border-green-500/50'
+                                  : 'border-teal-500/50'}"
+                        ></div>
+                        <div
+                          class="absolute -bottom-px -right-px w-2 h-2 border-r border-b {isDownloading
+                            ? 'border-blue-500/50'
+                            : isFailed
+                              ? 'border-red-500/50'
+                              : isLoading
+                                ? 'border-yellow-500/50'
+                                : isReady
+                                  ? 'border-green-500/50'
+                                  : 'border-teal-500/50'}"
+                        ></div>
+
+                        <div
+                          class="bg-exo-dark-gray/60 border border-l-2 {isDownloading
+                            ? 'border-blue-500/30 border-l-blue-400'
+                            : isFailed
+                              ? 'border-red-500/30 border-l-red-400'
+                              : isLoading
+                                ? 'border-exo-yellow/30 border-l-yellow-400'
+                                : isReady
+                                  ? 'border-green-500/30 border-l-green-400'
+                                  : 'border-teal-500/30 border-l-teal-400'} p-3"
+                        >
                           <div
-                            class="text-exo-yellow text-xs font-mono tracking-wide truncate"
+                            class="flex justify-between items-start mb-2 pl-2"
                           >
-                            {instanceModelId}
-                          </div>
-                          <div class="text-white/60 text-xs font-mono">
-                            Strategy: <span class="text-white/80"
-                              >{instanceInfo.sharding} ({instanceInfo.instanceType})</span
+                            <div class="flex items-center gap-2">
+                              <div
+                                class="w-1.5 h-1.5 {isDownloading
+                                  ? 'bg-blue-400 animate-pulse'
+                                  : isFailed
+                                    ? 'bg-red-400'
+                                    : isLoading
+                                      ? 'bg-yellow-400 animate-pulse'
+                                      : isReady
+                                        ? 'bg-green-400'
+                                        : 'bg-teal-400'} rounded-full shadow-[0_0_6px_currentColor]"
+                              ></div>
+                              <span
+                                class="text-exo-light-gray font-mono text-sm tracking-wider"
+                                >{id.slice(0, 8).toUpperCase()}</span
+                              >
+                            </div>
+                            <button
+                              onclick={() => deleteInstance(id)}
+                              class="text-xs px-2 py-1 font-mono tracking-wider uppercase border border-red-500/30 text-red-400 hover:bg-red-500/20 hover:text-red-400 hover:border-red-500/50 transition-all duration-200 cursor-pointer"
                             >
+                              DELETE
+                            </button>
                           </div>
-                          {#if instanceModelId && instanceModelId !== "Unknown" && instanceModelId !== "Unknown Model"}
-                            <a
-                              class="inline-flex items-center gap-1 text-[11px] text-white/60 hover:text-exo-yellow transition-colors mt-1"
-                              href={`https://huggingface.co/${instanceModelId}`}
-                              target="_blank"
-                              rel="noreferrer noopener"
-                              aria-label="View model on Hugging Face"
+                          <div class="pl-2">
+                            <div
+                              class="text-exo-yellow text-xs font-mono tracking-wide truncate"
                             >
-                              <span>Hugging Face</span>
-                              <svg
-                                class="w-3.5 h-3.5"
-                                viewBox="0 0 24 24"
-                                fill="none"
-                                stroke="currentColor"
-                                stroke-width="2"
-                                stroke-linecap="round"
-                                stroke-linejoin="round"
-                              >
-                                <path d="M14 3h7v7" />
-                                <path d="M10 14l11-11" />
-                                <path
-                                  d="M21 14v6a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1h6"
-                                />
-                              </svg>
-                            </a>
-                          {/if}
-                          {#if instanceInfo.nodeNames.length > 0}
-                            <div class="text-white/60 text-xs font-mono">
-                              {instanceInfo.nodeNames.join(", ")}
+                              {getInstanceModelId(instance)}
                             </div>
-                          {/if}
-                          {#if debugEnabled && instanceConnections.length > 0}
-                            <div class="mt-2 space-y-1">
-                              {#each instanceConnections as conn}
-                                <div
-                                  class="text-[11px] leading-snug font-mono text-white/70"
-                                >
-                                  <span
-                                    >{conn.from} -> {conn.to}: {conn.ip}</span
-                                  >
-                                  <span
-                                    class={conn.missingIface
-                                      ? "text-red-400"
-                                      : "text-white/60"}
-                                  >
-                                    ({conn.ifaceLabel})</span
-                                  >
-                                </div>
-                              {/each}
+                            <div
+                              class="flex items-center gap-2 text-white/60 text-xs font-mono"
+                            >
+                              <span
+                                >{instanceInfo.sharding} &middot; {instanceInfo.instanceType}</span
+                              >
+                              <span
+                                class="px-1.5 py-0.5 text-[10px] tracking-wider uppercase rounded transition-all duration-300 {isDownloading
+                                  ? 'bg-blue-500/15 text-blue-400'
+                                  : isFailed
+                                    ? 'bg-red-500/15 text-red-400'
+                                    : isLoading
+                                      ? 'bg-yellow-500/15 text-yellow-400'
+                                      : isReady
+                                        ? 'bg-green-500/15 text-green-400'
+                                        : 'bg-teal-500/15 text-teal-400'}"
+                              >
+                                {statusText}
+                              </span>
                             </div>
-                          {/if}
-
-                          <!-- Download Progress -->
-                          {#if downloadInfo.isDownloading && downloadInfo.progress}
-                            <div class="mt-2 space-y-1">
-                              <div
-                                class="flex justify-between text-xs font-mono"
+                            {#if instanceModelId && instanceModelId !== "Unknown" && instanceModelId !== "Unknown Model"}
+                              <a
+                                class="inline-flex items-center gap-1 text-[11px] text-white/60 hover:text-exo-yellow transition-colors mt-1"
+                                href={`https://huggingface.co/${instanceModelId}`}
+                                target="_blank"
+                                rel="noreferrer noopener"
+                                aria-label="View model on Hugging Face"
                               >
-                                <span class="text-blue-400"
-                                  >{downloadInfo.progress.percentage.toFixed(
-                                    1,
-                                  )}%</span
-                                >
-                                <span class="text-exo-light-gray"
-                                  >{formatBytes(
-                                    downloadInfo.progress.downloadedBytes,
-                                  )}/{formatBytes(
-                                    downloadInfo.progress.totalBytes,
-                                  )}</span
+                                <span>Hugging Face</span>
+                                <svg
+                                  class="w-3.5 h-3.5"
+                                  viewBox="0 0 24 24"
+                                  fill="none"
+                                  stroke="currentColor"
+                                  stroke-width="2"
+                                  stroke-linecap="round"
+                                  stroke-linejoin="round"
                                 >
+                                  <path d="M14 3h7v7" />
+                                  <path d="M10 14l11-11" />
+                                  <path
+                                    d="M21 14v6a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1h6"
+                                  />
+                                </svg>
+                              </a>
+                            {/if}
+                            {#if instanceInfo.nodeNames.length > 0}
+                              <div class="text-white/60 text-xs font-mono">
+                                {instanceInfo.nodeNames.join(", ")}
                               </div>
-                              <div
-                                class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
-                              >
-                                <div
-                                  class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
-                                  style="width: {downloadInfo.progress
-                                    .percentage}%"
-                                ></div>
+                            {/if}
+                            {#if debugEnabled && instanceConnections.length > 0}
+                              <div class="mt-2 space-y-1">
+                                {#each instanceConnections as conn}
+                                  <div
+                                    class="text-[11px] leading-snug font-mono text-white/70"
+                                  >
+                                    <span
+                                      >{conn.from} -> {conn.to}: {conn.ip}</span
+                                    >
+                                    <span
+                                      class={conn.missingIface
+                                        ? "text-red-400"
+                                        : "text-white/60"}
+                                    >
+                                      ({conn.ifaceLabel})</span
+                                    >
+                                  </div>
+                                {/each}
                               </div>
-                              <div
-                                class="flex justify-between text-xs font-mono text-exo-light-gray"
-                              >
-                                <span
-                                  >{formatSpeed(
-                                    downloadInfo.progress.speed,
-                                  )}</span
+                            {/if}
+
+                            <!-- Download Progress -->
+                            {#if downloadInfo.isDownloading && downloadInfo.progress}
+                              <div class="mt-2 space-y-1">
+                                <div
+                                  class="flex justify-between text-xs font-mono"
                                 >
-                                <span
-                                  >ETA: {formatEta(
-                                    downloadInfo.progress.etaMs,
-                                  )}</span
+                                  <span class="text-blue-400"
+                                    >{downloadInfo.progress.percentage.toFixed(
+                                      1,
+                                    )}%</span
+                                  >
+                                  <span class="text-exo-light-gray"
+                                    >{formatBytes(
+                                      downloadInfo.progress.downloadedBytes,
+                                    )}/{formatBytes(
+                                      downloadInfo.progress.totalBytes,
+                                    )}</span
+                                  >
+                                </div>
+                                <div
+                                  class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
                                 >
-                                <span
-                                  >{downloadInfo.progress
-                                    .completedFiles}/{downloadInfo.progress
-                                    .totalFiles} files</span
+                                  <div
+                                    class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
+                                    style="width: {downloadInfo.progress
+                                      .percentage}%"
+                                  ></div>
+                                </div>
+                                <div
+                                  class="flex justify-between text-xs font-mono text-exo-light-gray"
                                 >
+                                  <span
+                                    >{formatSpeed(
+                                      downloadInfo.progress.speed,
+                                    )}</span
+                                  >
+                                  <span
+                                    >ETA: {formatEta(
+                                      downloadInfo.progress.etaMs,
+                                    )}</span
+                                  >
+                                  <span
+                                    >{downloadInfo.progress
+                                      .completedFiles}/{downloadInfo.progress
+                                      .totalFiles} files</span
+                                  >
+                                </div>
                               </div>
-                            </div>
-                            {#if downloadInfo.perNode.length > 0}
-                              <div
-                                class="mt-2 space-y-2 max-h-48 overflow-y-auto pr-1"
-                              >
-                                {#each downloadInfo.perNode as nodeProg}
-                                  {@const nodePercent = Math.min(
-                                    100,
-                                    Math.max(0, nodeProg.progress.percentage),
-                                  )}
-                                  {@const isExpanded =
-                                    instanceDownloadExpandedNodes.has(
-                                      nodeProg.nodeId,
+                              {#if downloadInfo.perNode.length > 0}
+                                <div
+                                  class="mt-2 space-y-2 max-h-48 overflow-y-auto pr-1"
+                                >
+                                  {#each downloadInfo.perNode as nodeProg}
+                                    {@const nodePercent = Math.min(
+                                      100,
+                                      Math.max(0, nodeProg.progress.percentage),
                                     )}
-                                  <div
-                                    class="rounded border border-exo-medium-gray/40 bg-exo-black/30 p-2"
-                                  >
-                                    <button
-                                      type="button"
-                                      class="w-full text-left space-y-1.5"
-                                      onclick={() =>
-                                        toggleInstanceDownloadDetails(
-                                          nodeProg.nodeId,
-                                        )}
+                                    {@const isExpanded =
+                                      instanceDownloadExpandedNodes.has(
+                                        nodeProg.nodeId,
+                                      )}
+                                    <div
+                                      class="rounded border border-exo-medium-gray/40 bg-exo-black/30 p-2"
                                     >
-                                      <div
-                                        class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
+                                      <button
+                                        type="button"
+                                        class="w-full text-left space-y-1.5"
+                                        onclick={() =>
+                                          toggleInstanceDownloadDetails(
+                                            nodeProg.nodeId,
+                                          )}
                                       >
-                                        <span
-                                          class="text-white/80 truncate pr-2"
-                                          >{nodeProg.nodeName}</span
-                                        >
-                                        <span
-                                          class="flex items-center gap-1 text-blue-300"
+                                        <div
+                                          class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
                                         >
-                                          {nodePercent.toFixed(1)}%
-                                          <svg
-                                            class="w-3 h-3 text-exo-light-gray"
-                                            viewBox="0 0 20 20"
-                                            fill="none"
-                                            stroke="currentColor"
-                                            stroke-width="2"
+                                          <span
+                                            class="text-white/80 truncate pr-2"
+                                            >{nodeProg.nodeName}</span
                                           >
-                                            <path
-                                              d="M6 8l4 4 4-4"
-                                              class={isExpanded
-                                                ? "transform rotate-180 origin-center transition-transform duration-150"
-                                                : "transition-transform duration-150"}
-                                            ></path>
-                                          </svg>
-                                        </span>
-                                      </div>
-                                      <div
-                                        class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
-                                      >
+                                          <span
+                                            class="flex items-center gap-1 text-blue-300"
+                                          >
+                                            {nodePercent.toFixed(1)}%
+                                            <svg
+                                              class="w-3 h-3 text-exo-light-gray"
+                                              viewBox="0 0 20 20"
+                                              fill="none"
+                                              stroke="currentColor"
+                                              stroke-width="2"
+                                            >
+                                              <path
+                                                d="M6 8l4 4 4-4"
+                                                class={isExpanded
+                                                  ? "transform rotate-180 origin-center transition-transform duration-150"
+                                                  : "transition-transform duration-150"}
+                                              ></path>
+                                            </svg>
+                                          </span>
+                                        </div>
                                         <div
-                                          class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
-                                          style="width: {nodePercent.toFixed(
-                                            1,
-                                          )}%"
-                                        ></div>
-                                      </div>
-                                      <div
-                                        class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
-                                      >
-                                        <span
-                                          >{formatBytes(
-                                            nodeProg.progress.downloadedBytes,
-                                          )} / {formatBytes(
-                                            nodeProg.progress.totalBytes,
-                                          )}</span
+                                          class="relative h-1.5 bg-exo-black/60 rounded-sm overflow-hidden"
                                         >
-                                        <span
-                                          >{formatSpeed(
-                                            nodeProg.progress.speed,
-                                          )} • ETA {formatEta(
-                                            nodeProg.progress.etaMs,
-                                          )}</span
-                                        >
-                                      </div>
-                                    </button>
-
-                                    {#if isExpanded}
-                                      <div class="mt-2 space-y-1.5">
-                                        {#if nodeProg.progress.files.length === 0}
                                           <div
-                                            class="text-[11px] font-mono text-exo-light-gray/70"
+                                            class="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-300"
+                                            style="width: {nodePercent.toFixed(
+                                              1,
+                                            )}%"
+                                          ></div>
+                                        </div>
+                                        <div
+                                          class="flex items-center justify-between text-[11px] font-mono text-exo-light-gray"
+                                        >
+                                          <span
+                                            >{formatBytes(
+                                              nodeProg.progress.downloadedBytes,
+                                            )} / {formatBytes(
+                                              nodeProg.progress.totalBytes,
+                                            )}</span
                                           >
-                                            No file details reported.
-                                          </div>
-                                        {:else}
-                                          {#each nodeProg.progress.files as f}
-                                            {@const filePercent = Math.min(
-                                              100,
-                                              Math.max(0, f.percentage ?? 0),
-                                            )}
-                                            {@const isFileComplete =
-                                              filePercent >= 100}
+                                          <span
+                                            >{formatSpeed(
+                                              nodeProg.progress.speed,
+                                            )} • ETA {formatEta(
+                                              nodeProg.progress.etaMs,
+                                            )}</span
+                                          >
+                                        </div>
+                                      </button>
+
+                                      {#if isExpanded}
+                                        <div class="mt-2 space-y-1.5">
+                                          {#if nodeProg.progress.files.length === 0}
                                             <div
-                                              class="rounded border border-exo-medium-gray/30 bg-exo-black/40 p-2"
+                                              class="text-[11px] font-mono text-exo-light-gray/70"
                                             >
+                                              No file details reported.
+                                            </div>
+                                          {:else}
+                                            {#each nodeProg.progress.files as f}
+                                              {@const filePercent = Math.min(
+                                                100,
+                                                Math.max(0, f.percentage ?? 0),
+                                              )}
+                                              {@const isFileComplete =
+                                                filePercent >= 100}
                                               <div
-                                                class="flex items-center justify-between text-[10px] font-mono text-exo-light-gray/90"
+                                                class="rounded border border-exo-medium-gray/30 bg-exo-black/40 p-2"
                                               >
-                                                <span class="truncate pr-2"
-                                                  >{f.name}</span
-                                                >
-                                                <span
-                                                  class={isFileComplete
-                                                    ? "text-green-400"
-                                                    : "text-white/80"}
-                                                  >{filePercent.toFixed(
-                                                    1,
-                                                  )}%</span
+                                                <div
+                                                  class="flex items-center justify-between text-[10px] font-mono text-exo-light-gray/90"
                                                 >
-                                              </div>
-                                              <div
-                                                class="relative h-1 bg-exo-black/60 rounded-sm overflow-hidden mt-1"
-                                              >
+                                                  <span class="truncate pr-2"
+                                                    >{f.name}</span
+                                                  >
+                                                  <span
+                                                    class={isFileComplete
+                                                      ? "text-green-400"
+                                                      : "text-white/80"}
+                                                    >{filePercent.toFixed(
+                                                      1,
+                                                    )}%</span
+                                                  >
+                                                </div>
                                                 <div
-                                                  class="absolute inset-y-0 left-0 bg-gradient-to-r {isFileComplete
-                                                    ? 'from-green-500 to-green-400'
-                                                    : 'from-exo-yellow to-exo-yellow/70'} transition-all duration-300"
-                                                  style="width: {filePercent.toFixed(
-                                                    1,
-                                                  )}%"
-                                                ></div>
-                                              </div>
-                                              <div
-                                                class="flex items-center justify-between text-[10px] text-exo-light-gray/70 mt-0.5"
-                                              >
-                                                <span
-                                                  >{formatBytes(
-                                                    f.downloadedBytes,
-                                                  )} / {formatBytes(
-                                                    f.totalBytes,
-                                                  )}</span
+                                                  class="relative h-1 bg-exo-black/60 rounded-sm overflow-hidden mt-1"
                                                 >
-                                                <span
-                                                  >{formatSpeed(f.speed)} • ETA {formatEta(
-                                                    f.etaMs,
-                                                  )}</span
+                                                  <div
+                                                    class="absolute inset-y-0 left-0 bg-gradient-to-r {isFileComplete
+                                                      ? 'from-green-500 to-green-400'
+                                                      : 'from-exo-yellow to-exo-yellow/70'} transition-all duration-300"
+                                                    style="width: {filePercent.toFixed(
+                                                      1,
+                                                    )}%"
+                                                  ></div>
+                                                </div>
+                                                <div
+                                                  class="flex items-center justify-between text-[10px] text-exo-light-gray/70 mt-0.5"
                                                 >
+                                                  <span
+                                                    >{formatBytes(
+                                                      f.downloadedBytes,
+                                                    )} / {formatBytes(
+                                                      f.totalBytes,
+                                                    )}</span
+                                                  >
+                                                  <span
+                                                    >{formatSpeed(f.speed)} • ETA
+                                                    {formatEta(f.etaMs)}</span
+                                                  >
+                                                </div>
                                               </div>
-                                            </div>
-                                          {/each}
-                                        {/if}
-                                      </div>
-                                    {/if}
-                                  </div>
-                                {/each}
-                              </div>
-                            {/if}
-                            <div
-                              class="text-xs text-blue-400 font-mono tracking-wider mt-1"
-                            >
-                              DOWNLOADING
-                            </div>
-                          {:else}
-                            <div
-                              class="text-xs {getStatusColor(
-                                downloadInfo.statusText,
-                              )} font-mono tracking-wider mt-1"
-                            >
-                              {downloadInfo.statusText}
-                            </div>
-                            {#if downloadInfo.isFailed && downloadInfo.errorMessage}
-                              <div
-                                class="text-xs text-red-400/80 font-mono mt-1 break-words"
-                              >
-                                {downloadInfo.errorMessage}
-                              </div>
-                            {:else if retryError}
-                              <div
-                                class="text-xs text-orange-400/80 font-mono mt-1 break-words"
-                              >
-                                Retrying after error: {retryError}
+                                            {/each}
+                                          {/if}
+                                        </div>
+                                      {/if}
+                                    </div>
+                                  {/each}
+                                </div>
+                              {/if}
+                              <div class="mt-2 space-y-1">
+                                <div
+                                  class="text-xs text-blue-400 font-mono tracking-wider"
+                                >
+                                  DOWNLOADING
+                                </div>
+                                <p
+                                  class="text-[11px] text-white/50 leading-relaxed"
+                                >
+                                  Downloading model files. This runs locally on
+                                  your device and needs to finish before you can
+                                  chat.
+                                </p>
                               </div>
-                            {/if}
-                            {#if downloadInfo.perNodeStatus.length > 1 && (statusText.startsWith("LOADING") || statusText.startsWith("WARMING UP") || statusText === "WAITING" || statusText === "INITIALIZING")}
-                              <div class="mt-1.5 space-y-0.5">
-                                {#each downloadInfo.perNodeStatus as node}
-                                  <div
-                                    class="flex items-center justify-between text-[10px] font-mono"
+                            {:else}
+                              <div class="mt-1 space-y-1">
+                                <div
+                                  class="text-xs {getStatusColor(
+                                    downloadInfo.statusText,
+                                  )} font-mono tracking-wider"
+                                >
+                                  {downloadInfo.statusText}
+                                </div>
+                                {#if isLoading}
+                                  <p
+                                    class="text-[11px] text-white/50 leading-relaxed"
                                   >
-                                    <span class="text-white/60 truncate pr-2"
-                                      >{node.nodeName}</span
-                                    >
-                                    <span
-                                      class={getStatusColor(
-                                        node.status.toUpperCase(),
-                                      )}>{node.status}</span
-                                    >
-                                  </div>
-                                {/each}
+                                    Loading model into memory for fast
+                                    inference...
+                                  </p>
+                                {:else if isReady || isRunning}
+                                  <p
+                                    class="text-[11px] text-green-400/70 leading-relaxed"
+                                  >
+                                    Ready to chat! Type a message below.
+                                  </p>
+                                {/if}
                               </div>
+                              {#if downloadInfo.isFailed && downloadInfo.errorMessage}
+                                <div
+                                  class="text-xs text-red-400/80 font-mono mt-1 break-words"
+                                >
+                                  {downloadInfo.errorMessage}
+                                </div>
+                              {/if}
                             {/if}
-                          {/if}
+                          </div>
                         </div>
                       </div>
-                    </div>
-                  {/each}
+                    {/each}
+                  </div>
                 </div>
-              </div>
-            {/if}
-          </aside>
-        {/if}
-      </div>
-    {/if}
-  </main>
+              {/if}
+            </aside>
+          {/if}
+        </div>
+      {/if}
+    </main>
+  {/if}
 </div>
 
-<ModelPickerModal
-  isOpen={isModelPickerOpen}
-  {models}
-  {selectedModelId}
-  favorites={favoritesSet}
-  {recentModelIds}
-  hasRecents={showRecentsTab}
-  existingModelIds={new Set(models.map((m) => m.id))}
-  canModelFit={(modelId) => {
-    const model = models.find((m) => m.id === modelId);
-    return model ? hasEnoughMemory(model) : false;
-  }}
-  getModelFitStatus={(modelId): ModelMemoryFitStatus => {
-    const model = models.find((m) => m.id === modelId);
-    return model ? getModelMemoryFitStatus(model) : "too_large";
-  }}
-  onSelect={handleModelPickerSelect}
-  onClose={() => (isModelPickerOpen = false)}
-  onToggleFavorite={toggleFavorite}
-  onAddModel={addModelFromPicker}
-  onDeleteModel={deleteCustomModel}
-  totalMemoryGB={clusterMemory().total / (1024 * 1024 * 1024)}
-  usedMemoryGB={clusterMemory().used / (1024 * 1024 * 1024)}
-  {downloadsData}
-  topologyNodes={data?.nodes}
-/>
+{#if !showOnboarding}
+  <ModelPickerModal
+    isOpen={isModelPickerOpen}
+    {models}
+    {selectedModelId}
+    favorites={favoritesSet}
+    {recentModelIds}
+    hasRecents={showRecentsTab}
+    existingModelIds={new Set(models.map((m) => m.id))}
+    canModelFit={(modelId) => {
+      const model = models.find((m) => m.id === modelId);
+      return model ? hasEnoughMemory(model) : false;
+    }}
+    getModelFitStatus={(modelId): ModelMemoryFitStatus => {
+      const model = models.find((m) => m.id === modelId);
+      return model ? getModelMemoryFitStatus(model) : "too_large";
+    }}
+    onSelect={handleModelPickerSelect}
+    onClose={() => (isModelPickerOpen = false)}
+    onToggleFavorite={toggleFavorite}
+    onAddModel={addModelFromPicker}
+    onDeleteModel={deleteCustomModel}
+    totalMemoryGB={clusterMemory().total / (1024 * 1024 * 1024)}
+    usedMemoryGB={clusterMemory().used / (1024 * 1024 * 1024)}
+    {downloadsData}
+    topologyNodes={data?.nodes}
+  />
+{/if}
diff --git a/packaging/dmg/background.png b/packaging/dmg/background.png
new file mode 100644
index 00000000..5e56d8ab
Binary files /dev/null and b/packaging/dmg/background.png differ
diff --git a/packaging/dmg/create-dmg.sh b/packaging/dmg/create-dmg.sh
new file mode 100755
index 00000000..cd68b2a7
--- /dev/null
+++ b/packaging/dmg/create-dmg.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+# create-dmg.sh — Build a polished macOS DMG installer for EXO
+#
+# Usage:
+#   ./packaging/dmg/create-dmg.sh <app-path> <output-dmg> [volume-name]
+#
+# Example:
+#   ./packaging/dmg/create-dmg.sh output/EXO.app EXO-1.0.0.dmg "EXO"
+#
+# Creates a DMG with:
+#   - Custom background image with drag-to-Applications arrow
+#   - App icon on left, Applications alias on right
+#   - Proper window size and icon positioning
+set -euo pipefail
+
+APP_PATH="${1:?Usage: create-dmg.sh <app-path> <output-dmg> [volume-name]}"
+OUTPUT_DMG="${2:?Usage: create-dmg.sh <app-path> <output-dmg> [volume-name]}"
+VOLUME_NAME="${3:-EXO}"
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+BACKGROUND_SCRIPT="${SCRIPT_DIR}/generate-background.py"
+TEMP_DIR="$(mktemp -d)"
+DMG_STAGING="${TEMP_DIR}/dmg-root"
+TEMP_DMG="${TEMP_DIR}/temp.dmg"
+BACKGROUND_PNG="${TEMP_DIR}/background.png"
+
+cleanup() { rm -rf "$TEMP_DIR"; }
+trap cleanup EXIT
+
+echo "==> Creating DMG installer for ${VOLUME_NAME}"
+
+# ── Step 1: Generate background image ────────────────────────────────────────
+if command -v python3 &>/dev/null; then
+  python3 "$BACKGROUND_SCRIPT" "$BACKGROUND_PNG"
+  echo "    Background image generated"
+else
+  echo "    Warning: python3 not found, skipping custom background"
+  BACKGROUND_PNG=""
+fi
+
+# ── Step 2: Prepare staging directory ─────────────────────────────────────────
+mkdir -p "$DMG_STAGING"
+cp -R "$APP_PATH" "$DMG_STAGING/"
+ln -s /Applications "$DMG_STAGING/Applications"
+
+# ── Step 3: Create writable DMG ──────────────────────────────────────────────
+# Calculate required size (app size + 20MB headroom)
+APP_SIZE_KB=$(du -sk "$APP_PATH" | cut -f1)
+DMG_SIZE_KB=$((APP_SIZE_KB + 20480))
+
+hdiutil create \
+  -volname "$VOLUME_NAME" \
+  -size "${DMG_SIZE_KB}k" \
+  -fs HFS+ \
+  -layout SPUD \
+  "$TEMP_DMG"
+
+# ── Step 4: Mount and configure ──────────────────────────────────────────────
+MOUNT_DIR=$(hdiutil attach "$TEMP_DMG" -readwrite -noverify | awk -F'\t' '/Apple_HFS/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $NF); print $NF}')
+echo "    Mounted at: $MOUNT_DIR"
+
+# Copy contents
+cp -R "$DMG_STAGING/"* "$MOUNT_DIR/"
+
+# Add background image
+if [[ -n $BACKGROUND_PNG && -f $BACKGROUND_PNG ]]; then
+  mkdir -p "$MOUNT_DIR/.background"
+  cp "$BACKGROUND_PNG" "$MOUNT_DIR/.background/background.png"
+fi
+
+# ── Step 5: Configure window appearance via AppleScript ──────────────────────
+# Window: 800×400, app icon on left, Applications on right (matches Ollama layout)
+# Background image is 1600×740 (2× retina for 800×400 logical window).
+APP_NAME="$(basename "$APP_PATH")"
+
+osascript <<APPLESCRIPT
+tell application "Finder"
+    tell disk "$VOLUME_NAME"
+        open
+        set current view of container window to icon view
+        set toolbar visible of container window to false
+        set statusbar visible of container window to false
+        set bounds of container window to {200, 120, 1000, 520}
+        set opts to icon view options of container window
+        set icon size of opts to 128
+        set text size of opts to 12
+        set arrangement of opts to not arranged
+        if exists file ".background:background.png" then
+            set background picture of opts to file ".background:background.png"
+        end if
+        set position of item "$APP_NAME" of container window to {200, 190}
+        set position of item "Applications" of container window to {600, 190}
+        close
+        open
+        update without registering applications
+        delay 1
+        close
+    end tell
+end tell
+APPLESCRIPT
+
+echo "    Window layout configured"
+
+# Ensure Finder updates are flushed
+sync
+
+# ── Step 6: Finalise ─────────────────────────────────────────────────────────
+hdiutil detach "$MOUNT_DIR" -quiet
+hdiutil convert "$TEMP_DMG" -format UDZO -imagekey zlib-level=9 -o "$OUTPUT_DMG"
+
+echo "==> DMG created: $OUTPUT_DMG"
+echo "    Size: $(du -h "$OUTPUT_DMG" | cut -f1)"
diff --git a/packaging/dmg/generate-background.py b/packaging/dmg/generate-background.py
new file mode 100644
index 00000000..ac3dc649
--- /dev/null
+++ b/packaging/dmg/generate-background.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+"""Generate the DMG background image with a centered drag-to-Applications arrow.
+
+The output is a 1600×740 retina PNG (2× for 800×400 logical window).
+Icons are positioned at (200, 190) and (600, 190) in logical coordinates;
+the arrow is drawn centered between them.
+
+Usage:
+    python3 generate-background.py [output.png]
+
+If no output path is given, overwrites the bundled background.png in-place.
+"""
+
+from __future__ import annotations
+
+import math
+import sys
+from pathlib import Path
+
+from PIL import Image, ImageDraw
+
+# Retina dimensions (2× logical 800×400)
+WIDTH = 1600
+HEIGHT = 740
+
+# Icon positions in logical coords → retina coords
+# App icon at (200, 190), Applications at (600, 190)
+APP_X = 200 * 2  # 400
+APPS_X = 600 * 2  # 1200
+ICON_Y = 190 * 2  # 380
+
+# Arrow drawn between icons, slightly above icon center
+ARROW_START_X = APP_X + 160  # past the icon
+ARROW_END_X = APPS_X - 160  # before the Applications icon
+ARROW_Y = ICON_Y  # same height as icons
+ARROW_RISE = 120  # upward arc height
+
+
+def draw_arrow(draw: ImageDraw.ImageDraw) -> None:
+    """Draw a hand-drawn-style curved arrow from app icon toward Applications."""
+    color = (30, 30, 30)
+    line_width = 8
+
+    # Compute bezier curve points for a gentle upward arc
+    points: list[tuple[float, float]] = []
+    steps = 80
+    for i in range(steps + 1):
+        t = i / steps
+        # Quadratic bezier: start → control → end
+        cx = (ARROW_START_X + ARROW_END_X) / 2
+        cy = ARROW_Y - ARROW_RISE
+        x = (1 - t) ** 2 * ARROW_START_X + 2 * (1 - t) * t * cx + t**2 * ARROW_END_X
+        y = (1 - t) ** 2 * ARROW_Y + 2 * (1 - t) * t * cy + t**2 * ARROW_Y
+        points.append((x, y))
+
+    # Draw the curve as connected line segments
+    for i in range(len(points) - 1):
+        draw.line([points[i], points[i + 1]], fill=color, width=line_width)
+
+    # Arrowhead at the end
+    end_x, end_y = points[-1]
+    # Direction from second-to-last to last point
+    prev_x, prev_y = points[-3]
+    angle = math.atan2(end_y - prev_y, end_x - prev_x)
+    head_len = 36
+    head_angle = math.radians(25)
+
+    left_x = end_x - head_len * math.cos(angle - head_angle)
+    left_y = end_y - head_len * math.sin(angle - head_angle)
+    right_x = end_x - head_len * math.cos(angle + head_angle)
+    right_y = end_y - head_len * math.sin(angle + head_angle)
+
+    draw.polygon(
+        [(end_x, end_y), (left_x, left_y), (right_x, right_y)],
+        fill=color,
+    )
+
+
+def generate_background(output_path: str) -> None:
+    """Generate a white DMG background with a centered arrow."""
+    img = Image.new("RGBA", (WIDTH, HEIGHT), (255, 255, 255, 255))
+    draw = ImageDraw.Draw(img)
+    draw_arrow(draw)
+    img.save(output_path, "PNG")
+
+
+if __name__ == "__main__":
+    default_output = str(Path(__file__).parent / "background.png")
+    out = sys.argv[1] if len(sys.argv) >= 2 else default_output
+    generate_background(out)
+    print(f"Background image written to {out}")
diff --git a/src/exo/utils/banner.py b/src/exo/utils/banner.py
index ffdb5458..2742832e 100644
--- a/src/exo/utils/banner.py
+++ b/src/exo/utils/banner.py
@@ -1,8 +1,27 @@
+import logging
+import os
 import sys
+import webbrowser
+
+from exo.shared.constants import EXO_CONFIG_HOME
+
+logger = logging.getLogger(__name__)
+
+_FIRST_RUN_MARKER = EXO_CONFIG_HOME / ".dashboard_opened"
+
+
+def _is_first_run() -> bool:
+    return not _FIRST_RUN_MARKER.exists()
+
+
+def _mark_first_run_done() -> None:
+    _FIRST_RUN_MARKER.parent.mkdir(parents=True, exist_ok=True)
+    _FIRST_RUN_MARKER.touch()
 
 
 def print_startup_banner(port: int) -> None:
     dashboard_url = f"http://localhost:{port}"
+    first_run = _is_first_run()
     banner = f"""
 ╔═══════════════════════════════════════════════════════════════════════╗
 ║                                                                       ║
@@ -30,3 +49,14 @@ def print_startup_banner(port: int) -> None:
 """
 
     print(banner, file=sys.stderr)
+
+    if first_run:
+        # 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()

← facf2d4d Use custom fork that resolves GPU locks (#1489)  ·  back to Exo  ·  Revert "feat: better onboarding UX for new users (#1479)" c8997217 →