diff --git a/core/resolver.py b/core/resolver.py index 6345031..0648830 100644 --- a/core/resolver.py +++ b/core/resolver.py @@ -428,6 +428,11 @@ def analyze_and_find_matches( # Identify missing models missing_models = identify_missing_models(all_model_refs, available_models) + resolved_models = [ + {**model_ref, "matches": []} + for model_ref in all_model_refs + if model_ref.get("exists", False) + ] # Enrich missing models with workflow URLs for missing in missing_models: @@ -557,6 +562,8 @@ def analyze_and_find_matches( result = { "missing_models": missing_with_matches, + "resolved_models": resolved_models, + "total_resolved": len(resolved_models), "total_missing": len(missing_with_matches), "total_models_analyzed": len(all_model_refs), } diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 507f05d..0042d5f 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -558,6 +558,48 @@ gap: 8px; flex-wrap: wrap; } +.mr-missing-resolved-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 26px; + padding: 0 8px; + color: var(--mr-text-muted); + background: var(--mr-missing-control-bg, rgba(255,255,255,0.035)); + border: 1px solid var(--mr-missing-control-border, rgba(255,255,255,0.105)); + border-radius: var(--mr-missing-control-radius, 7px); + cursor: pointer; + font-size: 11px; + font-weight: 700; + line-height: 1; + white-space: nowrap; + transition: var(--mr-missing-control-transition, background 0.15s ease, border-color 0.15s ease, color 0.15s ease); +} +.mr-missing-resolved-toggle:hover { + color: var(--mr-text); + background: var(--mr-missing-control-bg-hover, rgba(255,255,255,0.06)); + border-color: var(--mr-missing-control-border-hover, rgba(255,255,255,0.17)); +} +.mr-missing-resolved-toggle input { + width: 13px; + height: 13px; + margin: 0; + accent-color: #72d38a; +} +.mr-missing-resolved-toggle em { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 999px; + color: #c9f6d0; + background: rgba(114,211,138,0.12); + font-style: normal; + font-size: 10px; +} .mr-missing-refresh-btn, #model-resolver-modal .mr-missing-refresh-btn { min-height: 26px; @@ -648,6 +690,9 @@ background: linear-gradient(90deg, rgba(114,211,138,0.12) 0%, rgba(114,211,138,0.055) 42%, rgba(114,211,138,0.018) 100%); box-shadow: inset 3px 0 0 rgba(114,211,138,0.92), inset 0 0 0 1px rgba(114,211,138,0.12); } +.mr-missing-list-row.is-resolved { + opacity: 0.72; +} .mr-missing-list-row.is-batch-selected { background: rgba(78,161,255,0.055); } @@ -715,6 +760,20 @@ font-size: 13px; font-weight: 700; } +.mr-missing-row-resolved-pill { + display: inline-flex; + align-items: center; + justify-self: start; + min-height: 17px; + padding: 1px 6px; + border-radius: 999px; + color: #c9f6d0; + background: rgba(114,211,138,0.10); + border: 1px solid rgba(114,211,138,0.20); + font-size: 9px; + font-weight: 750; + line-height: 1; +} .mr-missing-row-node { grid-column: 1; color: var(--mr-text-muted); diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 37dd1d6..75fdfd2 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -1284,6 +1284,7 @@ export const queueMethods = { this.updateSelectedBarForMissing?.(m); this.updateApplyPendingButton?.(); this.updateQueuePanel(); + this.refreshMissingModelsBrowserFromCache?.(); }, // Clear all queued selections @@ -1293,6 +1294,7 @@ export const queueMethods = { this.savePendingQueueForActiveWorkflow(); this.updateApplyPendingButton?.(); this.updateQueuePanel(); + this.refreshMissingModelsBrowserFromCache?.(); try { document.querySelectorAll('.model-resolver-selected').forEach(el => { el.style.display = 'none'; el.innerHTML = ''; }); } catch (e) { /* ignore */ } @@ -1405,6 +1407,7 @@ export const queueMethods = { this.updateSelectedBarForMissing(m); this.updateApplyPendingButton?.(); this.updateQueuePanel(); + this.refreshMissingModelsBrowserFromCache?.(); } }, @@ -1748,6 +1751,7 @@ export const queueMethods = { this.updateSelectedBarForMissing?.(missing); this.updateQueuePanel(); this.updateApplyPendingButton(); + this.refreshMissingModelsBrowserFromCache?.(); }, /** diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index aac7916..afcc5be 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -68,6 +68,12 @@ export class ResolverManagerDialog extends ComfyDialog { this.sidebarTabId = "comfyui-model-resolver"; this.sidebarOpenModeStorageKey = "model_resolver_sidebar_open_mode"; this.missingBrowserSplitStorageKey = "model_resolver_missing_browser_detail_w"; + this.showResolvedModelsStorageKey = "model_resolver_show_resolved_models"; + try { + this.showResolvedModels = localStorage.getItem(this.showResolvedModelsStorageKey) === '1'; + } catch (e) { + this.showResolvedModels = false; + } this.dockButton = null; this.undockButton = null; this._floatingRectBeforeDock = null; diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index bfac52e..ba8ca42 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -223,12 +223,76 @@ export const missingBrowserMethods = { }; }, - renderMissingModelsBrowser(missingModels, selectedKey, totalMissing, activeCount, hasAny100Match) { + isMissingModelResolved(missing = {}) { + if (!missing) return false; + if (missing.__isExistingResolved) return true; + const key = this.getMissingModelKey(missing); + return Boolean(key && this.pendingIndex?.has?.(key)); + }, + + getResolvedWorkflowModels(data = {}) { + return (data.resolved_models || []).map(model => this.normalizeResolvedWorkflowModel(model)); + }, + + normalizeResolvedWorkflowModel(model = {}) { + const originalPath = model.original_path || model.name || model.filename || ''; + const filename = originalPath.split('/').pop()?.split('\\').pop() || originalPath || 'Resolved model'; + const fullPath = model.full_path || model.path || ''; + const relativePath = model.relative_path || originalPath || filename; + const category = model.category || 'unknown'; + const resolvedModel = { + path: fullPath, + relative_path: relativePath, + filename, + category, + resolved_path: fullPath + }; + + return { + ...model, + __isExistingResolved: true, + name: model.name || filename, + original_path: originalPath || filename, + category, + matches: [{ + confidence: 100, + match_type: 'exact', + filename, + path: fullPath, + model: resolvedModel + }] + }; + }, + + getVisibleMissingModels(missingModels = []) { + if (this.showResolvedModels) return missingModels; + return missingModels.filter(missing => !this.isMissingModelResolved(missing)); + }, + + getResolvedMissingCount(missingModels = []) { + return missingModels.reduce((count, missing) => ( + count + (this.isMissingModelResolved(missing) ? 1 : 0) + ), 0); + }, + + refreshMissingModelsBrowserFromCache() { + if (this.activeTab !== 'missing' || !this.contentElement || !this.cachedAnalysisData) return; + this.displayMissingModels(this.contentElement, this.cachedAnalysisData); + }, + + renderMissingModelsBrowser(missingModels, selectedKey, totalMissing, activeCount, hasAny100Match, options = {}) { + const hiddenResolvedCount = Number(options.hiddenResolvedCount || 0); + const resolvedCount = Number(options.resolvedCount || 0); + const rawMissingCount = Number(options.rawMissingCount ?? totalMissing); + const missingCount = Number(options.missingCount ?? rawMissingCount); + const resolvedToggleCount = this.showResolvedModels ? resolvedCount : hiddenResolvedCount; const stats = this.getMissingModelSummaryStats(missingModels); const detailIndex = missingModels.findIndex(missing => this.getMissingModelKey(missing) === selectedKey); const detailMissing = detailIndex >= 0 ? missingModels[detailIndex] : null; const activeHint = activeCount > 0 ? `${activeCount} downloading` + : hiddenResolvedCount > 0 + ? `${hiddenResolvedCount} resolved hidden` : (hasAny100Match ? 'Auto-link ready for exact matches' : 'Review matches or search online'); const listLayout = this.getMissingModelsListLayout(missingModels); let savedDetailWidth = null; @@ -239,13 +303,16 @@ export const missingBrowserMethods = { ? `--mr-missing-detail-track:${savedDetailWidth}px;` : ''; const listStyle = `--mr-missing-model-col:${listLayout.modelPx}px;--mr-missing-type-col:${listLayout.typePx}px;`; + const titleText = this.showResolvedModels && resolvedCount > 0 + ? `${totalMissing} shown (${missingCount} missing / ${resolvedCount} resolved)` + : `${totalMissing} missing model${totalMissing === 1 ? '' : 's'}`; let html = `
-
${totalMissing} missing model${totalMissing === 1 ? '' : 's'}
+
${this.escapeHtml(titleText)}
${this.escapeHtml(activeHint)}
@@ -254,6 +321,11 @@ export const missingBrowserMethods = { ${stats.partial} partial ${stats.none} no match
+ @@ -277,6 +349,7 @@ export const missingBrowserMethods = { const key = this.getMissingModelKey(missing); const isSelected = key === selectedKey; const isBatchSelected = this.batchSelectedMissingKeys?.has(key); + const isResolved = this.isMissingModelResolved(missing); const filename = this.getMissingFilename(missing); const bestMatch = this.getBestLocalMatch(missing, 70); const confidence = bestMatch ? Number(bestMatch.confidence || 0) : 0; @@ -294,7 +367,7 @@ export const missingBrowserMethods = { html += `
@@ -302,6 +375,7 @@ export const missingBrowserMethods = { ${index + 1} ${this.escapeHtml(filename)} + ${isResolved ? 'Resolved' : ''} ${rowNodeHtml} ${this.escapeHtml(typeLabel)} @@ -337,6 +411,18 @@ export const missingBrowserMethods = { refreshBtn.addEventListener('click', () => this.refreshMissingAnalysis(refreshBtn)); } + const showResolvedToggle = container.querySelector('#mr-show-resolved-models'); + if (showResolvedToggle && showResolvedToggle.dataset.mlResolvedBound !== 'true') { + showResolvedToggle.dataset.mlResolvedBound = 'true'; + showResolvedToggle.addEventListener('change', () => { + this.showResolvedModels = Boolean(showResolvedToggle.checked); + try { + localStorage.setItem(this.showResolvedModelsStorageKey, this.showResolvedModels ? '1' : '0'); + } catch (e) {} + this.displayMissingModels(container, data); + }); + } + const selectRow = (row) => { const key = row.dataset.missingKey; if (!key || key === this.selectedMissingModelKey) return; @@ -797,30 +883,38 @@ export const missingBrowserMethods = { const missingModels = (data.missing_models || []).map(missing => ( this.restoreDownloadedLocalMatchesForMissing?.(missing) || missing )); - const totalMissing = data.total_missing || 0; - this.missingModels = missingModels; - this.syncBatchSelectionForMissingModels(missingModels); + const resolvedModels = this.getResolvedWorkflowModels(data); + const allModelsForDisplay = [...missingModels, ...resolvedModels]; + const rawMissingCount = data.total_missing ?? missingModels.length; + const resolvedMissingCount = this.getResolvedMissingCount(missingModels); + const resolvedCount = resolvedMissingCount + resolvedModels.length; + const unresolvedMissingCount = Math.max(0, rawMissingCount - resolvedMissingCount); + const hiddenResolvedCount = this.showResolvedModels ? 0 : resolvedCount; + const visibleMissingModels = this.getVisibleMissingModels(allModelsForDisplay); + this.missingModels = visibleMissingModels; + this.syncBatchSelectionForMissingModels(visibleMissingModels); // Check if there are active downloads const activeCount = Object.keys(this.activeDownloads).length; - const hasAny100Match = missingModels.some(missing => + const hasAny100Match = visibleMissingModels.some(missing => (missing.matches || []).some(match => match.confidence === 100) ); + const hasAnyModelsToDisplay = rawMissingCount > 0 || resolvedModels.length > 0; - this.setMissingFooterControlsVisible(totalMissing > 0 || activeCount > 0); + this.setMissingFooterControlsVisible(hasAnyModelsToDisplay || activeCount > 0); // Hide download all button if no missing models if (this.downloadAllButton) { - this.downloadAllButton.style.display = (totalMissing > 0 || activeCount > 0) ? 'inline-flex' : 'none'; + this.downloadAllButton.style.display = (rawMissingCount > 0 || activeCount > 0) ? 'inline-flex' : 'none'; } - if (totalMissing === 0 && activeCount === 0) { + if (!hasAnyModelsToDisplay && activeCount === 0) { container.innerHTML = this.renderStatusMessage('All models are available! No missing models found.', 'success'); return; } // If no missing models but downloads are active, show a waiting message - if (totalMissing === 0 && activeCount > 0) { + if (rawMissingCount === 0 && activeCount > 0 && !resolvedModels.length) { container.innerHTML = this.renderStatusMessage( `${activeCount} download${activeCount > 1 ? 's' : ''} in progress. Local matches will refresh when complete.`, 'info' @@ -850,7 +944,7 @@ export const missingBrowserMethods = { } // Sort missing models: those with 100% confidence matches first, then others - const sortedMissingModels = [...missingModels].sort((a, b) => { + const sortedMissingModels = [...visibleMissingModels].sort((a, b) => { const aMatches = a.matches || []; const bMatches = b.matches || []; @@ -890,9 +984,10 @@ export const missingBrowserMethods = { container.innerHTML = this.renderMissingModelsBrowser( sortedMissingModels, this.selectedMissingModelKey, - totalMissing, + sortedMissingModels.length, activeCount, - hasAny100Match + hasAny100Match, + { hiddenResolvedCount, resolvedCount, rawMissingCount, missingCount: unresolvedMissingCount } ); this.wireMissingModelsBrowser(container, data, sortedMissingModels); this.scheduleInitialUrnLocalMatchRefresh(sortedMissingModels, container, data);