[backport core/1.35] feat(manager): add Try Update button for nightly packs (#7635)

Backport of #7610 to `core/1.35`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7635-backport-core-1-35-feat-manager-add-Try-Update-button-for-nightly-packs-2ce6d73d36508135a070e17689cee5e7)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
This commit is contained in:
Comfy Org PR Bot
2025-12-20 09:21:11 +09:00
committed by GitHub
parent a26fc1cd8f
commit 36027a858f
7 changed files with 178 additions and 15 deletions

View File

@@ -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",

View File

@@ -63,7 +63,7 @@ const {
fill?: boolean
}>()
const { isUpdateAvailable } = usePackUpdateStatus(nodePack)
const { isUpdateAvailable } = usePackUpdateStatus(() => nodePack)
const popoverRef = ref()
const managerStore = useComfyManagerStore()

View File

@@ -0,0 +1,63 @@
<template>
<IconTextButton
v-tooltip.top="$t('manager.tryUpdateTooltip')"
v-bind="$attrs"
type="transparent"
:label="computedLabel"
:border="true"
:size="size"
:disabled="isUpdating"
@click="tryUpdate"
>
<template v-if="isUpdating" #icon>
<DotSpinner duration="1s" :size="size === 'sm' ? 12 : 16" />
</template>
</IconTextButton>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import type { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type NodePack = components['schemas']['Node']
const { nodePack, size = 'sm' } = defineProps<{
nodePack: NodePack
size?: ButtonSize
}>()
const { t } = useI18n()
const managerStore = useComfyManagerStore()
const isUpdating = ref(false)
async function tryUpdate() {
if (!nodePack.id) {
console.warn('Pack missing required id:', nodePack)
return
}
isUpdating.value = true
try {
await managerStore.updatePack.call({
id: nodePack.id,
version: 'nightly'
})
managerStore.updatePack.clear()
} catch (error) {
console.error('Nightly update failed:', error)
} finally {
isUpdating.value = false
}
}
const computedLabel = computed(() =>
isUpdating.value ? t('g.updating') : t('manager.tryUpdate')
)
</script>

View File

@@ -5,7 +5,14 @@
<InfoPanelHeader
:node-packs="[nodePack]"
:has-conflict="hasCompatibilityIssues"
/>
>
<template v-if="canTryNightlyUpdate" #install-button>
<div class="flex w-full justify-center gap-2">
<PackTryUpdateButton :node-pack="nodePack" size="md" />
<PackUninstallButton :node-packs="[nodePack]" size="md" />
</div>
</template>
</InfoPanelHeader>
</div>
<div
ref="scrollContainer"
@@ -68,9 +75,12 @@ import type { components } from '@/types/comfyRegistryTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -99,6 +109,8 @@ whenever(isInstalled, () => {
isInstalling.value = false
})
const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePack)
const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore()

View File

@@ -18,12 +18,27 @@
<div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }}
</div>
<!-- All installed: Show uninstall button -->
<PackUninstallButton
<!-- All installed: Show update (if nightly) and uninstall buttons -->
<div
v-else-if="isAllInstalled"
size="md"
:node-packs="installedPacks"
/>
class="flex w-full justify-center gap-2"
>
<IconTextButton
v-if="hasNightlyPacks"
v-tooltip.top="$t('manager.tryUpdateTooltip')"
type="transparent"
:label="updateSelectedLabel"
:border="true"
size="md"
:disabled="isUpdatingSelected"
@click="updateSelectedNightlyPacks"
>
<template v-if="isUpdatingSelected" #icon>
<DotSpinner duration="1s" :size="16" />
</template>
</IconTextButton>
<PackUninstallButton size="md" :node-packs="installedPacks" />
</div>
<!-- None installed: Show install button -->
<PackInstallButton
v-else-if="isNoneInstalled"
@@ -55,8 +70,11 @@
<script setup lang="ts">
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()

View File

@@ -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<components['schemas']['Node']>
) => {
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
}

View File

@@ -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<NodePack[]>) {
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
}
}