From 77f4dc7c1ff727204138366cff0065f2ffe00c27 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 2 Mar 2026 10:17:59 -0800 Subject: [PATCH 1/3] feat: add model management with downloads drawer, model browser, and download-from-URL Phase 1: Global downloads drawer - DownloadsDrawer slides out from sidebar, accessible from any view - Sidebar badge toggles drawer; dedicated download icon when active - Removed embedded DownloadsPanel from ModelsView Phase 2: Model file browser - ModelBrowser with folder-type sidebar and searchable file list - Backend IPC handlers to scan modelsDirs for model files - Tab switcher in ModelsView (Directories / Browse) Phase 3: Download from URL - DownloadModelDialog modal with URL, filename, and folder selection - launcher-start-download IPC handler reusing existing download infra Amp-Thread-ID: https://ampcode.com/threads/T-019cae16-0b12-758d-8211-9a8205b88929 Co-authored-by: Amp --- docs/model-management-plan.md | 153 +++++++++ locales/en.json | 21 +- src/main/lib/comfyDownloadManager.ts | 9 + src/main/lib/ipc.ts | 37 ++- src/preload/index.ts | 6 + src/renderer/src/App.vue | 25 +- src/renderer/src/assets/main.css | 55 ++++ .../src/components/DownloadModelDialog.vue | 125 +++++++ .../src/components/DownloadsDrawer.vue | 307 ++++++++++++++++++ src/renderer/src/components/ModelBrowser.vue | 272 ++++++++++++++++ src/renderer/src/types/ipc.ts | 1 + src/renderer/src/views/ModelsView.vue | 37 ++- src/types/ipc.ts | 13 + 13 files changed, 1054 insertions(+), 7 deletions(-) create mode 100644 docs/model-management-plan.md create mode 100644 src/renderer/src/components/DownloadModelDialog.vue create mode 100644 src/renderer/src/components/DownloadsDrawer.vue create mode 100644 src/renderer/src/components/ModelBrowser.vue diff --git a/docs/model-management-plan.md b/docs/model-management-plan.md new file mode 100644 index 000000000..ef71d5150 --- /dev/null +++ b/docs/model-management-plan.md @@ -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`. +- **`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 `` alongside the sidebar. Add `downloadsDrawerOpen` ref. Wire sidebar indicator click to toggle it. Remove `` from ModelsView embedding. | +| View | `src/renderer/src/views/ModelsView.vue` | Remove the `` 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` 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. diff --git a/locales/en.json b/locales/en.json index bf4c0586a..d3611f215 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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": { @@ -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": { @@ -101,6 +117,7 @@ "extract": "Extract", "useSharedPaths": "Use Shared Directories", "done": "Done", + "loading": "Loading…", "noItems": "No items available.", "advanced": "Advanced", "tabStatus": "Status", diff --git a/src/main/lib/comfyDownloadManager.ts b/src/main/lib/comfyDownloadManager.ts index 1a82cc006..2809b5c94 100644 --- a/src/main/lib/comfyDownloadManager.ts +++ b/src/main/lib/comfyDownloadManager.ts @@ -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) + }, + ) } diff --git a/src/main/lib/ipc.ts b/src/main/lib/ipc.ts index ae25f8337..bc4a46ff9 100644 --- a/src/main/lib/ipc.ts +++ b/src/main/lib/ipc.ts @@ -24,7 +24,7 @@ import type { GpuInfo } from './gpu' import { formatTime } from './util' 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' @@ -884,6 +884,41 @@ 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; 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 stat = fs.statSync(path.join(dirPath, entry.name)) + files.push({ + name: entry.name, + directory, + 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 [ diff --git a/src/preload/index.ts b/src/preload/index.ts index a0e2ed1d4..c67f53f73 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -83,6 +83,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'), diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 30c50b070..85cba04f4 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -26,8 +26,10 @@ import NewInstallModal from './views/NewInstallModal.vue' import QuickInstallModal from './views/QuickInstallModal.vue' import TrackModal from './views/TrackModal.vue' +import DownloadsDrawer from './components/DownloadsDrawer.vue' + // Lucide icons -import { LayoutDashboard, Box, Play, FolderOpen, Image, Settings } from 'lucide-vue-next' +import { LayoutDashboard, Box, Play, FolderOpen, Image, Settings, Download } from 'lucide-vue-next' const { t, setLocaleMessage, locale } = useI18n() const sessionStore = useSessionStore() @@ -42,6 +44,13 @@ useTheme() type TabView = 'dashboard' | 'list' | 'running' | 'models' | 'media' | 'settings' const activeView = ref('dashboard') +// --- Downloads drawer --- +const downloadsDrawerOpen = ref(false) + +function toggleDownloadsDrawer(): void { + downloadsDrawerOpen.value = !downloadsDrawerOpen.value +} + // --- Modal views --- const detailInstallation = ref(null) const consoleInstallationId = ref(null) @@ -233,12 +242,26 @@ onMounted(async () => { {{ downloadStore.activeDownloads.length }} + + + +
diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 6e1291f44..4bc95c1ef 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -595,6 +595,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; @@ -872,3 +893,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; +} diff --git a/src/renderer/src/components/DownloadModelDialog.vue b/src/renderer/src/components/DownloadModelDialog.vue new file mode 100644 index 000000000..f1513bd83 --- /dev/null +++ b/src/renderer/src/components/DownloadModelDialog.vue @@ -0,0 +1,125 @@ + + + diff --git a/src/renderer/src/components/DownloadsDrawer.vue b/src/renderer/src/components/DownloadsDrawer.vue new file mode 100644 index 000000000..a5d1e2d5c --- /dev/null +++ b/src/renderer/src/components/DownloadsDrawer.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/src/renderer/src/components/ModelBrowser.vue b/src/renderer/src/components/ModelBrowser.vue new file mode 100644 index 000000000..78dfde259 --- /dev/null +++ b/src/renderer/src/components/ModelBrowser.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/src/renderer/src/types/ipc.ts b/src/renderer/src/types/ipc.ts index 783af5452..f42e44a94 100644 --- a/src/renderer/src/types/ipc.ts +++ b/src/renderer/src/types/ipc.ts @@ -42,6 +42,7 @@ export type { UpdateDownloadProgress, ModelDownloadStatus, ModelDownloadProgress, + ModelFileInfo, TrackResult, CopyEvent, SnapshotDiffSummary, diff --git a/src/renderer/src/views/ModelsView.vue b/src/renderer/src/views/ModelsView.vue index f5567218c..3bad32282 100644 --- a/src/renderer/src/views/ModelsView.vue +++ b/src/renderer/src/views/ModelsView.vue @@ -1,9 +1,14 @@