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
This commit is contained in:
Dariusz L
2026-07-28 14:50:53 +02:00
parent 1e7cad595c
commit df01b17148
3 changed files with 174 additions and 11 deletions
+132 -1
View File
@@ -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')})`);
+11 -2
View File
@@ -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();
+31 -8
View File
@@ -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;
},