[object Object]

← back to Exo

Fix custom model add requiring two attempts + enlarge sidebar buttons (#1805)

5327bdde846647cb29591c10fc3cac0f22a0554a · 2026-03-26 17:57:00 -0700 · Alex Cheema

## Motivation

Adding a custom model from the Hub tab shows "Added" toast but the model
doesn't appear in the All tab. You have to add it a second time for it
to work. Also, the "All" button in the model picker sidebar is too small
to read comfortably.

## Changes

**Race condition fix (`src/exo/api/main.py`):**
- Call `add_to_card_cache(card)` directly in `add_custom_model()` after
sending the `ForwarderCommand`, before the API response returns

**Sidebar sizing
(`dashboard/src/lib/components/FamilySidebar.svelte`):**
- Increased sidebar min-width from 72/64px to 80/72px
- Increased "All" icon from `w-5 h-5` to `w-6 h-6`
- Increased all sidebar labels from 9px to 11px

## Why It Works

`POST /models/add` sends a `ForwarderCommand(AddCustomModelCard)` and
returns immediately. The frontend then calls `GET /models` which reads
from `_card_cache`. But the cache was only updated by the worker event
handler after the event round-trips through the master — a race the
frontend almost always loses. By updating the cache directly in the API
handler, `GET /models` immediately reflects the new model. The worker's
later `add_to_card_cache` call is idempotent (dict key assignment).

## Test Plan

### Manual Testing
<!-- Hardware: any Mac -->
- Open model picker → Hub tab → add a custom model → verify it appears
in All tab on the first attempt
- Verify sidebar "All" button and other labels are visually larger and
readable

### Automated Testing
- `uv run basedpyright` passes with 0 errors
- `uv run ruff check` passes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 5327bdde846647cb29591c10fc3cac0f22a0554a
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date:   Thu Mar 26 17:57:00 2026 -0700

    Fix custom model add requiring two attempts + enlarge sidebar buttons (#1805)
    
    ## Motivation
    
    Adding a custom model from the Hub tab shows "Added" toast but the model
    doesn't appear in the All tab. You have to add it a second time for it
    to work. Also, the "All" button in the model picker sidebar is too small
    to read comfortably.
    
    ## Changes
    
    **Race condition fix (`src/exo/api/main.py`):**
    - Call `add_to_card_cache(card)` directly in `add_custom_model()` after
    sending the `ForwarderCommand`, before the API response returns
    
    **Sidebar sizing
    (`dashboard/src/lib/components/FamilySidebar.svelte`):**
    - Increased sidebar min-width from 72/64px to 80/72px
    - Increased "All" icon from `w-5 h-5` to `w-6 h-6`
    - Increased all sidebar labels from 9px to 11px
    
    ## Why It Works
    
    `POST /models/add` sends a `ForwarderCommand(AddCustomModelCard)` and
    returns immediately. The frontend then calls `GET /models` which reads
    from `_card_cache`. But the cache was only updated by the worker event
    handler after the event round-trips through the master — a race the
    frontend almost always loses. By updating the cache directly in the API
    handler, `GET /models` immediately reflects the new model. The worker's
    later `add_to_card_cache` call is idempotent (dict key assignment).
    
    ## Test Plan
    
    ### Manual Testing
    <!-- Hardware: any Mac -->
    - Open model picker → Hub tab → add a custom model → verify it appears
    in All tab on the first attempt
    - Verify sidebar "All" button and other labels are visually larger and
    readable
    
    ### Automated Testing
    - `uv run basedpyright` passes with 0 errors
    - `uv run ruff check` passes
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 dashboard/src/lib/components/FamilySidebar.svelte  | 25 ++++++----------------
 .../src/lib/components/ModelFilterPopover.svelte   |  2 +-
 src/exo/api/main.py                                |  5 +++++
 3 files changed, 13 insertions(+), 19 deletions(-)

diff --git a/dashboard/src/lib/components/FamilySidebar.svelte b/dashboard/src/lib/components/FamilySidebar.svelte
index 4a7307a0..62633a5e 100644
--- a/dashboard/src/lib/components/FamilySidebar.svelte
+++ b/dashboard/src/lib/components/FamilySidebar.svelte
@@ -42,31 +42,20 @@
 </script>
 
 <div
-  class="flex flex-col gap-1 py-2 px-1 border-r border-exo-yellow/10 bg-exo-medium-gray/30 min-w-[72px] sm:min-w-[64px] overflow-y-auto scrollbar-hide"
+  class="flex flex-col gap-1 py-2 px-1 border-r border-exo-yellow/10 bg-exo-medium-gray/30 min-w-[80px] sm:min-w-[72px] overflow-y-auto scrollbar-hide"
 >
   <!-- All models (no filter) -->
   <button
     type="button"
     onclick={() => onSelect(null)}
-    class="group flex flex-col items-center justify-center p-2 sm:p-2 rounded transition-all duration-200 cursor-pointer min-h-[44px] sm:min-h-0 {selectedFamily ===
+    class="group flex items-center justify-center px-3 py-2.5 rounded transition-all duration-200 cursor-pointer min-h-[44px] sm:min-h-0 {selectedFamily ===
     null
       ? 'bg-exo-yellow/20 border-l-2 border-exo-yellow'
       : 'hover:bg-white/5 border-l-2 border-transparent'}"
     title="All models"
   >
-    <svg
-      class="w-5 h-5 {selectedFamily === null
-        ? 'text-exo-yellow'
-        : 'text-white/50 group-hover:text-white/70'}"
-      viewBox="0 0 24 24"
-      fill="currentColor"
-    >
-      <path
-        d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z"
-      />
-    </svg>
     <span
-      class="text-[9px] font-mono mt-0.5 {selectedFamily === null
+      class="text-[12px] font-mono font-medium {selectedFamily === null
         ? 'text-exo-yellow'
         : 'text-white/40 group-hover:text-white/60'}">All</span
     >
@@ -90,7 +79,7 @@
           : "text-white/50 group-hover:text-amber-400/70"}
       />
       <span
-        class="text-[9px] font-mono mt-0.5 {selectedFamily === 'favorites'
+        class="text-[11px] font-mono mt-0.5 {selectedFamily === 'favorites'
           ? 'text-amber-400'
           : 'text-white/40 group-hover:text-white/60'}">Faves</span
       >
@@ -115,7 +104,7 @@
           : "text-white/50 group-hover:text-white/70"}
       />
       <span
-        class="text-[9px] font-mono mt-0.5 {selectedFamily === 'recents'
+        class="text-[11px] font-mono mt-0.5 {selectedFamily === 'recents'
           ? 'text-exo-yellow'
           : 'text-white/40 group-hover:text-white/60'}">Recent</span
       >
@@ -139,7 +128,7 @@
         : "text-white/50 group-hover:text-orange-400/70"}
     />
     <span
-      class="text-[9px] font-mono mt-0.5 {selectedFamily === 'huggingface'
+      class="text-[11px] font-mono mt-0.5 {selectedFamily === 'huggingface'
         ? 'text-orange-400'
         : 'text-white/40 group-hover:text-white/60'}">Hub</span
     >
@@ -165,7 +154,7 @@
           : "text-white/50 group-hover:text-white/70"}
       />
       <span
-        class="text-[9px] font-mono mt-0.5 truncate max-w-full {selectedFamily ===
+        class="text-[11px] font-mono mt-0.5 truncate max-w-full {selectedFamily ===
         family
           ? 'text-exo-yellow'
           : 'text-white/40 group-hover:text-white/60'}"
diff --git a/dashboard/src/lib/components/ModelFilterPopover.svelte b/dashboard/src/lib/components/ModelFilterPopover.svelte
index 8c36af03..a1cd6e16 100644
--- a/dashboard/src/lib/components/ModelFilterPopover.svelte
+++ b/dashboard/src/lib/components/ModelFilterPopover.svelte
@@ -73,7 +73,7 @@
 
 <!-- svelte-ignore a11y_no_static_element_interactions -->
 <div
-  class="filter-popover absolute right-0 top-full mt-2 w-64 bg-exo-dark-gray border border-exo-yellow/10 rounded-lg shadow-xl z-10"
+  class="filter-popover absolute right-0 top-full mt-2 w-64 bg-exo-dark-gray border border-exo-yellow/10 rounded-lg shadow-xl z-20"
   transition:fly={{ y: -10, duration: 200, easing: cubicOut }}
   onclick={(e) => e.stopPropagation()}
   role="dialog"
diff --git a/src/exo/api/main.py b/src/exo/api/main.py
index 3927ee5e..7862801f 100644
--- a/src/exo/api/main.py
+++ b/src/exo/api/main.py
@@ -129,6 +129,7 @@ from exo.shared.logging import InterceptLogger
 from exo.shared.models.model_cards import (
     ModelCard,
     ModelId,
+    add_to_card_cache,
     get_card,
     get_model_cards,
 )
@@ -1587,6 +1588,10 @@ class API:
             )
         )
 
+        # Immediately update the local cache so the subsequent GET /models
+        # returns the new model without waiting for the event round-trip.
+        add_to_card_cache(card)
+
         return ModelListModel(
             id=card.model_id,
             hugging_face_id=card.model_id,

← 15f1b61f Rework model storage directory management (for external stor  ·  back to Exo  ·  chore: bump exo-version with release version (#1807) 1e51dc89 →