Skip to content
2 changes: 2 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const {
fill?: boolean
}>()

const { isUpdateAvailable } = usePackUpdateStatus(nodePack)
const { isUpdateAvailable } = usePackUpdateStatus(() => nodePack)
const popoverRef = ref()

const managerStore = useComfyManagerStore()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Comment on lines +40 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Error handling could provide user feedback.

The tryUpdate function catches errors but only logs to console. Consider showing a toast notification to inform users when the update fails.

🔎 Suggested improvement for user feedback:
+import { useToast } from 'primevue/usetoast'
+
 const { t } = useI18n()
 const managerStore = useComfyManagerStore()
+const toast = useToast()

 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)
+    toast.add({
+      severity: 'error',
+      summary: t('g.error'),
+      detail: String(error),
+      life: 5000
+    })
   } finally {
     isUpdating.value = false
   }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
around lines 40 to 58, the tryUpdate function currently only logs errors to the
console when the nightly update fails; update the error handling to present user
feedback by invoking the app's toast/notification mechanism (e.g., existing
useToast/$toast or Notifications service) with a clear failure message and
optional error details, keep the isUpdating flag toggled as-is, and ensure
managerStore.updatePack.clear() still runs on success; include a success toast
on completion if desired.


const computedLabel = computed(() =>
isUpdating.value ? t('g.updating') : t('manager.tryUpdate')
)
Comment on lines +60 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localization key g.updating expects an {id} parameter.

Looking at main.json line 192, "updating": "Updating {id}" requires an id parameter. The current usage t('g.updating') will show "Updating {id}" literally instead of the pack name.

🔎 Fix the interpolation:
 const computedLabel = computed(() =>
-  isUpdating.value ? t('g.updating') : t('manager.tryUpdate')
+  isUpdating.value ? t('g.updating', { id: nodePack.name || nodePack.id }) : t('manager.tryUpdate')
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const computedLabel = computed(() =>
isUpdating.value ? t('g.updating') : t('manager.tryUpdate')
)
const computedLabel = computed(() =>
isUpdating.value ? t('g.updating', { id: nodePack.name || nodePack.id }) : t('manager.tryUpdate')
)
🤖 Prompt for AI Agents
In
src/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue
around lines 60 to 62, the localization key 'g.updating' requires an {id}
parameter but the code calls t('g.updating') with no params; update the
computedLabel to pass the pack identifier (e.g. pack.name or pack.id) when
isUpdating is true — e.g. call t('g.updating', { id: pack.name || pack.id || ''
}) — so the translation interpolates the pack name and provide a safe fallback
for the id value.

</script>
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -99,6 +109,8 @@ whenever(isInstalled, () => {
isInstalling.value = false
})

const { canTryNightlyUpdate } = usePackUpdateStatus(() => nodePack)

const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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'
Expand All @@ -68,13 +86,16 @@ 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'

const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]
}>()

const { t } = useI18n()
const managerStore = useComfyManagerStore()
const nodePacksRef = toRef(() => nodePacks)

// Use new composables for cleaner code
Expand All @@ -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 {
Comment on lines +130 to +132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider surfacing batch update errors to the user.

Currently, errors are only logged to the console. Users may not realize an update failed. Consider using a toast notification or similar feedback mechanism to inform users when a batch update fails.

🤖 Prompt for AI Agents
In
src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue
around lines 130 to 132, the catch block only logs batch update failures to the
console so users aren't notified; update the catch to also surface the error to
the UI by invoking the app's notification mechanism (for example emit an event,
call the global/toast service like this.$toast.error(...) or dispatch a
notification action to the store) with a concise user-facing message and the
error details, while retaining the console.error for debugging.

isUpdatingSelected.value = false
}
}
Comment on lines +117 to +135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move updatePack.clear() to the finally block for consistent cleanup.

If an error occurs during the batch update loop, clear() is never called, which could leave stale state in the manager store. Moving it to finally ensures cleanup happens regardless of success or failure.

🔎 Apply this diff to ensure consistent cleanup:
 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 {
+    managerStore.updatePack.clear()
     isUpdatingSelected.value = false
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
}
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'
})
}
} catch (error) {
console.error('Batch nightly update failed:', error)
} finally {
managerStore.updatePack.clear()
isUpdatingSelected.value = false
}
}
🤖 Prompt for AI Agents
In
src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue
around lines 117 to 135, the managerStore.updatePack.clear() call is inside the
try block so it won't run if an error occurs; move the clear() call into the
finally block (after isUpdatingSelected.value is reset) so it always runs
regardless of success or failure, and remove the original clear() from the try
block to avoid duplicate calls.


const updateSelectedLabel = computed(() =>
isUpdatingSelected.value ? t('g.updating') : t('manager.updateSelected')
)

const { checkNodeCompatibility } = useConflictDetection()
const { getNodeDefs } = useComfyRegistryStore()

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { valid } from 'semver'
import { computed } from 'vue'
import type { Ref } from 'vue'

Expand Down Expand Up @@ -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
}
}
Loading