diff --git a/src/components/dialog/content/manager/button/PackInstallButton.vue b/src/components/dialog/content/manager/button/PackInstallButton.vue
index 4228eb92e2..0224c1cb5b 100644
--- a/src/components/dialog/content/manager/button/PackInstallButton.vue
+++ b/src/components/dialog/content/manager/button/PackInstallButton.vue
@@ -18,6 +18,7 @@
import { inject, ref } from 'vue'
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
+import { useConflictDetection } from '@/composables/useConflictDetection'
import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
@@ -75,15 +76,20 @@ const installAllPacks = async () => {
if (!nodePacks?.length) return
if (hasConflict && conflictInfo) {
- const conflictedPackages: ConflictDetectionResult[] = nodePacks.map(
- (pack) => ({
- package_id: pack.id || '',
- package_name: pack.name || '',
- has_conflict: true,
- conflicts: conflictInfo || [],
- is_compatible: false
+ // Check each package individually for conflicts
+ const { checkNodeCompatibility } = useConflictDetection()
+ const conflictedPackages: ConflictDetectionResult[] = nodePacks
+ .map((pack) => {
+ const compatibilityCheck = checkNodeCompatibility(pack)
+ return {
+ package_id: pack.id || '',
+ package_name: pack.name || '',
+ has_conflict: compatibilityCheck.hasConflict,
+ conflicts: compatibilityCheck.conflicts,
+ is_compatible: !compatibilityCheck.hasConflict
+ }
})
- )
+ .filter((result) => result.has_conflict) // Only show packages with conflicts
showNodeConflictDialog({
conflictedPackages,
diff --git a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue
index f89628dce5..692650435c 100644
--- a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue
+++ b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue
@@ -14,12 +14,32 @@
-
+
+
+ {{ $t('manager.mixedSelectionMessage') }}
+
+
+
+
+
-
+
import { useAsyncState } from '@vueuse/core'
-import { computed, onUnmounted } from 'vue'
+import { computed, onUnmounted, provide } from 'vue'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
+import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
+import { useConflictDetection } from '@/composables/useConflictDetection'
+import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
+import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import { components } from '@/types/comfyRegistryTypes'
+import type { ConflictDetail } from '@/types/conflictDetectionTypes'
+import { ImportFailedKey } from '@/types/importFailedTypes'
const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]
}>()
+const managerStore = useComfyManagerStore()
+const conflictDetectionStore = useConflictDetectionStore()
+const { checkNodeCompatibility } = useConflictDetection()
+
const { getNodeDefs } = useComfyRegistryStore()
+// Check if any package has import failed status
+const hasImportFailed = computed(() => {
+ return nodePacks.some((pack) => {
+ if (!pack.id) return false
+ const conflicts = conflictDetectionStore.getConflictsForPackageByID(pack.id)
+ return (
+ conflicts?.conflicts?.some((c) => c.type === 'import_failed') || false
+ )
+ })
+})
+
+// Provide import failed context for PackStatusMessage
+provide(ImportFailedKey, {
+ importFailed: hasImportFailed,
+ showImportFailedDialog: () => {} // No-op for multi-selection
+})
+
+// Check installation status
+const installedPacks = computed(() =>
+ nodePacks.filter((pack) => managerStore.isPackInstalled(pack.id))
+)
+
+const notInstalledPacks = computed(() =>
+ nodePacks.filter((pack) => !managerStore.isPackInstalled(pack.id))
+)
+
+const isAllInstalled = computed(
+ () => installedPacks.value.length === nodePacks.length
+)
+
+const isNoneInstalled = computed(
+ () => notInstalledPacks.value.length === nodePacks.length
+)
+
+const isMixed = computed(
+ () => installedPacks.value.length > 0 && notInstalledPacks.value.length > 0
+)
+
+// Check for conflicts in not-installed packages - store per package
+const packageConflicts = computed(() => {
+ const conflictsByPackage = new Map()
+
+ for (const pack of notInstalledPacks.value) {
+ const compatibilityCheck = checkNodeCompatibility(pack)
+ if (compatibilityCheck.hasConflict && pack.id) {
+ conflictsByPackage.set(pack.id, compatibilityCheck.conflicts)
+ }
+ }
+
+ return conflictsByPackage
+})
+
+// Aggregate all unique conflicts for display
+const conflictInfo = computed(() => {
+ const conflictMap = new Map()
+
+ packageConflicts.value.forEach((conflicts) => {
+ conflicts.forEach((conflict) => {
+ const key = `${conflict.type}-${conflict.current_value}-${conflict.required_value}`
+ if (!conflictMap.has(key)) {
+ conflictMap.set(key, conflict)
+ }
+ })
+ })
+
+ return Array.from(conflictMap.values())
+})
+
+const hasConflicts = computed(() => conflictInfo.value.length > 0)
+
+// Determine the most important status from all selected packages
+const overallStatus = computed(() => {
+ // Check for import failed first (highest priority for installed packages)
+ if (hasImportFailed.value) {
+ // Import failed doesn't have a specific status enum, so we return active
+ // but the PackStatusMessage will handle it via hasImportFailed prop
+ return 'NodeVersionStatusActive' as components['schemas']['NodeVersionStatus']
+ }
+
+ // Priority order: banned > deleted > flagged > pending > active
+ const statusPriority = [
+ 'NodeStatusBanned',
+ 'NodeVersionStatusBanned',
+ 'NodeStatusDeleted',
+ 'NodeVersionStatusDeleted',
+ 'NodeVersionStatusFlagged',
+ 'NodeVersionStatusPending',
+ 'NodeStatusActive',
+ 'NodeVersionStatusActive'
+ ]
+
+ for (const priorityStatus of statusPriority) {
+ if (nodePacks.some((pack) => pack.status === priorityStatus)) {
+ return priorityStatus as
+ | components['schemas']['NodeStatus']
+ | components['schemas']['NodeVersionStatus']
+ }
+ }
+
+ // Default to active if no specific status found
+ return 'NodeVersionStatusActive' as components['schemas']['NodeVersionStatus']
+})
+
const getPackNodes = async (pack: components['schemas']['Node']) => {
if (!pack.latest_version?.version) return []
const nodeDefs = await getNodeDefs.call({
diff --git a/src/components/dialog/content/manager/packIcon/PackIcon.vue b/src/components/dialog/content/manager/packIcon/PackIcon.vue
index 71ec4a400e..ae6d188c20 100644
--- a/src/components/dialog/content/manager/packIcon/PackIcon.vue
+++ b/src/components/dialog/content/manager/packIcon/PackIcon.vue
@@ -13,7 +13,7 @@