diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index e68328269..977954f31 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -294,6 +294,8 @@
"uninstall": "Uninstall",
"uninstalling": "Uninstalling {id}",
"update": "Update",
+ "tryUpdate": "Try Update",
+ "tryUpdateTooltip": "Pull latest changes from repository. Nightly versions may have updates that cannot be detected automatically.",
"uninstallSelected": "Uninstall Selected",
"updateSelected": "Update Selected",
"updateAll": "Update All",
diff --git a/src/workbench/extensions/manager/components/manager/PackVersionBadge.vue b/src/workbench/extensions/manager/components/manager/PackVersionBadge.vue
index 6c59af95d..3d22daaeb 100644
--- a/src/workbench/extensions/manager/components/manager/PackVersionBadge.vue
+++ b/src/workbench/extensions/manager/components/manager/PackVersionBadge.vue
@@ -63,7 +63,7 @@ const {
fill?: boolean
}>()
-const { isUpdateAvailable } = usePackUpdateStatus(nodePack)
+const { isUpdateAvailable } = usePackUpdateStatus(() => nodePack)
const popoverRef = ref()
const managerStore = useComfyManagerStore()
diff --git a/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue b/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
new file mode 100644
index 000000000..4e21364a2
--- /dev/null
+++ b/src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue
index 16a712792..9f25902cd 100644
--- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue
+++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue
@@ -5,7 +5,14 @@
+ >
+
+
+
+
{
isInstalling.value = false
})
+const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePack)
+
const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore()
diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue
index 26651d535..e75a0208d 100644
--- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue
+++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue
@@ -18,12 +18,27 @@
{{ $t('manager.mixedSelectionMessage') }}
-
-
+
+ class="flex w-full justify-center gap-2"
+ >
+
+
+
+
+
+
+
import { useAsyncState } from '@vueuse/core'
-import { computed, onUnmounted, provide, toRef } from 'vue'
+import { computed, onUnmounted, provide, ref, toRef } from 'vue'
+import { useI18n } from 'vue-i18n'
+import IconTextButton from '@/components/button/IconTextButton.vue'
+import DotSpinner from '@/components/common/DotSpinner.vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
@@ -68,6 +86,7 @@ import PackIconStacked from '@/workbench/extensions/manager/components/manager/p
import { usePacksSelection } from '@/workbench/extensions/manager/composables/nodePack/usePacksSelection'
import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus'
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'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
@@ -75,6 +94,8 @@ const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]
}>()
+const { t } = useI18n()
+const managerStore = useComfyManagerStore()
const nodePacksRef = toRef(() => nodePacks)
// Use new composables for cleaner code
@@ -83,11 +104,40 @@ const {
notInstalledPacks,
isAllInstalled,
isNoneInstalled,
- isMixed
+ isMixed,
+ nightlyPacks,
+ hasNightlyPacks
} = usePacksSelection(nodePacksRef)
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef)
+// Batch update state for nightly packs
+const isUpdatingSelected = ref(false)
+
+async function updateSelectedNightlyPacks() {
+ if (nightlyPacks.value.length === 0) return
+
+ isUpdatingSelected.value = true
+ try {
+ for (const pack of nightlyPacks.value) {
+ if (!pack.id) continue
+ await managerStore.updatePack.call({
+ id: pack.id,
+ version: 'nightly'
+ })
+ }
+ managerStore.updatePack.clear()
+ } catch (error) {
+ console.error('Batch nightly update failed:', error)
+ } finally {
+ isUpdatingSelected.value = false
+ }
+}
+
+const updateSelectedLabel = computed(() =>
+ isUpdatingSelected.value ? t('g.updating') : t('manager.updateSelected')
+)
+
const { checkNodeCompatibility } = useConflictDetection()
const { getNodeDefs } = useComfyRegistryStore()
diff --git a/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts b/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts
index 5aec2700d..5286169a6 100644
--- a/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts
+++ b/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts
@@ -1,19 +1,26 @@
+import { toValue } from '@vueuse/core'
import { compare, valid } from 'semver'
+import type { MaybeRefOrGetter } from 'vue'
import { computed } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
export const usePackUpdateStatus = (
- nodePack: components['schemas']['Node']
+ nodePackSource: MaybeRefOrGetter
) => {
- const { isPackInstalled, getInstalledPackVersion } = useComfyManagerStore()
+ const { isPackInstalled, isPackEnabled, getInstalledPackVersion } =
+ useComfyManagerStore()
- const isInstalled = computed(() => isPackInstalled(nodePack?.id))
+ // Use toValue to unwrap the source reactively inside computeds
+ const nodePack = computed(() => toValue(nodePackSource))
+
+ const isInstalled = computed(() => isPackInstalled(nodePack.value?.id))
+ const isEnabled = computed(() => isPackEnabled(nodePack.value?.id))
const installedVersion = computed(() =>
- getInstalledPackVersion(nodePack.id ?? '')
+ getInstalledPackVersion(nodePack.value?.id ?? '')
)
- const latestVersion = computed(() => nodePack.latest_version?.version)
+ const latestVersion = computed(() => nodePack.value?.latest_version?.version)
const isNightlyPack = computed(
() => !!installedVersion.value && !valid(installedVersion.value)
@@ -31,9 +38,19 @@ export const usePackUpdateStatus = (
return compare(latestVersion.value, installedVersion.value) > 0
})
+ /**
+ * Nightly packs can always "try update" since we cannot compare git hashes
+ * to determine if an update is actually available. This allows users to
+ * pull the latest changes from the repository.
+ */
+ const canTryNightlyUpdate = computed(
+ () => isInstalled.value && isEnabled.value && isNightlyPack.value
+ )
+
return {
isUpdateAvailable,
isNightlyPack,
+ canTryNightlyUpdate,
installedVersion,
latestVersion
}
diff --git a/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.ts b/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.ts
index 760d2ba5a..814db951f 100644
--- a/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.ts
+++ b/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.ts
@@ -1,3 +1,4 @@
+import { valid } from 'semver'
import { computed } from 'vue'
import type { Ref } from 'vue'
@@ -41,12 +42,30 @@ export function usePacksSelection(nodePacks: Ref) {
return 'mixed'
})
+ /**
+ * Nightly packs are installed packs with a non-semver version (git hash)
+ * that are also enabled
+ */
+ const nightlyPacks = computed(() =>
+ installedPacks.value.filter((pack) => {
+ if (!pack.id) return false
+ const version = managerStore.getInstalledPackVersion(pack.id)
+ const isNightly = !!version && !valid(version)
+ const isEnabled = managerStore.isPackEnabled(pack.id)
+ return isNightly && isEnabled
+ })
+ )
+
+ const hasNightlyPacks = computed(() => nightlyPacks.value.length > 0)
+
return {
installedPacks,
notInstalledPacks,
isAllInstalled,
isNoneInstalled,
isMixed,
- selectionState
+ selectionState,
+ nightlyPacks,
+ hasNightlyPacks
}
}