diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs
index 8e1ca66..a7a6721 100644
--- a/tests/test_downloads_tab_workflow_route.mjs
+++ b/tests/test_downloads_tab_workflow_route.mjs
@@ -1642,6 +1642,74 @@ test('local match bad folder badge treats unet gguf as diffusion models', () =>
assert.equal(html, '');
});
+test('local match bad type badge warns when node catalog does not accept gguf', () => {
+ const getModelFileTypeInfo = eval(`(${extractMethod(searchPanelMethodsSource, 'getModelFileTypeInfo')})`);
+ const getMissingAcceptedModelFileTypes = eval(`(${extractMethod(searchPanelMethodsSource, 'getMissingAcceptedModelFileTypes')})`);
+ const getLocalMatchBadTypeWarning = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchBadTypeWarning')})`);
+ const renderLocalMatchBadTypeBadge = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchBadTypeBadge')})`);
+ const dialog = {
+ getModelFileTypeInfo,
+ getMissingAcceptedModelFileTypes,
+ getLocalMatchBadTypeWarning,
+ getCurrentComfyCatalogValues(nodeType, widgetName) {
+ assert.equal(nodeType, 'CLIPLoader');
+ assert.equal(widgetName, 'clip_name');
+ return [
+ 'QWEN/qwen3vl_4b_fp8_scaled.safetensors',
+ 'FLUX/t5xxl_fp16.safetensors',
+ ];
+ },
+ escapeHtml(value) {
+ return String(value).replace(/"/g, '"');
+ },
+ };
+
+ const html = renderLocalMatchBadTypeBadge.call(
+ dialog,
+ {
+ node_type: 'CLIPLoader',
+ node_title: 'Load CLIP',
+ widget_name: 'clip_name',
+ widget_index: 0,
+ },
+ {
+ confidence: 76.7,
+ model: { relative_path: 'QWEN/Qwen3-4B-abliterated-iq4_nl.gguf' },
+ }
+ );
+
+ assert.match(html, /mr-match-status-bad-type[^>]*>Bad type);
+ assert.match(html, /This file is GGUF \(.gguf\)/);
+ assert.match(html, /Load CLIP only lists Safetensors \(.safetensors\) files/);
+ assert.match(html, /use a loader made for GGUF models/);
+});
+
+test('local match bad type badge is omitted for a file type accepted by node catalog', () => {
+ const getModelFileTypeInfo = eval(`(${extractMethod(searchPanelMethodsSource, 'getModelFileTypeInfo')})`);
+ const getMissingAcceptedModelFileTypes = eval(`(${extractMethod(searchPanelMethodsSource, 'getMissingAcceptedModelFileTypes')})`);
+ const getLocalMatchBadTypeWarning = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchBadTypeWarning')})`);
+ const renderLocalMatchBadTypeBadge = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchBadTypeBadge')})`);
+ const dialog = {
+ getModelFileTypeInfo,
+ getMissingAcceptedModelFileTypes,
+ getLocalMatchBadTypeWarning,
+ getCurrentComfyCatalogValues() {
+ return ['QWEN/qwen3vl_4b_fp8_scaled.safetensors'];
+ },
+ escapeHtml(value) {
+ return String(value);
+ },
+ };
+
+ const html = renderLocalMatchBadTypeBadge.call(
+ dialog,
+ { node_type: 'CLIPLoader', widget_name: 'clip_name', widget_index: 0 },
+ { model: { relative_path: 'QWEN/another_text_encoder.safetensors' } }
+ );
+
+ assert.equal(html, '');
+});
+
test('download category normalization maps unet gguf to diffusion models', () => {
const normalizeDownloadCategory = eval(`(${extractMethod(downloadTargetMethodsSource, 'normalizeDownloadCategory')})`);
diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css
index 679ff49..3308afa 100644
--- a/web/css/resolver-main.css
+++ b/web/css/resolver-main.css
@@ -1916,7 +1916,8 @@
background: rgba(241, 207, 100, 0.10);
}
-.mr-match-status-bad-folder {
+.mr-match-status-bad-folder,
+.mr-match-status-bad-type {
color: #fca5a5;
background: rgba(248, 113, 113, 0.12);
box-shadow: inset 0 0 0 1px rgba(248, 113, 113, 0.22);
@@ -1930,7 +1931,8 @@
cursor: help;
}
-.mr-match-status-bad-folder:focus-visible {
+.mr-match-status-bad-folder:focus-visible,
+.mr-match-status-bad-type:focus-visible {
outline: 2px solid rgba(248, 113, 113, 0.58);
outline-offset: 2px;
}
diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js
index 61eba92..a3b9a3a 100644
--- a/web/resolver/search/search_panel.js
+++ b/web/resolver/search/search_panel.js
@@ -2493,6 +2493,95 @@ export const searchPanelMethods = {
return `Bad folder`;
},
+ getModelFileTypeInfo(value = '') {
+ const match = String(value || '').trim().match(/\.([a-z0-9]+)(?:$|[?#])/i);
+ if (!match) return null;
+
+ const extension = match[1].toLowerCase();
+ const labels = {
+ safetensors: 'Safetensors',
+ gguf: 'GGUF',
+ ckpt: 'Checkpoint',
+ pt: 'PyTorch',
+ pt2: 'PyTorch',
+ pth: 'PyTorch',
+ bin: 'Binary model',
+ onnx: 'ONNX',
+ pkl: 'Pickle',
+ sft: 'SFT'
+ };
+ if (!labels[extension]) return null;
+
+ return {
+ extension,
+ label: labels[extension],
+ display: `${labels[extension]} (.${extension})`
+ };
+ },
+
+ getMissingAcceptedModelFileTypes(missing = {}) {
+ const explicitExtensions = missing.accepted_extensions
+ || missing.allowed_extensions
+ || missing.accepted_model_extensions;
+ let acceptedValues = Array.isArray(explicitExtensions) ? explicitExtensions : [];
+
+ if (!acceptedValues.length) {
+ const widgetName = missing.widget_name
+ || this.getComfyWidgetNameByIndex?.(missing.node_type || '', Number(missing.widget_index))
+ || '';
+ acceptedValues = this.getCurrentComfyCatalogValues?.(
+ missing.node_type || '',
+ widgetName
+ ) || [];
+ }
+
+ const byExtension = new Map();
+ for (const value of acceptedValues) {
+ const typeInfo = this.getModelFileTypeInfo?.(value);
+ if (typeInfo && !byExtension.has(typeInfo.extension)) {
+ byExtension.set(typeInfo.extension, typeInfo);
+ }
+ }
+ return Array.from(byExtension.values());
+ },
+
+ getLocalMatchBadTypeWarning(missing = {}, match = {}) {
+ const model = match.model && typeof match.model === 'object' ? match.model : {};
+ const matchValue = model.relative_path
+ || model.path
+ || match.relative_path
+ || match.path
+ || model.filename
+ || match.filename
+ || model.name
+ || match.name
+ || '';
+ const matchType = this.getModelFileTypeInfo?.(matchValue);
+ if (!matchType) return null;
+
+ const acceptedTypes = this.getMissingAcceptedModelFileTypes?.(missing) || [];
+ if (!acceptedTypes.length || acceptedTypes.some(type => type.extension === matchType.extension)) {
+ return null;
+ }
+
+ const acceptedText = acceptedTypes.length === 1
+ ? acceptedTypes[0].display
+ : acceptedTypes.map(type => type.display).join(', ');
+ const nodeLabel = String(missing.node_title || missing.node_type || 'this node').trim();
+
+ return {
+ matchType,
+ acceptedTypes,
+ tooltip: `This file is ${matchType.display}, but ${nodeLabel} only lists ${acceptedText} files.\n\nLinking it may not work. Choose a supported file type or use a loader made for ${matchType.label} models.`
+ };
+ },
+
+ renderLocalMatchBadTypeBadge(missing = {}, match = {}) {
+ const warning = this.getLocalMatchBadTypeWarning?.(missing, match);
+ if (!warning) return '';
+ return `Bad type`;
+ },
+
getActiveDownloadInfoForLocalMatch(match = {}) {
const model = match.model || {};
const normalizePath = (value = '') => normalizePathIdentity(value);
@@ -2580,7 +2669,8 @@ export const searchPanelMethods = {
},
renderLocalMatchStatusGroup(missing = {}, match = {}, hashLabelMap = null) {
- return `${this.renderLocalMatchStatus(match, hashLabelMap)}${this.renderLocalMatchDownloadingBadge(match)}${this.renderLocalMatchBadFolderBadge(missing, match)}`;
+ const badTypeBadge = this.renderLocalMatchBadTypeBadge?.(missing, match) || '';
+ return `${this.renderLocalMatchStatus(match, hashLabelMap)}${this.renderLocalMatchDownloadingBadge(match)}${this.renderLocalMatchBadFolderBadge(missing, match)}${badTypeBadge}`;
},
areLocalMatchAlternativesCollapsed() {