diff --git a/src/components/widget/nav/NavItem.vue b/src/components/widget/nav/NavItem.vue
index eb20b9852..b8b62cd5d 100644
--- a/src/components/widget/nav/NavItem.vue
+++ b/src/components/widget/nav/NavItem.vue
@@ -3,7 +3,7 @@
v-tooltip.right="{
value: tooltipText,
disabled: !isOverflowing,
- pt: { text: { class: 'whitespace-nowrap' } }
+ pt: { text: { class: 'w-max whitespace-nowrap' } }
}"
class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
:class="
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index fbb3b965f..f3602479e 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -295,6 +295,17 @@
"changingVersion": "Changing version from {from} to {to}",
"dependencies": "Dependencies",
"inWorkflow": "In Workflow",
+ "nav": {
+ "allExtensions": "All Extensions",
+ "notInstalled": "Not Installed",
+ "installedSection": "INSTALLED",
+ "allInstalled": "All installed",
+ "updatesAvailable": "Updates Available",
+ "conflicting": "Conflicting",
+ "inWorkflowSection": "IN WORKFLOW",
+ "allInWorkflow": "All in: {workflowName}",
+ "missingNodes": "Missing Nodes"
+ },
"infoPanelEmpty": "Click an item to see the info",
"applyChanges": "Apply Changes",
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
diff --git a/src/workbench/extensions/manager/components/manager/ManagerDialog.vue b/src/workbench/extensions/manager/components/manager/ManagerDialog.vue
index ad7657645..8dde24f68 100644
--- a/src/workbench/extensions/manager/components/manager/ManagerDialog.vue
+++ b/src/workbench/extensions/manager/components/manager/ManagerDialog.vue
@@ -16,32 +16,63 @@
-
@@ -79,37 +110,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -118,7 +130,7 @@
-
+
workflowStore.activeWorkflow?.filename ?? t('manager.inWorkflow')
+)
+
// Navigation items for LeftSidePanel
-const navItems = computed(() => [
- { id: ManagerTab.All, label: t('g.all'), icon: 'pi pi-list' },
- { id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi pi-box' },
+const navItems = computed<(NavItemData | NavGroupData)[]>(() => [
{
- id: ManagerTab.Workflow,
- label: t('manager.inWorkflow'),
- icon: 'pi pi-folder'
+ id: ManagerTab.All,
+ label: t('manager.nav.allExtensions'),
+ icon: 'icon-[lucide--list]'
},
{
- id: ManagerTab.Missing,
- label: t('g.missing'),
- icon: 'pi pi-exclamation-circle'
+ id: ManagerTab.NotInstalled,
+ label: t('manager.nav.notInstalled'),
+ icon: 'icon-[lucide--globe]'
},
{
- id: ManagerTab.UpdateAvailable,
- label: t('g.updateAvailable'),
- icon: 'pi pi-sync'
+ title: t('manager.nav.installedSection'),
+ items: [
+ {
+ id: ManagerTab.AllInstalled,
+ label: t('manager.nav.allInstalled'),
+ icon: 'icon-[lucide--download]'
+ },
+ {
+ id: ManagerTab.UpdateAvailable,
+ label: t('manager.nav.updatesAvailable'),
+ icon: 'icon-[lucide--refresh-cw]'
+ },
+ {
+ id: ManagerTab.Conflicting,
+ label: t('manager.nav.conflicting'),
+ icon: 'icon-[lucide--triangle-alert]',
+ badge: conflictDetectionStore.conflictedPackages.length || undefined
+ }
+ ]
+ },
+ {
+ title: t('manager.nav.inWorkflowSection'),
+ items: [
+ {
+ id: ManagerTab.Workflow,
+ label: t('manager.nav.allInWorkflow', {
+ workflowName: workflowName.value
+ }),
+ icon: 'icon-[lucide--share-2]'
+ },
+ {
+ id: ManagerTab.Missing,
+ label: t('manager.nav.missingNodes'),
+ icon: 'icon-[lucide--triangle-alert]'
+ }
+ ]
}
])
const initialTabId = initialTab ?? initialState.selectedTabId ?? ManagerTab.All
const selectedNavId = ref(initialTabId)
+// Helper to find a nav item by id in the nested structure
+const findNavItemById = (
+ items: (NavItemData | NavGroupData)[],
+ id: string | null
+): NavItemData | undefined => {
+ for (const item of items) {
+ if ('items' in item) {
+ const found = item.items.find((subItem) => subItem.id === id)
+ if (found) return found
+ } else if (item.id === id) {
+ return item
+ }
+ }
+ return undefined
+}
+
const selectedTab = computed(() =>
- navItems.value.find((item) => item.id === selectedNavId.value)
+ findNavItemById(navItems.value, selectedNavId.value)
)
const {
@@ -318,120 +384,20 @@ const isInitialLoad = computed(
() => searchResults.value.length === 0 && searchQuery.value === ''
)
-const isEmptySearch = computed(() => searchQuery.value === '')
-const displayPacks = ref([])
-
+// Use the new composable for tab-based display packs
const {
- startFetchInstalled,
- filterInstalledPack,
- installedPacks,
- isLoading: isLoadingInstalled,
- isReady: installedPacksReady
-} = useInstalledPacks()
-
-const {
- startFetchWorkflowPacks,
- filterWorkflowPack,
- workflowPacks,
- isLoading: isLoadingWorkflow,
- isReady: workflowPacksReady
-} = useWorkflowPacks()
-
-const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
- packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
+ displayPacks,
+ isLoading: isTabLoading,
+ workflowPacks
+} = useManagerDisplayPacks(selectedNavId, searchResults, searchQuery, sortField)
+// Tab helpers for template
const isUpdateAvailableTab = computed(
() => selectedTab.value?.id === ManagerTab.UpdateAvailable
)
-const isInstalledTab = computed(
- () => selectedTab.value?.id === ManagerTab.Installed
-)
const isMissingTab = computed(
() => selectedTab.value?.id === ManagerTab.Missing
)
-const isWorkflowTab = computed(
- () => selectedTab.value?.id === ManagerTab.Workflow
-)
-const isAllTab = computed(() => selectedTab.value?.id === ManagerTab.All)
-
-const isOutdatedPack = (pack: components['schemas']['Node']) => {
- const { isUpdateAvailable } = usePackUpdateStatus(pack)
- return isUpdateAvailable.value === true
-}
-const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
- packs.filter(isOutdatedPack)
-
-watch(
- [isUpdateAvailableTab, installedPacks],
- async () => {
- if (!isUpdateAvailableTab.value) return
-
- if (!isEmptySearch.value) {
- displayPacks.value = filterOutdatedPacks(installedPacks.value)
- } else if (
- !installedPacks.value.length &&
- !installedPacksReady.value &&
- !isLoadingInstalled.value
- ) {
- await startFetchInstalled()
- } else {
- displayPacks.value = filterOutdatedPacks(installedPacks.value)
- }
- },
- { immediate: true }
-)
-
-watch(
- [isInstalledTab, installedPacks],
- async () => {
- if (!isInstalledTab.value) return
-
- if (!isEmptySearch.value) {
- displayPacks.value = filterInstalledPack(searchResults.value)
- } else if (
- !installedPacks.value.length &&
- !installedPacksReady.value &&
- !isLoadingInstalled.value
- ) {
- await startFetchInstalled()
- } else {
- displayPacks.value = installedPacks.value
- }
- },
- { immediate: true }
-)
-
-watch(
- [isMissingTab, isWorkflowTab, workflowPacks, installedPacks],
- async () => {
- if (!isWorkflowTab.value && !isMissingTab.value) return
-
- if (!isEmptySearch.value) {
- displayPacks.value = isMissingTab.value
- ? filterMissingPacks(filterWorkflowPack(searchResults.value))
- : filterWorkflowPack(searchResults.value)
- } else if (
- !workflowPacks.value.length &&
- !isLoadingWorkflow.value &&
- !workflowPacksReady.value
- ) {
- await startFetchWorkflowPacks()
- if (isMissingTab.value) {
- await startFetchInstalled()
- }
- } else {
- displayPacks.value = isMissingTab.value
- ? filterMissingPacks(workflowPacks.value)
- : workflowPacks.value
- }
- },
- { immediate: true }
-)
-
-watch([isAllTab, searchResults], () => {
- if (!isAllTab.value) return
- displayPacks.value = searchResults.value
-})
const onClickWarningLink = () => {
window.open(
@@ -442,49 +408,9 @@ const onClickWarningLink = () => {
)
}
-const onResultsChange = () => {
- switch (selectedTab.value?.id) {
- case ManagerTab.Installed:
- displayPacks.value = isEmptySearch.value
- ? installedPacks.value
- : filterInstalledPack(searchResults.value)
- break
- case ManagerTab.Workflow:
- displayPacks.value = isEmptySearch.value
- ? workflowPacks.value
- : filterWorkflowPack(searchResults.value)
- break
- case ManagerTab.Missing:
- if (!isEmptySearch.value) {
- displayPacks.value = filterMissingPacks(
- filterWorkflowPack(searchResults.value)
- )
- }
- break
- case ManagerTab.UpdateAvailable:
- displayPacks.value = isEmptySearch.value
- ? filterOutdatedPacks(installedPacks.value)
- : filterOutdatedPacks(searchResults.value)
- break
- default:
- displayPacks.value = searchResults.value
- }
-}
-
-watch(searchResults, onResultsChange, { flush: 'post' })
-watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
-
const isLoading = computed(() => {
if (isSearchLoading.value) return searchResults.value.length === 0
- if (selectedTab.value?.id === ManagerTab.Installed) {
- return isLoadingInstalled.value
- }
- if (
- selectedTab.value?.id === ManagerTab.Workflow ||
- selectedTab.value?.id === ManagerTab.Missing
- ) {
- return isLoadingWorkflow.value
- }
+ if (isTabLoading.value) return true
return isInitialLoad.value
})
@@ -511,7 +437,7 @@ watch(
const getLoadingCount = () => {
switch (selectedTab.value?.id) {
- case ManagerTab.Installed:
+ case ManagerTab.AllInstalled:
return comfyManagerStore.installedPacksIds?.size
case ManagerTab.Workflow:
return workflowPacks.value?.length
@@ -581,10 +507,6 @@ whenever(selectedNodePack, async () => {
if (packIndex !== -1) {
selectedNodePacks.value.splice(packIndex, 1, mergedPack)
}
- const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id)
- if (idx !== -1) {
- displayPacks.value.splice(idx, 1, mergedPack)
- }
}
})
diff --git a/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue b/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue
index 3000a4a33..0aeab79fe 100644
--- a/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue
+++ b/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue
@@ -1,6 +1,6 @@
+
{{ computedLabel }}
diff --git a/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue b/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
index 218aaf35f..9f681e010 100644
--- a/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
+++ b/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
@@ -1,7 +1,7 @@
diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue
index 9f25902cd..a25a13417 100644
--- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue
+++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue
@@ -1,22 +1,15 @@
-
-
+
+
-
+
{
isInstalling.value = false
})
-const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePack)
-
const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore()
diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue
index c1be524e9..fe47bcca4 100644
--- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue
+++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue
@@ -1,36 +1,24 @@
-
-
-
-
-
- {{ nodePacks[0].name }}
-
-
-
-
-
+
+ {{ nodePacks[0].name }}
+
@@ -47,8 +35,10 @@ import { computed, inject, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import type { components } from '@/types/comfyRegistryTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
+import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
+import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
@@ -76,6 +66,9 @@ watch(
{ immediate: true }
)
+// Check if nightly update is available for the first pack
+const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePacks[0])
+
// Add conflict detection for install button dialog
const { checkNodeCompatibility } = useConflictDetection()
diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue
index 29ae4f986..3da099283 100644
--- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue
+++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue
@@ -1,54 +1,46 @@
-
-
-
-
+
+
+
+
⚠️
{{ importFailed ? $t('g.error') : $t('g.warning') }}
-
+
{{ $t('g.description') }}
-
+
{{ $t('g.nodes') }}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
diff --git a/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts b/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts
new file mode 100644
index 000000000..884c75a7e
--- /dev/null
+++ b/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts
@@ -0,0 +1,207 @@
+import { whenever } from '@vueuse/core'
+import { orderBy } from 'es-toolkit/compat'
+import type { Ref } from 'vue'
+import { computed } from 'vue'
+
+import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
+import type { components } from '@/types/comfyRegistryTypes'
+import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks'
+import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
+import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
+import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
+import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
+import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
+
+type NodePack = components['schemas']['Node']
+
+export function useManagerDisplayPacks(
+ selectedTabId: Ref,
+ searchResults: Ref,
+ searchQuery: Ref,
+ sortField: Ref
+) {
+ const comfyManagerStore = useComfyManagerStore()
+ const conflictDetectionStore = useConflictDetectionStore()
+ const { getSortValue, getSortableFields } = useRegistrySearchGateway()
+
+ const {
+ startFetchInstalled,
+ filterInstalledPack,
+ installedPacks,
+ isLoading: isLoadingInstalled,
+ isReady: installedPacksReady
+ } = useInstalledPacks()
+
+ const {
+ startFetchWorkflowPacks,
+ filterWorkflowPack,
+ workflowPacks,
+ isLoading: isLoadingWorkflow,
+ isReady: workflowPacksReady
+ } = useWorkflowPacks()
+
+ const tabType = computed(() => selectedTabId.value as ManagerTab | null)
+ const isEmptySearch = computed(() => searchQuery.value === '')
+
+ // Sorting function for packs not from searchResults
+ const sortPacks = (packs: NodePack[]) => {
+ if (!sortField.value || packs.length === 0) return packs
+
+ const sortableFields = getSortableFields()
+ const fieldConfig = sortableFields.find((f) => f.id === sortField.value)
+ const direction = fieldConfig?.direction || 'desc'
+
+ return orderBy(
+ packs,
+ [(pack) => getSortValue(pack, sortField.value)],
+ [direction]
+ )
+ }
+
+ // Filter functions
+ const filterNotInstalled = (packs: NodePack[]) =>
+ packs.filter((p) => !comfyManagerStore.isPackInstalled(p.id))
+
+ const filterConflicting = (packs: NodePack[]) =>
+ packs.filter(
+ (p) =>
+ !!p.id &&
+ conflictDetectionStore.conflictedPackages.some(
+ (c) => c.package_id === p.id
+ )
+ )
+
+ const filterOutdated = (packs: NodePack[]) =>
+ packs.filter((p) => {
+ const { isUpdateAvailable } = usePackUpdateStatus(p)
+ return isUpdateAvailable.value
+ })
+
+ const filterMissing = (packs: NodePack[]) =>
+ packs.filter((p) => !comfyManagerStore.isPackInstalled(p.id))
+
+ // Data fetching triggers using whenever
+ const needsInstalledPacks = computed(() =>
+ [
+ ManagerTab.AllInstalled,
+ ManagerTab.UpdateAvailable,
+ ManagerTab.Conflicting
+ ].includes(tabType.value as ManagerTab)
+ )
+
+ const needsWorkflowPacks = computed(() =>
+ [ManagerTab.Workflow, ManagerTab.Missing].includes(
+ tabType.value as ManagerTab
+ )
+ )
+
+ whenever(
+ () =>
+ needsInstalledPacks.value &&
+ !installedPacksReady.value &&
+ !isLoadingInstalled.value,
+ () => startFetchInstalled()
+ )
+
+ whenever(
+ () =>
+ needsWorkflowPacks.value &&
+ !workflowPacksReady.value &&
+ !isLoadingWorkflow.value,
+ () => startFetchWorkflowPacks()
+ )
+
+ // For Missing tab, also need installed packs to determine what's missing
+ whenever(
+ () =>
+ tabType.value === ManagerTab.Missing &&
+ !installedPacksReady.value &&
+ !isLoadingInstalled.value,
+ () => startFetchInstalled()
+ )
+
+ // Single computed for display packs - replaces 7 watches
+ const displayPacks = computed(() => {
+ const tab = tabType.value
+ const hasSearch = !isEmptySearch.value
+
+ switch (tab) {
+ case ManagerTab.All:
+ return searchResults.value
+
+ case ManagerTab.NotInstalled:
+ return filterNotInstalled(searchResults.value)
+
+ case ManagerTab.AllInstalled:
+ return hasSearch
+ ? filterInstalledPack(searchResults.value)
+ : sortPacks(installedPacks.value)
+
+ case ManagerTab.UpdateAvailable:
+ return sortPacks(
+ filterOutdated(
+ hasSearch
+ ? filterInstalledPack(searchResults.value)
+ : installedPacks.value
+ )
+ )
+
+ case ManagerTab.Conflicting:
+ return sortPacks(
+ filterConflicting(
+ hasSearch
+ ? filterInstalledPack(searchResults.value)
+ : installedPacks.value
+ )
+ )
+
+ case ManagerTab.Workflow: {
+ return hasSearch
+ ? filterWorkflowPack(searchResults.value)
+ : sortPacks(workflowPacks.value)
+ }
+
+ case ManagerTab.Missing: {
+ const base = hasSearch
+ ? filterWorkflowPack(searchResults.value)
+ : workflowPacks.value
+ return sortPacks(filterMissing(base))
+ }
+
+ default:
+ return searchResults.value
+ }
+ })
+
+ // Loading state - single computed
+ const isLoading = computed(() => {
+ const tab = tabType.value
+ if (
+ [
+ ManagerTab.AllInstalled,
+ ManagerTab.UpdateAvailable,
+ ManagerTab.Conflicting
+ ].includes(tab as ManagerTab)
+ ) {
+ return isLoadingInstalled.value
+ }
+ if ([ManagerTab.Workflow, ManagerTab.Missing].includes(tab as ManagerTab)) {
+ return isLoadingWorkflow.value
+ }
+ return false
+ })
+
+ const missingNodePacks = computed(() => filterMissing(workflowPacks.value))
+
+ return {
+ displayPacks,
+ isLoading,
+ isLoadingInstalled,
+ isLoadingWorkflow,
+ installedPacks,
+ workflowPacks,
+ filterInstalledPack,
+ filterWorkflowPack,
+ missingNodePacks
+ }
+}
diff --git a/src/workbench/extensions/manager/composables/useRegistrySearch.ts b/src/workbench/extensions/manager/composables/useRegistrySearch.ts
index c6dc9e90e..174785ca2 100644
--- a/src/workbench/extensions/manager/composables/useRegistrySearch.ts
+++ b/src/workbench/extensions/manager/composables/useRegistrySearch.ts
@@ -90,7 +90,10 @@ export function useRegistrySearch(
}
const onQueryChange = () => updateSearchResults({ append: false })
- const onPageChange = () => updateSearchResults({ append: true })
+ const onPageChange = () => {
+ if (pageNumber.value === 0) return
+ updateSearchResults({ append: true })
+ }
watch([sortField, searchMode], onQueryChange)
watch(pageNumber, onPageChange)
diff --git a/src/workbench/extensions/manager/types/comfyManagerTypes.ts b/src/workbench/extensions/manager/types/comfyManagerTypes.ts
index bd7ab024e..b009ec64d 100644
--- a/src/workbench/extensions/manager/types/comfyManagerTypes.ts
+++ b/src/workbench/extensions/manager/types/comfyManagerTypes.ts
@@ -14,10 +14,12 @@ export const IsInstallingKey: InjectionKey[> =
export enum ManagerTab {
All = 'all',
- Installed = 'installed',
+ NotInstalled = 'notInstalled',
+ AllInstalled = 'allInstalled',
+ UpdateAvailable = 'updateAvailable',
+ Conflicting = 'conflicting',
Workflow = 'workflow',
- Missing = 'missing',
- UpdateAvailable = 'updateAvailable'
+ Missing = 'missing'
}
export type TaskLog = {
]