← back to Exo
fix: prevent stale loading state and conversation loss when switching chats (#1613)
bea64fe85e7abd7c62d376fdd651cfa20af40854 · 2026-02-24 11:08:31 -0800 · Alex Cheema
## Motivation
When navigating back to a previous conversation that used a different
model than the one currently loading, the UI incorrectly displayed the
other model's loading/download progress bar instead of the conversation
messages. Additionally, attempting to continue that old conversation by
sending a message would create an entirely new chat rather than
continuing the existing one.
## Changes
All changes in `dashboard/src/routes/+page.svelte`:
1. **Added fallthrough reset in the `chatLaunchState` restore
`$effect`**: When switching to a conversation whose model has no active
instance (not running, not downloading, not loading), the effect now
resets `chatLaunchState` to `"idle"` instead of leaving stale state from
a different model.
2. **Added `skipCreate` parameter to `launchModelForChat()`**: When
continuing an existing conversation (has messages),
`createConversation()` is skipped so the old conversation is preserved
rather than replaced with a new empty one.
3. **Reordered view conditional**: Progress views
(downloading/loading/launching) take priority, then conversation
messages (when idle with messages or model ready), then model selector
(idle with no messages). This ensures:
- Old conversations display normally when their model isn't running
- Download progress shows when relaunching a model for any conversation
- Model selector only appears when there are no messages
## Why It Works
- The `$effect` fallthrough prevents `chatLaunchState` from retaining a
stale value (e.g., `"downloading"`) from Model A when the user switches
to a conversation using Model B that has no active state.
- The `skipCreate` flag ensures `launchModelForChat` can be reused for
both new conversations (from model picker) and continuing existing ones
(from chat input) without always creating a new conversation.
- The reordered view logic ensures each state maps to the correct UI:
active launch → progress, existing messages → chat view, nothing → model
selector.
## Test Plan
### Manual Testing
- Load an old conversation while a different model is downloading →
should see conversation messages, not the other model's loading bar
- Send a message from that old conversation → model should
launch/download with progress shown → conversation continues with the
response appended
- Select a new model from the picker → should still create a new
conversation as before
- New conversation with model download → progress bar shows correctly
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Files touched
M dashboard/src/routes/+page.svelte
Diff
commit bea64fe85e7abd7c62d376fdd651cfa20af40854
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Tue Feb 24 11:08:31 2026 -0800
fix: prevent stale loading state and conversation loss when switching chats (#1613)
## Motivation
When navigating back to a previous conversation that used a different
model than the one currently loading, the UI incorrectly displayed the
other model's loading/download progress bar instead of the conversation
messages. Additionally, attempting to continue that old conversation by
sending a message would create an entirely new chat rather than
continuing the existing one.
## Changes
All changes in `dashboard/src/routes/+page.svelte`:
1. **Added fallthrough reset in the `chatLaunchState` restore
`$effect`**: When switching to a conversation whose model has no active
instance (not running, not downloading, not loading), the effect now
resets `chatLaunchState` to `"idle"` instead of leaving stale state from
a different model.
2. **Added `skipCreate` parameter to `launchModelForChat()`**: When
continuing an existing conversation (has messages),
`createConversation()` is skipped so the old conversation is preserved
rather than replaced with a new empty one.
3. **Reordered view conditional**: Progress views
(downloading/loading/launching) take priority, then conversation
messages (when idle with messages or model ready), then model selector
(idle with no messages). This ensures:
- Old conversations display normally when their model isn't running
- Download progress shows when relaunching a model for any conversation
- Model selector only appears when there are no messages
## Why It Works
- The `$effect` fallthrough prevents `chatLaunchState` from retaining a
stale value (e.g., `"downloading"`) from Model A when the user switches
to a conversation using Model B that has no active state.
- The `skipCreate` flag ensures `launchModelForChat` can be reused for
both new conversations (from model picker) and continuing existing ones
(from chat input) without always creating a new conversation.
- The reordered view logic ensures each state maps to the correct UI:
active launch → progress, existing messages → chat view, nothing → model
selector.
## Test Plan
### Manual Testing
- Load an old conversation while a different model is downloading →
should see conversation messages, not the other model's loading bar
- Send a message from that old conversation → model should
launch/download with progress shown → conversation continues with the
response appended
- Select a new model from the picker → should still create a new
conversation as before
- New conversation with model download → progress bar shows correctly
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
---
dashboard/src/routes/+page.svelte | 98 +++++++++++++++++++++------------------
1 file changed, 54 insertions(+), 44 deletions(-)
diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte
index e02d3b88..43973cd6 100644
--- a/dashboard/src/routes/+page.svelte
+++ b/dashboard/src/routes/+page.svelte
@@ -42,6 +42,7 @@
setSelectedChatModel,
selectedChatModel,
sendMessage,
+ messages,
debugMode,
toggleDebugMode,
topologyOnlyMode,
@@ -2517,6 +2518,11 @@
return;
}
}
+
+ // Fallthrough: model exists but has no active instance/download/loading state
+ chatLaunchState = "idle";
+ pendingChatModelId = null;
+ selectedChatCategory = null;
});
// Suggested prompts per category
@@ -2640,7 +2646,11 @@
}
// Launch a model for seamless chat
- async function launchModelForChat(modelId: string, category: string) {
+ async function launchModelForChat(
+ modelId: string,
+ category: string,
+ skipCreate = false,
+ ) {
userForcedIdle = false;
pendingChatModelId = modelId;
selectedChatCategory = category;
@@ -2648,7 +2658,7 @@
// Check if already running — skip straight to chat
if (hasRunningInstance(modelId)) {
setSelectedChatModel(modelId);
- createConversation();
+ if (!skipCreate) createConversation();
chatLaunchState = "ready";
return;
}
@@ -2657,7 +2667,7 @@
if (hasExistingInstance(modelId)) {
setSelectedChatModel(modelId);
pendingChatModelId = modelId;
- createConversation();
+ if (!skipCreate) createConversation();
const dlStatus = getModelDownloadStatus(modelId);
if (dlStatus.isDownloading) {
chatLaunchState = "downloading";
@@ -2710,7 +2720,7 @@
setSelectedChatModel(modelId);
recordRecentLaunch(modelId);
- createConversation();
+ if (!skipCreate) createConversation();
chatLaunchState = "downloading";
} catch (error) {
addToast({ type: "error", message: `Network error: ${error}` });
@@ -3025,7 +3035,7 @@
if (model) {
pendingAutoMessage = { content, files };
userForcedIdle = false;
- launchModelForChat(model, "picker");
+ launchModelForChat(model, "picker", messages().length > 0);
return;
}
@@ -5809,43 +5819,7 @@
class="flex-1 flex flex-col min-w-0 overflow-hidden"
in:fade={{ duration: 300, delay: 100 }}
>
- {#if chatLaunchState === "idle"}
- <!-- No running instance: show model selector -->
- <div
- class="flex-1 overflow-y-auto flex items-center justify-center px-8 py-6"
- >
- <ChatModelSelector
- models={models.map((m) => ({
- id: m.id,
- name: m.name ?? "",
- base_model: m.base_model ?? "",
- storage_size_megabytes: m.storage_size_megabytes ?? 0,
- capabilities: m.capabilities ?? [],
- family: m.family ?? "",
- quantization: m.quantization ?? "",
- }))}
- clusterLabel={chatClusterLabel}
- totalMemoryGB={availableMemoryGB()}
- onSelect={handleChatModelSelect}
- onAddModel={handleChatAddModel}
- />
- </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 — we'll pick the best model automatically"
- showModelSelector={!!bestRunningModelId}
- modelDisplayOverride={bestRunningModelId ?? undefined}
- modelTasks={modelTasks()}
- modelCapabilities={modelCapabilities()}
- onAutoSend={handleAutoSend}
- onOpenModelPicker={openChatModelPicker}
- />
- </div>
- </div>
- {:else if chatLaunchState !== "idle" && chatLaunchState !== "ready"}
+ {#if chatLaunchState !== "idle" && chatLaunchState !== "ready"}
<!-- Model launching/downloading/loading: show progress -->
<div class="flex-1 flex items-center justify-center px-8 py-6">
<div class="flex flex-col items-center gap-6 max-w-md w-full">
@@ -5952,8 +5926,8 @@
/>
</div>
</div>
- {:else}
- <!-- Normal chat: model is running -->
+ {:else if messages().length > 0 || chatLaunchState === "ready"}
+ <!-- Normal chat: show messages -->
<div
class="flex-1 overflow-y-auto px-8 py-6"
bind:this={chatScrollRef}
@@ -6009,6 +5983,42 @@
/>
</div>
</div>
+ {:else}
+ <!-- No running instance, no messages: show model selector -->
+ <div
+ class="flex-1 overflow-y-auto flex items-center justify-center px-8 py-6"
+ >
+ <ChatModelSelector
+ models={models.map((m) => ({
+ id: m.id,
+ name: m.name ?? "",
+ base_model: m.base_model ?? "",
+ storage_size_megabytes: m.storage_size_megabytes ?? 0,
+ capabilities: m.capabilities ?? [],
+ family: m.family ?? "",
+ quantization: m.quantization ?? "",
+ }))}
+ clusterLabel={chatClusterLabel}
+ totalMemoryGB={availableMemoryGB()}
+ onSelect={handleChatModelSelect}
+ onAddModel={handleChatAddModel}
+ />
+ </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 — we'll pick the best model automatically"
+ showModelSelector={!!bestRunningModelId}
+ modelDisplayOverride={bestRunningModelId ?? undefined}
+ modelTasks={modelTasks()}
+ modelCapabilities={modelCapabilities()}
+ onAutoSend={handleAutoSend}
+ onOpenModelPicker={openChatModelPicker}
+ />
+ </div>
+ </div>
{/if}
</div>
← 14526d28 update mlx 2 (#1611)
·
back to Exo
·
bump (#1608) 9a2d2a4a →