Skip to content

Commit df01b17

Browse files
committed
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
1 parent 1e7cad5 commit df01b17

3 files changed

Lines changed: 174 additions & 11 deletions

File tree

tests/test_downloads_tab_workflow_route.mjs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import test from 'node:test';
44
import assert from 'node:assert/strict';
5-
import { html, normalizePathIdentity } from '../web/resolver/utils/html_utils.js';
5+
import {
6+
html,
7+
normalizePathIdentity,
8+
safeStorage,
9+
} from '../web/resolver/utils/html_utils.js';
610
import { getModelCardUrl } from '../web/resolver/utils/url_utils.js';
711
import { extractComfyWorkflow } from '../web/resolver/utils/workflow_metadata.js';
812
import {
@@ -2320,6 +2324,133 @@ test('workflow model selection wait resolves only after the queued request compl
23202324
assert.equal(completed.status, 'selected');
23212325
});
23222326

2327+
test('Show in Model Resolver reuses the visible Missing Models browser without reloading', async () => {
2328+
const showResolvedNodeModelInResolver = eval(
2329+
`(${extractMethod(modelResolverSource, 'showResolvedNodeModelInResolver')})`
2330+
);
2331+
const reference = {
2332+
node_id: 7,
2333+
widget_index: 0,
2334+
original_path: 'model.safetensors',
2335+
};
2336+
const analysisData = { resolved_models: [reference] };
2337+
const selectCalls = [];
2338+
let loadCount = 0;
2339+
let queueCount = 0;
2340+
const previousLocalStorageDescriptor = Object.getOwnPropertyDescriptor(
2341+
globalThis,
2342+
'localStorage'
2343+
);
2344+
Object.defineProperty(globalThis, 'localStorage', {
2345+
configurable: true,
2346+
value: { setItem() {} },
2347+
});
2348+
2349+
const resolver = {
2350+
dialog: {
2351+
activeTab: 'missing',
2352+
cachedAnalysisData: analysisData,
2353+
isVisible: () => true,
2354+
persistActiveTab() {},
2355+
selectWorkflowModelReference(...args) {
2356+
selectCalls.push(args);
2357+
return reference;
2358+
},
2359+
queueWorkflowModelReferenceSelection() {
2360+
queueCount += 1;
2361+
return { status: 'pending' };
2362+
},
2363+
async loadWorkflowData() {
2364+
loadCount += 1;
2365+
},
2366+
},
2367+
getNodeContextWorkflowState: () => ({ signature: 'current' }),
2368+
getCurrentNodeContextAnalysis: () => analysisData,
2369+
waitForResolverDialogReady: () => {
2370+
throw new Error('The already visible browser should not wait for reopening');
2371+
},
2372+
};
2373+
2374+
try {
2375+
await showResolvedNodeModelInResolver.call(resolver, reference);
2376+
} finally {
2377+
if (previousLocalStorageDescriptor) {
2378+
Object.defineProperty(
2379+
globalThis,
2380+
'localStorage',
2381+
previousLocalStorageDescriptor
2382+
);
2383+
} else {
2384+
delete globalThis.localStorage;
2385+
}
2386+
}
2387+
2388+
assert.equal(loadCount, 0);
2389+
assert.equal(queueCount, 0);
2390+
assert.equal(selectCalls.length, 1);
2391+
assert.equal(selectCalls[0][0], reference);
2392+
assert.equal(selectCalls[0][1], analysisData);
2393+
assert.deepEqual(selectCalls[0][2], { preferExistingBrowser: true });
2394+
});
2395+
2396+
test('workflow model selection patches the existing browser and centers its row', () => {
2397+
const selectWorkflowModelReference = eval(
2398+
`(${extractMethod(missingBrowserMethodsSource, 'selectWorkflowModelReference')})`
2399+
);
2400+
const selected = {
2401+
node_id: 7,
2402+
widget_index: 0,
2403+
original_path: 'model.safetensors',
2404+
};
2405+
const row = {
2406+
dataset: { missingKey: 'selected-key' },
2407+
scrollIntoViewOptions: null,
2408+
scrollIntoView(options) {
2409+
this.scrollIntoViewOptions = options;
2410+
},
2411+
};
2412+
const renderOptions = [];
2413+
const previousRequestAnimationFrame = globalThis.requestAnimationFrame;
2414+
globalThis.requestAnimationFrame = callback => callback();
2415+
2416+
const dialog = {
2417+
cachedAnalysisData: { resolved_models: [selected] },
2418+
contentElement: {
2419+
querySelectorAll: () => [row],
2420+
},
2421+
selectedMissingModelKey: 'previous-key',
2422+
getResolvedWorkflowModels: data => data.resolved_models,
2423+
getMissingModelKey: () => 'selected-key',
2424+
displayMissingModels(_container, _data, options) {
2425+
renderOptions.push(options);
2426+
},
2427+
};
2428+
2429+
try {
2430+
assert.equal(
2431+
selectWorkflowModelReference.call(
2432+
dialog,
2433+
selected,
2434+
dialog.cachedAnalysisData,
2435+
{ preferExistingBrowser: true }
2436+
),
2437+
selected
2438+
);
2439+
} finally {
2440+
if (previousRequestAnimationFrame) {
2441+
globalThis.requestAnimationFrame = previousRequestAnimationFrame;
2442+
} else {
2443+
delete globalThis.requestAnimationFrame;
2444+
}
2445+
}
2446+
2447+
assert.deepEqual(renderOptions, [{ selectionOnly: true }]);
2448+
assert.deepEqual(row.scrollIntoViewOptions, {
2449+
block: 'center',
2450+
inline: 'nearest',
2451+
});
2452+
});
2453+
23232454
test('Local Database source ignores installed local model matches before search', () => {
23242455
const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`);
23252456
const getMissingSourceResultStatus = eval(`(${extractMethod(missingBrowserMethodsSource, 'getMissingSourceResultStatus')})`);

web/resolver/model_resolver.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,17 +654,26 @@ export class ModelResolver {
654654
}
655655

656656
this.dialog.persistActiveTab?.('missing');
657+
658+
if (wasVisible && wasOnMissingTab) {
659+
const selected = this.dialog.selectWorkflowModelReference?.(
660+
reference,
661+
analysisData || this.dialog.cachedAnalysisData,
662+
{ preferExistingBrowser: true }
663+
);
664+
if (selected) return;
665+
}
666+
657667
this.dialog.showResolvedModels = true;
658668
this.dialog.missingModelsTypeFilter = 'all';
659669
this.dialog.missingModelsTypeFilterMenuOpen = false;
660-
const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference);
661-
662670
try {
663671
localStorage.setItem(this.dialog.showResolvedModelsStorageKey, '1');
664672
} catch (error) {
665673
log.debug('Model Resolver: failed to persist resolved-model visibility.', error);
666674
}
667675

676+
const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference);
668677
if (!wasVisible) {
669678
this.dialog.activeTab = 'missing';
670679
this.activateResolverButton();

web/resolver/views/missing_browser_methods.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -538,23 +538,46 @@ export const missingBrowserMethods = {
538538
return selected;
539539
},
540540

541-
selectWorkflowModelReference(reference = {}, data = this.cachedAnalysisData || {}) {
541+
selectWorkflowModelReference(
542+
reference = {},
543+
data = this.cachedAnalysisData || {},
544+
options = {}
545+
) {
542546
const selected = this.getResolvedWorkflowModels(data)
543547
.find(model => matchesWorkflowModelReference(model, reference));
544548
if (!selected || !this.contentElement) return null;
545549

546-
this.showResolvedModels = true;
547-
this.missingModelsTypeFilter = 'all';
548-
this.missingModelsTypeFilterMenuOpen = false;
549-
safeStorage.setItem(this.showResolvedModelsStorageKey, '1');
550-
this.selectedMissingModelKey = this.getMissingModelKey(selected);
551-
this.displayMissingModels(this.contentElement, data);
550+
const selectedKey = this.getMissingModelKey(selected);
551+
const selectionChanged = selectedKey !== this.selectedMissingModelKey;
552+
const existingRow = Array.from(
553+
this.contentElement.querySelectorAll?.('.mr-missing-list-row') || []
554+
).find(item => item.dataset.missingKey === selectedKey);
555+
const reuseExistingBrowser = Boolean(
556+
options.preferExistingBrowser && existingRow
557+
);
558+
this.selectedMissingModelKey = selectedKey;
559+
560+
if (reuseExistingBrowser) {
561+
if (selectionChanged) {
562+
this.displayMissingModels(
563+
this.contentElement,
564+
data,
565+
{ selectionOnly: true }
566+
);
567+
}
568+
} else {
569+
this.showResolvedModels = true;
570+
this.missingModelsTypeFilter = 'all';
571+
this.missingModelsTypeFilterMenuOpen = false;
572+
safeStorage.setItem(this.showResolvedModelsStorageKey, '1');
573+
this.displayMissingModels(this.contentElement, data);
574+
}
552575

553576
requestAnimationFrame(() => {
554577
const row = Array.from(
555578
this.contentElement?.querySelectorAll?.('.mr-missing-list-row') || []
556579
).find(item => item.dataset.missingKey === this.selectedMissingModelKey);
557-
row?.scrollIntoView?.({ block: 'nearest' });
580+
row?.scrollIntoView?.({ block: 'center', inline: 'nearest' });
558581
});
559582
return selected;
560583
},

0 commit comments

Comments
 (0)