Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions docs/model-management-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Model Management & Downloads Panel Plan

## Goal

Bring the desktop/ComfyUI_frontend model management experience into the
Launcher's **Models** view, and add a global **Downloads sidebar panel** — a
toggleable drawer docked to the left sidebar (like browser download drawers, or
the existing `comfyContentScript.ts` floating toast in the ComfyUI windows).

---

## Current State

### What the Launcher already has
- **`ModelsView.vue`** — shows configured model directories with DirCard
widgets (browse, add, remove, reorder). No model file listing, no browsing,
no search.
- **`DownloadsPanel.vue`** — renders active & finished downloads as inline
cards. Currently embedded *inside* ModelsView at the bottom.
- **`downloadStore.ts`** — Pinia store that subscribes to
`model-download-progress` IPC events from the main process and tracks them
in a reactive `Map<url, ModelDownloadProgress>`.
- **`comfyDownloadManager.ts`** (main process) — handles Electron
`will-download`, temp files, managed model downloads and general
browser-like downloads. Broadcasts progress to both the originating
ComfyUI window and the Launcher window.
- **`comfyContentScript.ts`** — injected into ComfyUI webview pages; creates a
floating download toast panel with a collapsible tab, drag-to-undock, and
inline pause/resume/cancel buttons. Theme-aware.

### What the desktop app / frontend has
- **`DownloadManager.ts`** (desktop) — singleton managing `.safetensors`
downloads via Electron `session.downloadURL`. IPC handlers for start / pause
/ resume / cancel / getAllDownloads / deleteModel.
- **`AssetBrowserModal.vue`** — full model browser with search, left nav by
category, filter bar (architecture, ownership), asset grid, right info panel.
Uses `useAssetBrowser`, `useModelTypes`, `useModelUpload` composables.
- **`UploadModelDialog.vue`** — 3-step wizard: enter URL → confirm metadata →
upload progress.
- **`ModelImportProgressDialog.vue`** — uses `HoneyToast` (expandable toast)
with `ProgressToastItem` cards, filter popover (all/completed/failed).
- **`modelStore.ts`** — `ComfyModelDef` class (metadata from safetensors),
`ModelFolder` (lazy-load per directory). Loaded from ComfyUI API.

---

## Design

### 1. Downloads Sidebar Panel (global, always accessible)

Replace the current approach of embedding `DownloadsPanel` inside `ModelsView`
with a **sidebar-attached drawer** that is accessible from any view.

**Behaviour:**
- A persistent **download indicator** appears in the sidebar when there are
active or recent downloads — a small icon/badge next to the Models tab (this
already exists as `sidebar-count`).
- Clicking the indicator (or a dedicated button) toggles a **drawer panel**
that slides out from the left sidebar, overlaying the content area.
- The drawer stays open across view switches. Clicking outside or pressing
Escape closes it.
- The drawer shows the same content currently in `DownloadsPanel.vue`: active
downloads with progress bars, pause/resume/cancel, and finished downloads
with dismiss/show-in-folder.

**Implementation:**

| Layer | File | Change |
|-------|------|--------|
| Component | `src/renderer/src/components/DownloadsDrawer.vue` | **New.** Renders a slide-out drawer panel anchored to the sidebar. Reuses all the formatting logic from `DownloadsPanel.vue` (move or import). Add a close button and "clear completed" button in the header. |
| App shell | `src/renderer/src/App.vue` | Add `<DownloadsDrawer>` alongside the sidebar. Add `downloadsDrawerOpen` ref. Wire sidebar indicator click to toggle it. Remove `<DownloadsPanel>` from ModelsView embedding. |
| View | `src/renderer/src/views/ModelsView.vue` | Remove the `<DownloadsPanel />` include (downloads move to the global drawer). |
| Style | `src/renderer/src/assets/main.css` | Add `.downloads-drawer` styles: fixed position left of content, slide transition, backdrop, z-index layering. |
| Store | `downloadStore.ts` | No changes needed — already provides the reactive data. |

**Drawer visual spec:**
```
┌──────────┬──────────────────┬──────────────────────┐
│ Sidebar │ Downloads Drawer │ Content area (dimmed) │
│ │ ┌──────────────┐ │ │
│ ● Dash │ │ Downloads │ │ │
│ Inst │ │ │ │ │
│ Run │ │ [card] │ │ │
│ ● Models │ │ [card] │ │ │
│ Media │ │ [card] │ │ │
│ Sett │ └──────────────┘ │ │
└──────────┴──────────────────┴──────────────────────┘
```

Width: ~340px. Background: `var(--surface)`. Border-right: `var(--border)`.

### 2. Model Browser in ModelsView

Replace the current directory-card-only Models view with a tabbed layout:

**Tab 1: "Directories"** (current content) — model directory configuration.

**Tab 2: "Browse Models"** — a file browser that lists model files in the
configured directories, grouped by folder type.

**Implementation:**

| Layer | File | Change |
|-------|------|--------|
| IPC type | `src/types/ipc.ts` | Add `ModelFileInfo` type (`name`, `directory`, `sizeBytes`, `modified`). Add `getModelFiles(directory: string): Promise<ModelFileInfo[]>` to `ElectronApi`. |
| Main | `src/main/lib/models.ts` | Add `listModelFiles(baseDir, directory)` function that scans a model subdirectory and returns file info. |
| Main IPC | `src/main/lib/ipc.ts` | Register `get-model-files` handler. |
| Preload | `src/preload/index.ts` | Expose `getModelFiles`. |
| Component | `src/renderer/src/components/ModelBrowser.vue` | **New.** Left sidebar listing model folder types (`checkpoints`, `loras`, etc. from `MODEL_FOLDER_TYPES`). Main area shows a file list/grid for the selected folder. Search bar at top. File cards show name, size, date. |
| Component | `src/renderer/src/components/ModelFileCard.vue` | **New.** Single model file card — name, size, modified date, "Show in Folder" button, optional delete button. |
| View | `src/renderer/src/views/ModelsView.vue` | Add tab switcher (Directories / Browse). Conditionally render `ModelBrowser` or the existing dir-card content. |

### 3. Download from URL (stretch)

Add the ability to initiate a model download from the Launcher's Models view
(similar to the frontend's `UploadModelDialog` wizard).

| Layer | File | Change |
|-------|------|--------|
| Component | `src/renderer/src/components/DownloadModelDialog.vue` | **New.** Simple dialog: URL input, destination folder dropdown (from `MODEL_FOLDER_TYPES`), optional filename override, Download button. |
| Main | `src/main/lib/comfyDownloadManager.ts` | Add a `startLauncherDownload(url, filename, directory)` variant that works from the Launcher window directly (not from a ComfyUI webview). The infrastructure is already there — we just need an IPC handler that calls `startModelDownload` with the launcher window. |
| Preload | `src/preload/index.ts` | Expose `startModelDownload(url, filename, directory)`. |
| IPC type | `src/types/ipc.ts` | Add `startModelDownload` to `ElectronApi`. |

---

## Implementation Order

1. **Phase 1: Downloads Drawer** — Extract `DownloadsPanel` into
`DownloadsDrawer`, wire it into the App shell sidebar. This is the most
impactful UX improvement and is self-contained.

2. **Phase 2: Model Browser** — Add the file-listing backend IPC + the
`ModelBrowser` component in `ModelsView`.

3. **Phase 3: Download from URL** — Add the download dialog and
launcher-initiated download path.

---

## Notes

- The Launcher already uses `lucide-vue-next` for sidebar icons — use the same
for the drawer's close/clear/folder icons.
- The existing CSS design system (`--surface`, `--border`, `--accent`, etc.)
should be used throughout — no new design tokens needed.
- The drawer should work with all existing themes (dark, light, nord, etc.).
- The `comfyContentScript.ts` floating toast in ComfyUI windows is independent
and should NOT be changed — it serves a different context (inside the
webview).
- General (non-model) downloads already flow through the same pipeline via the
`will-download` fallback in `comfyDownloadManager.ts`, so the drawer will
automatically show those too.
21 changes: 19 additions & 2 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,16 @@
"completed": "Complete",
"cancelled": "Cancelled",
"error": "Failed",
"sizeProgress": "{received} / {total}"
"sizeProgress": "{received} / {total}",
"clearCompleted": "Clear completed",
"downloadModel": "Download Model",
"urlLabel": "URL",
"urlPlaceholder": "https://example.com/model.safetensors",
"filenameLabel": "Filename",
"filenamePlaceholder": "model.safetensors",
"directoryLabel": "Destination Folder",
"startDownload": "Download",
"downloadFailed": "Download failed. Please check the URL and try again."
},

"media": {
Expand All @@ -70,7 +79,14 @@
"removeDir": "Remove",
"primary": "primary",
"default": "default",
"makePrimary": "Make Primary"
"makePrimary": "Make Primary",
"browse": "Browse",
"directoriesTab": "Directories",
"folderTypes": "Model Types",
"searchPlaceholder": "Search files…",
"noFiles": "No model files found",
"openFolder": "Open folder",
"fileCount": "{count} files"
},

"common": {
Expand Down Expand Up @@ -102,6 +118,7 @@
"extract": "Extract",
"useSharedPaths": "Use Shared Directories",
"done": "Done",
"loading": "Loading…",
"noItems": "No items available.",
"advanced": "Advanced",
"tabStatus": "Status",
Expand Down
9 changes: 9 additions & 0 deletions src/main/lib/comfyDownloadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,13 @@ export function registerDownloadIpc(): void {
shell.showItemInFolder(path.resolve(savePath))
}
})

ipcMain.handle(
'launcher-start-download',
(event, { url, filename, directory }: { url: string; filename: string; directory: string }) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return false
return startModelDownload(win, url, filename, directory)
},
)
}
39 changes: 38 additions & 1 deletion src/main/lib/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { formatTime } from './util'
import { getActiveDownloads } from './comfyDownloadManager'
import * as releaseCache from './release-cache'
import * as i18n from './i18n'
import { ensureModelPathsConfig } from './models'
import { ensureModelPathsConfig, MODEL_FOLDER_TYPES } from './models'
import { copyDirWithProgress } from './copy'
import { fetchJSON } from './fetch'
import { fetchLatestRelease, truncateNotes } from './comfyui-releases'
Expand Down Expand Up @@ -1175,6 +1175,43 @@ export function register(callbacks: RegisterCallbacks = {}): void {
}
})

ipcMain.handle('get-model-folders', () => {
return [...MODEL_FOLDER_TYPES]
})

ipcMain.handle('get-model-files', (_event, directory: string) => {
const modelsDirs = (settings.get('modelsDirs') as string[]) || settings.defaults.modelsDirs
const files: Array<{ name: string; directory: string; fullPath: string; sizeBytes: number; modifiedAt: number }> = []
const ALLOWED_EXTS = new Set(['.safetensors', '.sft', '.ckpt', '.pth', '.pt', '.bin', '.onnx'])

for (const baseDir of modelsDirs) {
const dirPath = path.join(baseDir, directory)
try {
if (!fs.existsSync(dirPath)) continue
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
for (const entry of entries) {
if (!entry.isFile()) continue
const ext = path.extname(entry.name).toLowerCase()
if (!ALLOWED_EXTS.has(ext)) continue
try {
const filePath = path.join(dirPath, entry.name)
const stat = fs.statSync(filePath)
files.push({
name: entry.name,
directory,
fullPath: filePath,
sizeBytes: stat.size,
modifiedAt: stat.mtimeMs,
})
} catch {}
}
} catch {}
}

files.sort((a, b) => b.modifiedAt - a.modifiedAt)
return files
})

ipcMain.handle('get-media-sections', () => {
const s = settings.getAll()
return [
Expand Down
6 changes: 6 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ const api: ElectronApi = {
resumeModelDownload: (url) => ipcRenderer.invoke('model-download-resume', { url }),
cancelModelDownload: (url) => ipcRenderer.invoke('model-download-cancel', { url }),
showDownloadInFolder: (savePath) => ipcRenderer.invoke('show-download-in-folder', { savePath }),
startModelDownload: (url, filename, directory) =>
ipcRenderer.invoke('launcher-start-download', { url, filename, directory }),

// Model file browser
getModelFolders: () => ipcRenderer.invoke('get-model-folders'),
getModelFiles: (directory: string) => ipcRenderer.invoke('get-model-files', directory),

// Updates
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
Expand Down
55 changes: 55 additions & 0 deletions src/renderer/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,27 @@ input[type="checkbox"]:checked::after { transform: translateX(16px); }
.modal-select-item-label { flex: 1; font-weight: 500; }
.modal-select-item-desc { color: var(--text-muted); font-size: 12px; }

/* Modal layout (for structured dialogs with header/body/footer) */
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 24px; border-bottom: 1px solid var(--border);
}
.modal-body { padding: 16px 24px; }
.modal-footer {
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
padding: 12px 24px; border-top: 1px solid var(--border);
}

/* Form field inputs */
.field-input {
padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); color: var(--text); font-size: 14px;
outline: none; width: 100%;
}
.field-input:focus { border-color: var(--accent); }
.field-input::placeholder { color: var(--text-faint); }
select.field-input { cursor: pointer; }

/* Update banner */
.update-banner {
display: flex; align-items: center; gap: 12px;
Expand Down Expand Up @@ -881,3 +902,37 @@ button.detail-header-btn.active:hover:not(:disabled) {
button.detail-header-btn:disabled {
cursor: default; opacity: 0.4;
}

/* Downloads drawer */
.downloads-drawer-backdrop {
position: fixed; inset: 0; z-index: 40;
background: var(--overlay-bg);
transition: opacity 0.2s ease;
}
.downloads-drawer {
position: fixed; top: 0; bottom: 0; left: 200px; z-index: 45;
width: 340px; background: var(--surface);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
transition: transform 0.2s ease;
box-shadow: 4px 0 16px rgba(0, 0, 0, 0.3);
}
.downloads-drawer-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 16px 12px; border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.downloads-drawer-title {
font-size: 15px; font-weight: 600; color: var(--text);
}
.downloads-drawer-actions {
display: flex; align-items: center; gap: 4px;
}
.downloads-drawer-body {
flex: 1; overflow-y: auto; padding: 12px;
display: flex; flex-direction: column; gap: 8px;
}
.downloads-drawer-empty {
display: flex; align-items: center; justify-content: center;
height: 100%; color: var(--text-muted); font-size: 14px;
}
Loading
Loading