From df01b171482ce8fb7c4663421c073eea3d5d7072 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 28 Jul 2026 14:50:53 +0200 Subject: [PATCH] Reuse visible missing model browser when navigating from resolved model When the resolver dialog is already visible on the Missing Models tab, showResolvedNodeModelInResolver now reuses the existing browser instead of forcing a reload or requeuing the selection. This avoids unnecessary UI refreshes and keeps the current view stable. Changes: - web/resolver/model_resolver.js: skip reopen/queue when the dialog is already visible on the missing tab; prefer existing browser selection - web/resolver/views/missing_browser_methods.js: support preferExistingBrowser in selectWorkflowModelReference and center the selected row instead of scrolling to nearest - tests/test_downloads_tab_workflow_route.mjs: cover reuse behavior and row-patching/centering --- tests/test_downloads_tab_workflow_route.mjs | 133 +++++++++++++++++- web/resolver/model_resolver.js | 13 +- web/resolver/views/missing_browser_methods.js | 39 +++-- 3 files changed, 174 insertions(+), 11 deletions(-) diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 3762d00..cd178d7 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -2,7 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import assert from 'node:assert/strict'; -import { html, normalizePathIdentity } from '../web/resolver/utils/html_utils.js'; +import { + html, + normalizePathIdentity, + safeStorage, +} from '../web/resolver/utils/html_utils.js'; import { getModelCardUrl } from '../web/resolver/utils/url_utils.js'; import { extractComfyWorkflow } from '../web/resolver/utils/workflow_metadata.js'; import { @@ -2320,6 +2324,133 @@ test('workflow model selection wait resolves only after the queued request compl assert.equal(completed.status, 'selected'); }); +test('Show in Model Resolver reuses the visible Missing Models browser without reloading', async () => { + const showResolvedNodeModelInResolver = eval( + `(${extractMethod(modelResolverSource, 'showResolvedNodeModelInResolver')})` + ); + const reference = { + node_id: 7, + widget_index: 0, + original_path: 'model.safetensors', + }; + const analysisData = { resolved_models: [reference] }; + const selectCalls = []; + let loadCount = 0; + let queueCount = 0; + const previousLocalStorageDescriptor = Object.getOwnPropertyDescriptor( + globalThis, + 'localStorage' + ); + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: { setItem() {} }, + }); + + const resolver = { + dialog: { + activeTab: 'missing', + cachedAnalysisData: analysisData, + isVisible: () => true, + persistActiveTab() {}, + selectWorkflowModelReference(...args) { + selectCalls.push(args); + return reference; + }, + queueWorkflowModelReferenceSelection() { + queueCount += 1; + return { status: 'pending' }; + }, + async loadWorkflowData() { + loadCount += 1; + }, + }, + getNodeContextWorkflowState: () => ({ signature: 'current' }), + getCurrentNodeContextAnalysis: () => analysisData, + waitForResolverDialogReady: () => { + throw new Error('The already visible browser should not wait for reopening'); + }, + }; + + try { + await showResolvedNodeModelInResolver.call(resolver, reference); + } finally { + if (previousLocalStorageDescriptor) { + Object.defineProperty( + globalThis, + 'localStorage', + previousLocalStorageDescriptor + ); + } else { + delete globalThis.localStorage; + } + } + + assert.equal(loadCount, 0); + assert.equal(queueCount, 0); + assert.equal(selectCalls.length, 1); + assert.equal(selectCalls[0][0], reference); + assert.equal(selectCalls[0][1], analysisData); + assert.deepEqual(selectCalls[0][2], { preferExistingBrowser: true }); +}); + +test('workflow model selection patches the existing browser and centers its row', () => { + const selectWorkflowModelReference = eval( + `(${extractMethod(missingBrowserMethodsSource, 'selectWorkflowModelReference')})` + ); + const selected = { + node_id: 7, + widget_index: 0, + original_path: 'model.safetensors', + }; + const row = { + dataset: { missingKey: 'selected-key' }, + scrollIntoViewOptions: null, + scrollIntoView(options) { + this.scrollIntoViewOptions = options; + }, + }; + const renderOptions = []; + const previousRequestAnimationFrame = globalThis.requestAnimationFrame; + globalThis.requestAnimationFrame = callback => callback(); + + const dialog = { + cachedAnalysisData: { resolved_models: [selected] }, + contentElement: { + querySelectorAll: () => [row], + }, + selectedMissingModelKey: 'previous-key', + getResolvedWorkflowModels: data => data.resolved_models, + getMissingModelKey: () => 'selected-key', + displayMissingModels(_container, _data, options) { + renderOptions.push(options); + }, + }; + + try { + assert.equal( + selectWorkflowModelReference.call( + dialog, + selected, + dialog.cachedAnalysisData, + { preferExistingBrowser: true } + ), + selected + ); + } finally { + if (previousRequestAnimationFrame) { + globalThis.requestAnimationFrame = previousRequestAnimationFrame; + } else { + delete globalThis.requestAnimationFrame; + } + } + + assert.deepEqual(renderOptions, [{ selectionOnly: true }]); + assert.deepEqual(row.scrollIntoViewOptions, { + block: 'center', + inline: 'nearest', + }); +}); + test('Local Database source ignores installed local model matches before search', () => { const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`); const getMissingSourceResultStatus = eval(`(${extractMethod(missingBrowserMethodsSource, 'getMissingSourceResultStatus')})`); diff --git a/web/resolver/model_resolver.js b/web/resolver/model_resolver.js index 9c20b67..9ddcda6 100644 --- a/web/resolver/model_resolver.js +++ b/web/resolver/model_resolver.js @@ -654,17 +654,26 @@ export class ModelResolver { } this.dialog.persistActiveTab?.('missing'); + + if (wasVisible && wasOnMissingTab) { + const selected = this.dialog.selectWorkflowModelReference?.( + reference, + analysisData || this.dialog.cachedAnalysisData, + { preferExistingBrowser: true } + ); + if (selected) return; + } + this.dialog.showResolvedModels = true; this.dialog.missingModelsTypeFilter = 'all'; this.dialog.missingModelsTypeFilterMenuOpen = false; - const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference); - try { localStorage.setItem(this.dialog.showResolvedModelsStorageKey, '1'); } catch (error) { log.debug('Model Resolver: failed to persist resolved-model visibility.', error); } + const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference); if (!wasVisible) { this.dialog.activeTab = 'missing'; this.activateResolverButton(); diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 1f16b81..96eb4c0 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -538,23 +538,46 @@ export const missingBrowserMethods = { return selected; }, - selectWorkflowModelReference(reference = {}, data = this.cachedAnalysisData || {}) { + selectWorkflowModelReference( + reference = {}, + data = this.cachedAnalysisData || {}, + options = {} + ) { const selected = this.getResolvedWorkflowModels(data) .find(model => matchesWorkflowModelReference(model, reference)); if (!selected || !this.contentElement) return null; - this.showResolvedModels = true; - this.missingModelsTypeFilter = 'all'; - this.missingModelsTypeFilterMenuOpen = false; - safeStorage.setItem(this.showResolvedModelsStorageKey, '1'); - this.selectedMissingModelKey = this.getMissingModelKey(selected); - this.displayMissingModels(this.contentElement, data); + const selectedKey = this.getMissingModelKey(selected); + const selectionChanged = selectedKey !== this.selectedMissingModelKey; + const existingRow = Array.from( + this.contentElement.querySelectorAll?.('.mr-missing-list-row') || [] + ).find(item => item.dataset.missingKey === selectedKey); + const reuseExistingBrowser = Boolean( + options.preferExistingBrowser && existingRow + ); + this.selectedMissingModelKey = selectedKey; + + if (reuseExistingBrowser) { + if (selectionChanged) { + this.displayMissingModels( + this.contentElement, + data, + { selectionOnly: true } + ); + } + } else { + this.showResolvedModels = true; + this.missingModelsTypeFilter = 'all'; + this.missingModelsTypeFilterMenuOpen = false; + safeStorage.setItem(this.showResolvedModelsStorageKey, '1'); + this.displayMissingModels(this.contentElement, data); + } requestAnimationFrame(() => { const row = Array.from( this.contentElement?.querySelectorAll?.('.mr-missing-list-row') || [] ).find(item => item.dataset.missingKey === this.selectedMissingModelKey); - row?.scrollIntoView?.({ block: 'nearest' }); + row?.scrollIntoView?.({ block: 'center', inline: 'nearest' }); }); return selected; },