From 78697ffc496570ff48ee0df81d2d5be168cb678a Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 19 Jun 2026 20:54:37 +0200 Subject: [PATCH] feat: add active workflow context to download queue items and download data Track the active ComfyUI workflow name, route key, tab identity, and signature on every download-info object, then expose those details in the download queue render. Add a contextual 'Switch to Workflow' menu action and helpers for locating and activating the correct workflow tab from a stored route/label/tab identity. --- web/resolver/actions/queue_methods.js | 567 +++++++++++++++++- .../actions/resolve_download_methods.js | 74 ++- web/resolver/resolver_dialog.js | 12 + web/resolver/views/model_info_methods.js | 15 +- 4 files changed, 642 insertions(+), 26 deletions(-) diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index e7f158d..f50d53b 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -196,28 +196,549 @@ export const queueMethods = { const entries = Object.entries(this.activeDownloads || {}); if (!entries.length) return []; - const workflowKey = this.getWorkflowScopedQueueKey?.() || ''; - const visibleMissingKeys = new Set( - (this.missingModels || []) - .filter(Boolean) - .map(missing => this.getDownloadStateKey?.(missing) || this.getMissingModelKey(missing)) - ); - return entries - .filter(([, info]) => { - if (!info?.missing) return false; - if (workflowKey && info.workflowKey) { - return info.workflowKey === workflowKey; - } - if (visibleMissingKeys.size) { - const missingKey = this.getDownloadStateKey?.(info.missing) || this.getMissingModelKey(info.missing); - return visibleMissingKeys.has(missingKey); - } - return true; - }) + .filter(([, info]) => Boolean(info?.missing)) .map(([downloadId, info]) => ({ downloadId, info })); }, + getWorkflowLabelFromRouteKey(routeKey = '') { + const rawRoute = String(routeKey || '').split('\n')[0].trim(); + if (!rawRoute) return ''; + + const decodeLabel = (value = '') => { + try { + return decodeURIComponent(String(value || '').replace(/\+/g, ' ')); + } catch (error) { + return String(value || ''); + } + }; + const cleanRoutePart = (value = '') => this.cleanWorkflowLabel?.(decodeLabel(value)) || decodeLabel(value).trim(); + const commonRouteParts = new Set(['', '#', '/', 'workflow', 'workflows', 'view', 'tab', 'tabs', 'graph', 'graphs']); + + try { + const base = typeof window !== 'undefined' && window.location?.origin ? window.location.origin : 'http://localhost'; + const url = new URL(rawRoute, base); + const candidates = [ + url.searchParams.get('workflow'), + url.searchParams.get('workflowName'), + url.searchParams.get('workflow_name'), + url.searchParams.get('name'), + url.searchParams.get('filename'), + url.searchParams.get('file'), + url.hash ? new URLSearchParams(url.hash.includes('?') ? url.hash.slice(url.hash.indexOf('?') + 1) : '').get('workflow') : '', + url.hash ? new URLSearchParams(url.hash.includes('?') ? url.hash.slice(url.hash.indexOf('?') + 1) : '').get('workflowName') : '', + url.hash ? new URLSearchParams(url.hash.includes('?') ? url.hash.slice(url.hash.indexOf('?') + 1) : '').get('name') : '' + ]; + const found = candidates.map(cleanRoutePart).find(Boolean); + if (found) return found; + } catch (error) { /* fall through to manual route parsing */ } + + try { + const hash = rawRoute.startsWith('#') ? rawRoute.slice(1) : rawRoute; + const paramsText = hash.includes('?') ? hash.slice(hash.indexOf('?') + 1) : hash.replace(/^\?/, ''); + const params = new URLSearchParams(paramsText); + const candidates = [ + params.get('workflow'), + params.get('workflowName'), + params.get('workflow_name'), + params.get('name'), + params.get('filename'), + params.get('file'), + params.get('workflowId'), + params.get('workflow_id'), + params.get('tab'), + params.get('id') + ]; + const found = candidates.map(cleanRoutePart).find(Boolean); + if (found) return found; + } catch (error) { /* fall through to readable route */ } + + const withoutQuery = rawRoute.split('?')[0].split('&')[0]; + const parts = withoutQuery + .replace(/^#/, '') + .split(/[\\/]/) + .map(part => cleanRoutePart(part)) + .filter(part => !commonRouteParts.has(part.toLowerCase())); + if (parts.length) return parts[parts.length - 1]; + + return cleanRoutePart(rawRoute + .replace(/^#\/?/, '') + .replace(/^\?/, '') + .replace(/[?&](workflow|workflowName|workflow_name|name|filename|file|workflowId|workflow_id|tab|id)=/i, ' ') + .replace(/[=&]+/g, ' ')); + }, + + cleanWorkflowLabel(label = '') { + return String(label || '') + .replace(/[\r\n\t]+/g, ' ') + .replace(/\s+/g, ' ') + .replace(/^[*•\s]+|[×✕✖*•\s]+$/g, '') + .trim(); + }, + + getWorkflowTabActiveSelectors() { + return [ + '[data-workflow-name][aria-selected="true"]', + '[data-workflow-name].active', + '[data-workflow-name].selected', + '[data-workflow-id][aria-selected="true"]', + '[data-workflow-id][data-active="true"]', + '[data-workflow-id].active', + '[data-workflow-id].selected', + '[data-tab-id*="workflow" i][aria-selected="true"]', + '[data-tab-id*="workflow" i][data-active="true"]', + '[class*="workflow" i][class*="tab" i].active', + '[class*="workflow" i][class*="tab" i].selected', + '[class*="workflow" i][class*="tab" i].p-highlight', + '[class*="workflow" i][class*="tab" i].p-tab-active', + '[class*="tab" i][class*="workflow" i].active', + '[class*="tab" i][class*="workflow" i].selected', + '[class*="tab" i][class*="workflow" i].p-highlight', + '[class*="tab" i][class*="workflow" i].p-tab-active', + '[role="tab"][aria-selected="true"]', + '[role="tab"][data-active="true"]', + '[role="tab"].active', + '[role="tab"].selected', + '[role="tab"].p-highlight', + '[role="tab"].p-tab-active' + ]; + }, + + getWorkflowTabSearchSelectors() { + return [ + '[data-workflow-name]', + '[data-workflow-id]', + '[data-tab-id*="workflow" i]', + '[aria-controls*="workflow" i]', + '[class*="workflow" i][class*="tab" i]', + '[class*="tab" i][class*="workflow" i]', + '[role="tab"]' + ]; + }, + + getWorkflowTabElementClassText(element) { + return String(element?.getAttribute?.('class') || ''); + }, + + getWorkflowTabElementIdentity(element) { + if (!element) return null; + + const workflowTabId = String( + element.getAttribute?.('data-workflow-id') + || element.dataset?.workflowId + || element.getAttribute?.('data-tab-id') + || element.dataset?.tabId + || '' + ).trim(); + const workflowTabName = String( + element.getAttribute?.('data-workflow-name') + || element.dataset?.workflowName + || element.getAttribute?.('data-tab-title') + || element.dataset?.tabTitle + || '' + ).trim(); + const workflowTabAriaControls = String(element.getAttribute?.('aria-controls') || '').trim(); + const title = String(element.getAttribute?.('title') || '').trim(); + const ariaLabel = String(element.getAttribute?.('aria-label') || '').trim(); + const workflowTabText = this.cleanWorkflowLabel(element.textContent || ''); + const workflowLabel = this.cleanWorkflowLabel(workflowTabName || title || ariaLabel || workflowTabText); + + return { + element, + workflowLabel, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText, + workflowTabTitle: title, + workflowTabAriaLabel: ariaLabel + }; + }, + + isLikelyWorkflowTabElement(element, label = '') { + if (!element) return false; + if (element.closest?.('#model-resolver-modal, .model-resolver-backdrop')) return false; + + const attrText = [ + element.getAttribute?.('data-workflow-id') || '', + element.getAttribute?.('data-workflow-name') || '', + element.getAttribute?.('data-tab-id') || '', + element.getAttribute?.('aria-controls') || '', + element.getAttribute?.('title') || '', + element.getAttribute?.('aria-label') || '', + this.getWorkflowTabElementClassText(element) + ].join(' '); + + if (/workflow|untitled|unsaved|\.json/i.test(`${label} ${attrText}`)) return true; + if (element.hasAttribute?.('data-workflow-id') || element.hasAttribute?.('data-workflow-name')) return true; + if (element.matches?.('[role="tab"]')) { + const normalized = this.cleanWorkflowLabel(label).toLowerCase(); + if (!normalized) return false; + if (/^(queued|downloads|missing|loaded|options|settings|queue|history|logs|console|manager)$/i.test(normalized)) { + return false; + } + return true; + } + + return false; + }, + + isWorkflowTabElementActive(element) { + if (!element) return false; + const classList = element.classList; + return element.getAttribute?.('aria-selected') === 'true' + || element.getAttribute?.('data-active') === 'true' + || classList?.contains('active') + || classList?.contains('selected') + || classList?.contains('p-highlight') + || classList?.contains('p-tab-active'); + }, + + findActiveWorkflowTabElement() { + if (typeof document === 'undefined') return null; + + for (const selector of this.getWorkflowTabActiveSelectors()) { + let elements = []; + try { + elements = Array.from(document.querySelectorAll(selector)); + } catch (error) { + continue; + } + + for (const element of elements) { + const identity = this.getWorkflowTabElementIdentity(element); + if (!identity?.workflowLabel && !identity?.workflowTabId && !identity?.workflowTabName) continue; + if (!this.isLikelyWorkflowTabElement(element, identity.workflowLabel)) continue; + return identity; + } + } + + return null; + }, + + getActiveWorkflowTabContext() { + const activeTab = this.findActiveWorkflowTabElement?.(); + const routeKey = this.getActiveWorkflowRouteKey?.() || this.activeWorkflowRouteKey || ''; + const workflow = this.getCurrentWorkflow?.(); + const workflowSignature = this.activeWorkflowSignature || (workflow ? this.getWorkflowSignature?.(workflow) : '') || ''; + const fallbackLabel = this.getWorkflowLabelFromComfyState?.() + || activeTab?.workflowLabel + || this.getWorkflowLabelFromRouteKey?.(routeKey) + || 'Current workflow'; + + return { + workflowRouteKey: routeKey, + workflowSignature, + workflowLabel: activeTab?.workflowLabel || fallbackLabel, + workflowTabId: activeTab?.workflowTabId || '', + workflowTabName: activeTab?.workflowTabName || '', + workflowTabAriaControls: activeTab?.workflowTabAriaControls || '', + workflowTabText: activeTab?.workflowTabText || '' + }; + }, + + getWorkflowLabelFromComfyState() { + const candidates = []; + try { + const activeWorkflow = app?.workflowManager?.activeWorkflow + || app?.workflowManager?.workflow + || app?.ui?.workflow?.activeWorkflow + || app?.canvas?.workflow; + candidates.push( + activeWorkflow?.name, + activeWorkflow?.title, + activeWorkflow?.filename, + activeWorkflow?.path?.split?.(/[\\/]/)?.pop?.(), + activeWorkflow?.workflow?.name, + activeWorkflow?.workflow?.title + ); + } catch (error) { /* ignore unavailable Comfy internals */ } + + for (const candidate of candidates) { + const label = this.cleanWorkflowLabel(candidate); + if (label) return label; + } + return ''; + }, + + getWorkflowLabelFromActiveTabElement() { + return this.findActiveWorkflowTabElement?.()?.workflowLabel || ''; + }, + + getActiveWorkflowDownloadLabel() { + const stateLabel = this.getWorkflowLabelFromComfyState?.(); + if (stateLabel) return stateLabel; + + const tabLabel = this.getWorkflowLabelFromActiveTabElement?.(); + if (tabLabel) return tabLabel; + + const routeKey = this.getActiveWorkflowRouteKey?.() || this.activeWorkflowRouteKey || ''; + const routeLabel = this.getWorkflowLabelFromRouteKey(routeKey); + return routeLabel || 'Current workflow'; + }, + + getDownloadWorkflowLabel(info = {}) { + return info.workflowLabel + || this.getWorkflowLabelFromRouteKey(info.workflowRouteKey || '') + || this.getWorkflowLabelFromRouteKey(info.workflowKey || '') + || this.getActiveWorkflowDownloadLabel?.() + || 'Unknown workflow'; + }, + + getWorkflowContextRouteKey(context = {}) { + const direct = context.workflow_route_key || context.workflowRouteKey || ''; + if (direct) return String(direct).split('\n')[0].trim(); + + const key = context.workflow_key || context.workflowKey || ''; + return String(key || '').split('\n')[0].trim(); + }, + + isUsableWorkflowRouteKey(routeKey = '') { + const normalized = String(routeKey || '').trim(); + return Boolean(normalized && normalized !== '#'); + }, + + getWorkflowContextSignature(context = {}) { + const direct = context.workflow_signature || context.workflowSignature || ''; + if (direct) return String(direct).trim(); + + const key = context.workflow_key || context.workflowKey || ''; + const parts = String(key || '').split('\n').map(part => part.trim()).filter(Boolean); + return parts.length > 1 ? parts.slice(1).join('\n') : ''; + }, + + getWorkflowContextLabel(context = {}) { + return this.cleanWorkflowLabel( + context.workflow_label + || context.workflowLabel + || context.workflow_tab_name + || context.workflowTabName + || context.workflow_tab_text + || context.workflowTabText + || this.getWorkflowLabelFromRouteKey?.(this.getWorkflowContextRouteKey(context)) + || '' + ); + }, + + getDownloadWorkflowRuntimeContext(context = {}) { + const downloadId = context.download_id || context.downloadId || ''; + const info = downloadId ? this.activeDownloads?.[downloadId] : null; + if (!info) return context; + + return { + ...context, + workflowRouteKey: context.workflowRouteKey || context.workflow_route_key || info.workflowRouteKey || '', + workflow_route_key: context.workflow_route_key || context.workflowRouteKey || info.workflowRouteKey || '', + workflowKey: context.workflowKey || info.workflowKey || '', + workflowLabel: context.workflowLabel || context.workflow_label || info.workflowLabel || '', + workflow_label: context.workflow_label || context.workflowLabel || info.workflowLabel || '', + workflowSignature: context.workflowSignature || info.workflowSignature || '', + workflowTabId: context.workflowTabId || context.workflow_tab_id || info.workflowTabId || '', + workflow_tab_id: context.workflow_tab_id || context.workflowTabId || info.workflowTabId || '', + workflowTabName: context.workflowTabName || context.workflow_tab_name || info.workflowTabName || '', + workflow_tab_name: context.workflow_tab_name || context.workflowTabName || info.workflowTabName || '', + workflowTabAriaControls: context.workflowTabAriaControls || context.workflow_tab_aria_controls || info.workflowTabAriaControls || '', + workflow_tab_aria_controls: context.workflow_tab_aria_controls || context.workflowTabAriaControls || info.workflowTabAriaControls || '', + workflowTabText: context.workflowTabText || context.workflow_tab_text || info.workflowTabText || '', + workflow_tab_text: context.workflow_tab_text || context.workflowTabText || info.workflowTabText || '' + }; + }, + + canSwitchToDownloadWorkflow(context = {}) { + context = this.getDownloadWorkflowRuntimeContext(context); + const label = this.getWorkflowContextLabel(context); + const hasSpecificLabel = Boolean(label && !/^(current workflow|unknown workflow|workflow)$/i.test(label)); + return Boolean( + this.isUsableWorkflowRouteKey(this.getWorkflowContextRouteKey(context)) + || this.getWorkflowContextSignature(context) + || hasSpecificLabel + || context.workflow_tab_id + || context.workflowTabId + || context.workflow_tab_aria_controls + || context.workflowTabAriaControls + ); + }, + + isDownloadWorkflowActive(context = {}) { + context = this.getDownloadWorkflowRuntimeContext(context); + const routeKey = this.getWorkflowContextRouteKey(context); + const currentRoute = this.getActiveWorkflowRouteKey?.() || this.activeWorkflowRouteKey || ''; + if (this.isUsableWorkflowRouteKey(routeKey) && currentRoute && routeKey === currentRoute) return true; + + const signature = this.getWorkflowContextSignature(context); + const currentSignature = this.activeWorkflowSignature || this.getWorkflowSignature?.(this.getCurrentWorkflow?.()) || ''; + if (signature && currentSignature && signature === currentSignature) return true; + + const activeTab = this.findActiveWorkflowTabElement?.(); + if (activeTab) { + const targetTabId = String(context.workflow_tab_id || context.workflowTabId || '').trim(); + const targetTabName = String(context.workflow_tab_name || context.workflowTabName || '').trim(); + const targetAriaControls = String(context.workflow_tab_aria_controls || context.workflowTabAriaControls || '').trim(); + if (targetTabId && targetTabId === activeTab.workflowTabId) return true; + if (targetTabName && targetTabName === activeTab.workflowTabName) return true; + if (targetAriaControls && targetAriaControls === activeTab.workflowTabAriaControls) return true; + } + + const label = this.cleanWorkflowLabel(this.getWorkflowContextLabel(context)).toLowerCase(); + const activeLabel = this.cleanWorkflowLabel(this.getActiveWorkflowDownloadLabel?.()).toLowerCase(); + return Boolean(label && activeLabel && label === activeLabel); + }, + + getDownloadQueueContext(progress = {}, info = {}, workflowLabel = '', downloadId = '') { + const folderContext = this.getDownloadFolderContext?.(progress, info) || null; + const missing = info?.missing || {}; + const label = workflowLabel || this.getDownloadWorkflowLabel?.(info) || ''; + const context = { + ...(folderContext || {}), + context_scope: 'download_queue', + download_id: downloadId, + open_folder_label: folderContext ? 'Open Download Folder' : '', + name: folderContext?.name || progress?.filename || info?.filename || missing?.original_path?.split(/[\/\\]/).pop() || 'Download', + workflow_label: label, + workflow_route_key: info.workflowRouteKey || this.getWorkflowContextRouteKey(info) || '', + workflow_tab_id: info.workflowTabId || '', + workflow_tab_name: info.workflowTabName || '', + workflow_tab_aria_controls: info.workflowTabAriaControls || '', + workflow_tab_text: info.workflowTabText || '', + node_id: missing.locate_node_id ?? missing.node_id ?? '', + widget_index: missing.widget_index ?? '', + subgraph_id: missing.locate_subgraph_id || missing.subgraph_id || '', + is_top_level: missing.locate_is_top_level ?? missing.is_top_level ?? true + }; + + return (folderContext || this.canSwitchToDownloadWorkflow(context)) ? context : null; + }, + + getWorkflowTabComparisonValues(identity = {}, element = null) { + if (!identity) return []; + return [ + identity.workflowLabel, + identity.workflowTabId, + identity.workflowTabName, + identity.workflowTabAriaControls, + identity.workflowTabText, + identity.workflowTabTitle, + identity.workflowTabAriaLabel, + element?.getAttribute?.('href') || '', + element?.getAttribute?.('data-route') || '', + element?.getAttribute?.('data-path') || '' + ].map(value => String(value || '').trim()).filter(Boolean); + }, + + findWorkflowTabForDownload(context = {}) { + if (typeof document === 'undefined') return null; + + context = this.getDownloadWorkflowRuntimeContext(context); + const routeKey = this.getWorkflowContextRouteKey(context); + const routeLabel = this.getWorkflowLabelFromRouteKey?.(routeKey) || ''; + const labelTargets = new Set([ + this.getWorkflowContextLabel(context), + routeLabel, + context.workflow_tab_text, + context.workflowTabText, + context.workflow_tab_name, + context.workflowTabName + ].map(value => this.cleanWorkflowLabel(value).toLowerCase()).filter(Boolean)); + const idTargets = new Set([ + context.workflow_tab_id, + context.workflowTabId, + context.workflow_tab_aria_controls, + context.workflowTabAriaControls + ].map(value => String(value || '').trim().toLowerCase()).filter(Boolean)); + const routeTarget = String(routeKey || '').trim().toLowerCase(); + const seen = new Set(); + let bestMatch = null; + + for (const selector of this.getWorkflowTabSearchSelectors()) { + let elements = []; + try { + elements = Array.from(document.querySelectorAll(selector)); + } catch (error) { + continue; + } + + for (const element of elements) { + if (!element || seen.has(element)) continue; + seen.add(element); + + const identity = this.getWorkflowTabElementIdentity(element); + if (!identity || !this.isLikelyWorkflowTabElement(element, identity.workflowLabel)) continue; + + const values = this.getWorkflowTabComparisonValues(identity, element); + const normalizedValues = values.map(value => this.cleanWorkflowLabel(value).toLowerCase()).filter(Boolean); + const rawValues = values.map(value => String(value || '').trim().toLowerCase()).filter(Boolean); + let score = 0; + + for (const value of rawValues) { + if (idTargets.has(value)) score += 100; + if (routeTarget && value.includes(routeTarget)) score += 70; + } + + for (const value of normalizedValues) { + if (labelTargets.has(value)) score += 80; + for (const target of labelTargets) { + if (target && value !== target && (value.includes(target) || target.includes(value))) score += 25; + } + } + + if (this.isWorkflowTabElementActive(element)) score += 5; + + if (score > (bestMatch?.score || 0)) { + bestMatch = { element, score }; + } + } + } + + return bestMatch?.score > 0 ? bestMatch.element : null; + }, + + switchToDownloadWorkflow(context = {}) { + context = this.getDownloadWorkflowRuntimeContext(context); + if (!this.canSwitchToDownloadWorkflow(context)) { + this.showNotification('No workflow information available for this download', 'error'); + return false; + } + + const label = this.getWorkflowContextLabel(context) || 'workflow'; + if (this.isDownloadWorkflowActive(context)) { + this.showNotification(`Already on workflow: ${label}`, 'info'); + return true; + } + + const tab = this.findWorkflowTabForDownload(context); + if (tab) { + try { + tab.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); + tab.click(); + setTimeout(() => { + window.dispatchEvent?.(new Event('model-resolver-active-workflowchange')); + }, 30); + this.showNotification(`Switched to workflow: ${label}`, 'success'); + return true; + } catch (error) { + console.warn('Model Resolver: failed to switch workflow tab', error); + } + } + + const routeKey = this.getWorkflowContextRouteKey(context); + const currentRoute = this.getActiveWorkflowRouteKey?.() || ''; + if (this.isUsableWorkflowRouteKey(routeKey) && routeKey !== currentRoute && typeof window !== 'undefined') { + try { + window.location.hash = routeKey.startsWith('#') ? routeKey : `#${routeKey.replace(/^#/, '')}`; + setTimeout(() => { + window.dispatchEvent?.(new Event('model-resolver-active-workflowchange')); + }, 30); + this.showNotification(`Switched to workflow: ${label}`, 'success'); + return true; + } catch (error) { + console.warn('Model Resolver: failed to switch workflow route', error); + } + } + + this.showNotification(`Could not find workflow tab: ${label}. It may be closed.`, 'error'); + return false; + }, + getActiveQueuePanelDownloadIds() { return this.getActiveQueuePanelDownloads().map(({ downloadId }) => downloadId); }, @@ -249,9 +770,14 @@ export const queueMethods = { const sizeText = total ? `${downloaded} / ${total}` : downloaded; const targetPath = progress.directory || info.downloadDirectory || info.downloadPath || ''; const targetLabel = targetPath ? targetPath.split(/[\/\\]/).filter(Boolean).pop() || targetPath : ''; - const contextModel = this.getDownloadFolderContext?.(progress, info); + const workflowLabel = this.getDownloadWorkflowLabel?.(info) || 'Unknown workflow'; + const contextModel = this.getDownloadQueueContext?.(progress, info, workflowLabel, downloadId); + const hasFolderAction = Boolean(contextModel?.folder_path || contextModel?.download_directory || contextModel?.directory || contextModel?.path || contextModel?.resolved_path); + const contextTooltip = hasFolderAction + ? 'Right-click for workflow and download folder actions' + : 'Right-click to switch to workflow'; const contextData = contextModel - ? ` data-model="${this.escapeHtml(encodeURIComponent(JSON.stringify(contextModel)))}" oncontextmenu="window.MLOpenContextMenu(event, this)" data-tooltip="Right-click to open download folder"` + ? ` data-model="${this.escapeHtml(encodeURIComponent(JSON.stringify(contextModel)))}" oncontextmenu="window.MLOpenContextMenu(event, this)" data-tooltip="${this.escapeHtml(contextTooltip)}"` : ''; html += `
`; @@ -260,6 +786,7 @@ export const queueMethods = { html += `${this.escapeHtml(statusLabel)}`; html += `
`; html += `
Model${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(info.missing?.node_id ?? ''))}
`; + html += `
Workflow${this.escapeHtml(workflowLabel)}
`; if (category) { html += `
Type${this.escapeHtml(category)}
`; } diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 35a0143..b33b81e 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -672,6 +672,14 @@ export const resolveDownloadMethods = { downloadDirectory: progress.directory || info.downloadDirectory || '', baseDirectory: info.baseDirectory || '', sourceUrl: info.sourceUrl || '', + workflowKey: info.workflowKey || '', + workflowRouteKey: info.workflowRouteKey || '', + workflowLabel: info.workflowLabel || this.getWorkflowLabelFromRouteKey?.(info.workflowRouteKey || '') || '', + workflowSignature: info.workflowSignature || '', + workflowTabId: info.workflowTabId || '', + workflowTabName: info.workflowTabName || '', + workflowTabAriaControls: info.workflowTabAriaControls || '', + workflowTabText: info.workflowTabText || '', progress: { ...progress, status, @@ -1076,7 +1084,15 @@ export const resolveDownloadMethods = { const progressDiv = this.contentElement?.querySelector(`#${progressId}`); const downloadBtn = this.contentElement?.querySelector(`#${this.getDownloadButtonElementId(missing)}`); const tokens = this.getStoredTokens(); + const workflowContext = this.getActiveWorkflowTabContext?.() || {}; const workflowKey = this.getWorkflowScopedQueueKey?.() || ''; + const workflowRouteKey = workflowContext.workflowRouteKey || this.getActiveWorkflowRouteKey?.() || this.activeWorkflowRouteKey || ''; + const workflowLabel = workflowContext.workflowLabel || this.getActiveWorkflowDownloadLabel?.() || 'Current workflow'; + const workflowSignature = workflowContext.workflowSignature || this.activeWorkflowSignature || ''; + const workflowTabId = workflowContext.workflowTabId || ''; + const workflowTabName = workflowContext.workflowTabName || ''; + const workflowTabAriaControls = workflowContext.workflowTabAriaControls || ''; + const workflowTabText = workflowContext.workflowTabText || ''; try { this.rememberDownloadSnapshotForMissing(missing, { @@ -1087,6 +1103,14 @@ export const resolveDownloadMethods = { downloadDirectory: '', baseDirectory, sourceUrl: source.url, + workflowKey, + workflowRouteKey, + workflowLabel, + workflowSignature, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText, progress: { status: 'starting', progress: 0, filename }, status: 'starting', isActive: true @@ -1157,7 +1181,14 @@ export const resolveDownloadMethods = { downloadDirectory: data.directory || '', baseDirectory, sourceUrl: source.url, - workflowKey + workflowKey, + workflowRouteKey, + workflowLabel, + workflowSignature, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText }; const snapshot = this.rememberDownloadUiState( downloadId, @@ -1189,6 +1220,14 @@ export const resolveDownloadMethods = { downloadDirectory: '', baseDirectory, sourceUrl: source.url, + workflowKey, + workflowRouteKey, + workflowLabel, + workflowSignature, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText, progress: { status: 'error', filename, error: error.message }, status: 'error', message: error.message, @@ -2164,7 +2203,15 @@ export const resolveDownloadMethods = { const progressDiv = this.contentElement?.querySelector(`#${progressId}`); const tokens = this.getStoredTokens(); const targetSelection = this.getDownloadTargetSelection(missing, category || missing.category || 'checkpoints'); + const workflowContext = this.getActiveWorkflowTabContext?.() || {}; const workflowKey = this.getWorkflowScopedQueueKey?.() || ''; + const workflowRouteKey = workflowContext.workflowRouteKey || this.getActiveWorkflowRouteKey?.() || this.activeWorkflowRouteKey || ''; + const workflowLabel = workflowContext.workflowLabel || this.getActiveWorkflowDownloadLabel?.() || 'Current workflow'; + const workflowSignature = workflowContext.workflowSignature || this.activeWorkflowSignature || ''; + const workflowTabId = workflowContext.workflowTabId || ''; + const workflowTabName = workflowContext.workflowTabName || ''; + const workflowTabAriaControls = workflowContext.workflowTabAriaControls || ''; + const workflowTabText = workflowContext.workflowTabText || ''; const resolvedPathMetadata = pathMetadata || this.getDownloadPathMetadata(missing, { filename, category: targetSelection.category @@ -2197,6 +2244,14 @@ export const resolveDownloadMethods = { downloadDirectory: '', baseDirectory: targetSelection.baseDirectory || '', sourceUrl: url, + workflowKey, + workflowRouteKey, + workflowLabel, + workflowSignature, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText, progress: { status: 'starting', progress: 0, filename }, status: 'starting', isActive: true @@ -2262,7 +2317,14 @@ export const resolveDownloadMethods = { downloadDirectory: data.directory || '', baseDirectory: targetSelection.baseDirectory || '', sourceUrl: url, - workflowKey + workflowKey, + workflowRouteKey, + workflowLabel, + workflowSignature, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText }; const snapshot = this.rememberDownloadUiState( downloadId, @@ -2294,6 +2356,14 @@ export const resolveDownloadMethods = { downloadDirectory: '', baseDirectory: targetSelection.baseDirectory || '', sourceUrl: url, + workflowKey, + workflowRouteKey, + workflowLabel, + workflowSignature, + workflowTabId, + workflowTabName, + workflowTabAriaControls, + workflowTabText, progress: { status: 'error', filename, error: error.message }, status: 'error', message: error.message, diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index c5794f7..509e842 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -122,6 +122,18 @@ export class ResolverManagerDialog extends ComfyDialog { }), $el("span", { textContent: "Open in CivitAI" }) ]), + $el("div.mr-context-menu-divider.mr-context-menu-divider-workflow", { + "data-menu-divider": "workflow" + }), + $el("div.mr-context-menu-item.mr-context-menu-action-switch-workflow", { + "data-menu-action": "switchWorkflow", + onclick: () => this.handleContextMenuAction('switchWorkflow') + }, [ + $el("span.mr-context-menu-item-icon", { + innerHTML: getSvgIcon('internalLink', 'currentColor', 'mr-context-menu-item-svg') + }), + $el("span", { textContent: "Switch to Workflow" }) + ]), $el("div.mr-context-menu-divider.mr-context-menu-divider-folder", { "data-menu-divider": "folder" }), diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index 41e01be..108c6ed 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -27,15 +27,20 @@ export const modelInfoMethods = { const isDownloadTableContext = model?.context_scope === 'download_table'; const isDownloadFolderContext = model?.context_scope === 'download_folder'; const isDownloadRootContext = model?.context_scope === 'download_root'; + const isDownloadQueueContext = model?.context_scope === 'download_queue'; const isFolderOnlyContext = isDownloadFolderContext || isDownloadRootContext; + const isSourceModelContext = !isDownloadTableContext && !isFolderOnlyContext && !isDownloadQueueContext; const hasLocalPath = Boolean(model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path); const showOpenFolder = !isDownloadTableContext && hasLocalPath; - this.setContextMenuItemVisible('showInfo', !isDownloadTableContext && !isFolderOnlyContext); + const showSwitchWorkflow = isDownloadQueueContext && Boolean(this.canSwitchToDownloadWorkflow?.(model)); + this.setContextMenuItemVisible('showInfo', isSourceModelContext); this.setContextMenuItemVisible('showMore', canShowMore); - this.setContextMenuItemVisible('civitai', !isDownloadTableContext && !isFolderOnlyContext); + this.setContextMenuItemVisible('civitai', isSourceModelContext); + this.setContextMenuItemVisible('switchWorkflow', showSwitchWorkflow); this.setContextMenuItemVisible('openFolder', showOpenFolder); - this.setContextMenuDividerVisible('source', (!isDownloadTableContext && !isFolderOnlyContext) || canShowMore); - this.setContextMenuDividerVisible('folder', showOpenFolder && !isFolderOnlyContext); + this.setContextMenuDividerVisible('source', isSourceModelContext || canShowMore); + this.setContextMenuDividerVisible('workflow', showSwitchWorkflow && (isSourceModelContext || canShowMore)); + this.setContextMenuDividerVisible('folder', showOpenFolder && (isSourceModelContext || canShowMore || showSwitchWorkflow)); const openFolderLabel = this.contextMenu.querySelector('.mr-context-menu-action-open-folder span:last-child'); if (openFolderLabel) { @@ -80,6 +85,8 @@ export const modelInfoMethods = { this.openInCivitAI(model); } else if (action === 'openFolder') { this.openContainingFolder(model); + } else if (action === 'switchWorkflow') { + this.switchToDownloadWorkflow(model); } else if (action === 'showInfo') { this.showModelInfo(model); } else if (action === 'showMore') {