diff --git a/src/components/breadcrumb/SubgraphBreadcrumb.vue b/src/components/breadcrumb/SubgraphBreadcrumb.vue
index 57ca316d78..8e6f72e574 100644
--- a/src/components/breadcrumb/SubgraphBreadcrumb.vue
+++ b/src/components/breadcrumb/SubgraphBreadcrumb.vue
@@ -1,7 +1,7 @@
-
+
+
diff --git a/src/components/common/WorkflowActionsList.test.ts b/src/components/common/WorkflowActionsList.test.ts
index 498d9542d0..67c668fe7d 100644
--- a/src/components/common/WorkflowActionsList.test.ts
+++ b/src/components/common/WorkflowActionsList.test.ts
@@ -17,7 +17,7 @@ function createWrapper(items: WorkflowMenuItem[]) {
describe('WorkflowActionsList', () => {
it('renders action items with label and icon', () => {
const items: WorkflowMenuItem[] = [
- { label: 'Save', icon: 'pi pi-save', command: vi.fn() }
+ { id: 'save', label: 'Save', icon: 'pi pi-save', command: vi.fn() }
]
const wrapper = createWrapper(items)
@@ -28,9 +28,9 @@ describe('WorkflowActionsList', () => {
it('renders separator items', () => {
const items: WorkflowMenuItem[] = [
- { label: 'Before', icon: 'pi pi-a', command: vi.fn() },
+ { id: 'before', label: 'Before', icon: 'pi pi-a', command: vi.fn() },
{ separator: true },
- { label: 'After', icon: 'pi pi-b', command: vi.fn() }
+ { id: 'after', label: 'After', icon: 'pi pi-b', command: vi.fn() }
]
const wrapper = createWrapper(items)
@@ -44,7 +44,7 @@ describe('WorkflowActionsList', () => {
it('dispatches command on select', async () => {
const command = vi.fn()
const items: WorkflowMenuItem[] = [
- { label: 'Action', icon: 'pi pi-play', command }
+ { id: 'action', label: 'Action', icon: 'pi pi-play', command }
]
const wrapper = createWrapper(items)
@@ -57,6 +57,7 @@ describe('WorkflowActionsList', () => {
it('renders badge when present', () => {
const items: WorkflowMenuItem[] = [
{
+ id: 'new-feature',
label: 'New Feature',
icon: 'pi pi-star',
command: vi.fn(),
@@ -71,7 +72,7 @@ describe('WorkflowActionsList', () => {
it('does not render badge when absent', () => {
const items: WorkflowMenuAction[] = [
- { label: 'Plain', icon: 'pi pi-check', command: vi.fn() }
+ { id: 'plain', label: 'Plain', icon: 'pi pi-check', command: vi.fn() }
]
const wrapper = createWrapper(items)
diff --git a/src/components/topbar/WorkflowTab.vue b/src/components/topbar/WorkflowTab.vue
index 6b5ea205ab..ac61dbcf73 100644
--- a/src/components/topbar/WorkflowTab.vue
+++ b/src/components/topbar/WorkflowTab.vue
@@ -198,11 +198,13 @@ const contextMenuItems = computed(() => [
...baseMenuItems.value,
{ separator: true },
{
+ id: 'close-tab',
label: t('tabMenu.closeTab'),
icon: 'pi pi-times',
command: () => onCloseWorkflow(props.workflowOption)
},
{
+ id: 'close-tabs-to-left',
label: t('tabMenu.closeTabsToLeft'),
overlayIcon: {
mainIcon: 'pi pi-times',
@@ -215,6 +217,7 @@ const contextMenuItems = computed(() => [
disabled: props.isFirst
},
{
+ id: 'close-tabs-to-right',
label: t('tabMenu.closeTabsToRight'),
overlayIcon: {
mainIcon: 'pi pi-times',
@@ -227,6 +230,7 @@ const contextMenuItems = computed(() => [
disabled: props.isLast
},
{
+ id: 'close-other-tabs',
label: t('tabMenu.closeOtherTabs'),
overlayIcon: {
mainIcon: 'pi pi-times',
diff --git a/src/composables/useNewMenuItemIndicator.test.ts b/src/composables/useNewMenuItemIndicator.test.ts
new file mode 100644
index 0000000000..9ca8939866
--- /dev/null
+++ b/src/composables/useNewMenuItemIndicator.test.ts
@@ -0,0 +1,111 @@
+import { createPinia, setActivePinia } from 'pinia'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { useNewMenuItemIndicator } from '@/composables/useNewMenuItemIndicator'
+import type { WorkflowMenuItem } from '@/types/workflowMenuItem'
+
+const mockSettingStore = vi.hoisted(() => ({
+ get: vi.fn((): string[] => []),
+ set: vi.fn()
+}))
+
+vi.mock('@/platform/settings/settingStore', () => ({
+ useSettingStore: vi.fn(() => mockSettingStore)
+}))
+
+function createItems(...ids: string[]): WorkflowMenuItem[] {
+ return ids.map((id) => ({
+ id,
+ label: `Label for ${id}`,
+ icon: 'pi pi-test',
+ command: vi.fn(),
+ isNew: true,
+ badge: 'BETA'
+ }))
+}
+
+describe('useNewMenuItemIndicator', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ vi.clearAllMocks()
+ mockSettingStore.get.mockReturnValue([])
+ })
+
+ it('reports unseen items when no items have been seen', () => {
+ const items = createItems('feature-a')
+ const { hasUnseenItems } = useNewMenuItemIndicator(() => items)
+
+ expect(hasUnseenItems.value).toBe(true)
+ })
+
+ it('reports no unseen items when all new items are already seen', () => {
+ mockSettingStore.get.mockReturnValue(['feature-a'])
+ const items = createItems('feature-a')
+ const { hasUnseenItems } = useNewMenuItemIndicator(() => items)
+
+ expect(hasUnseenItems.value).toBe(false)
+ })
+
+ it('reports unseen when some new items are not yet seen', () => {
+ mockSettingStore.get.mockReturnValue(['feature-a'])
+ const items = createItems('feature-a', 'feature-b')
+ const { hasUnseenItems } = useNewMenuItemIndicator(() => items)
+
+ expect(hasUnseenItems.value).toBe(true)
+ })
+
+ it('reports no unseen items when menu has no isNew items', () => {
+ const items: WorkflowMenuItem[] = [
+ { id: 'regular', label: 'Regular', icon: 'pi pi-test', command: vi.fn() }
+ ]
+ const { hasUnseenItems } = useNewMenuItemIndicator(() => items)
+
+ expect(hasUnseenItems.value).toBe(false)
+ })
+
+ it('ignores separators', () => {
+ const items: WorkflowMenuItem[] = [
+ { separator: true },
+ ...createItems('feature-a')
+ ]
+ const { hasUnseenItems } = useNewMenuItemIndicator(() => items)
+
+ expect(hasUnseenItems.value).toBe(true)
+ })
+
+ it('markAsSeen persists current new item ids', () => {
+ const items = createItems('feature-a', 'feature-b')
+ const { markAsSeen } = useNewMenuItemIndicator(() => items)
+
+ markAsSeen()
+
+ expect(mockSettingStore.set).toHaveBeenCalledWith(
+ 'Comfy.WorkflowActions.SeenItems',
+ ['feature-a', 'feature-b']
+ )
+ })
+
+ it('markAsSeen replaces stale entries with current new items', () => {
+ mockSettingStore.get.mockReturnValue(['old-feature', 'feature-a'])
+ const items = createItems('feature-a')
+ const { markAsSeen } = useNewMenuItemIndicator(() => items)
+
+ markAsSeen()
+
+ expect(mockSettingStore.set).toHaveBeenCalledWith(
+ 'Comfy.WorkflowActions.SeenItems',
+ ['feature-a']
+ )
+ })
+
+ it('markAsSeen does nothing when there are no new items', () => {
+ const items: WorkflowMenuItem[] = [
+ { id: 'regular', label: 'Regular', icon: 'pi pi-test', command: vi.fn() }
+ ]
+ const { markAsSeen } = useNewMenuItemIndicator(() => items)
+
+ markAsSeen()
+
+ expect(mockSettingStore.set).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/composables/useNewMenuItemIndicator.ts b/src/composables/useNewMenuItemIndicator.ts
new file mode 100644
index 0000000000..40dafe7347
--- /dev/null
+++ b/src/composables/useNewMenuItemIndicator.ts
@@ -0,0 +1,41 @@
+import type { MaybeRefOrGetter } from 'vue'
+import { computed, toValue } from 'vue'
+
+import { useSettingStore } from '@/platform/settings/settingStore'
+import type {
+ WorkflowMenuAction,
+ WorkflowMenuItem
+} from '@/types/workflowMenuItem'
+
+function getNewItemIds(items: WorkflowMenuItem[]): string[] {
+ return items
+ .filter((i): i is WorkflowMenuAction => !('separator' in i && i.separator))
+ .filter((i) => i.isNew)
+ .map((i) => i.id)
+}
+
+export function useNewMenuItemIndicator(
+ menuItems: MaybeRefOrGetter
+) {
+ const settingStore = useSettingStore()
+
+ const newItemIds = computed(() => getNewItemIds(toValue(menuItems)))
+
+ const seenItems = computed(
+ () => settingStore.get('Comfy.WorkflowActions.SeenItems') ?? []
+ )
+
+ const hasUnseenItems = computed(() => {
+ const seen = new Set(seenItems.value)
+ return newItemIds.value.some((id) => !seen.has(id))
+ })
+
+ function markAsSeen() {
+ if (!newItemIds.value.length) return
+ void settingStore.set('Comfy.WorkflowActions.SeenItems', [
+ ...newItemIds.value
+ ])
+ }
+
+ return { hasUnseenItems, markAsSeen }
+}
diff --git a/src/composables/useWorkflowActionsMenu.ts b/src/composables/useWorkflowActionsMenu.ts
index 1c4d32e0e9..62eb2d5dfa 100644
--- a/src/composables/useWorkflowActionsMenu.ts
+++ b/src/composables/useWorkflowActionsMenu.ts
@@ -28,6 +28,7 @@ interface WorkflowActionsMenuOptions {
}
interface AddItemOptions {
+ id: string
label: string
icon: string
command: () => void
@@ -71,6 +72,7 @@ export function useWorkflowActionsMenu(
const items: WorkflowMenuItem[] = []
const addItem = ({
+ id,
label,
icon,
command,
@@ -81,9 +83,10 @@ export function useWorkflowActionsMenu(
}: AddItemOptions) => {
if (!visible) return
if (prependSeparator) items.push({ separator: true })
- const item: WorkflowMenuAction = { label, icon, command, disabled }
+ const item: WorkflowMenuAction = { id, label, icon, command, disabled }
if (isNew) {
- item.badge = t('contextMenu.new')
+ item.badge = t('g.experimental')
+ item.isNew = true
}
items.push(item)
}
@@ -94,6 +97,7 @@ export function useWorkflowActionsMenu(
const isBookmarked = bookmarkStore.isBookmarked(workflow?.path ?? '')
addItem({
+ id: 'rename',
label: t('g.rename'),
icon: 'pi pi-pencil',
command: async () => {
@@ -104,6 +108,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'duplicate',
label: t('breadcrumbsMenu.duplicate'),
icon: 'pi pi-copy',
command: async () => {
@@ -115,6 +120,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'toggle-bookmark',
label: isBookmarked
? t('tabMenu.removeFromBookmarks')
: t('tabMenu.addToBookmarks'),
@@ -129,6 +135,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'save',
label: t('menuLabels.Save'),
icon: 'pi pi-save',
command: async () => {
@@ -140,6 +147,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'save-as',
label: t('menuLabels.Save As'),
icon: 'pi pi-save',
command: async () => {
@@ -150,6 +158,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'export',
label: t('menuLabels.Export'),
icon: 'pi pi-download',
command: async () => {
@@ -161,6 +170,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'export-api',
label: t('menuLabels.Export (API)'),
icon: 'pi pi-download',
command: async () => {
@@ -171,6 +181,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'toggle-app-mode',
label: isLinearMode
? t('breadcrumbsMenu.exitAppMode')
: t('breadcrumbsMenu.enterAppMode'),
@@ -188,6 +199,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'clear-workflow',
label: t('breadcrumbsMenu.clearWorkflow'),
icon: 'pi pi-trash',
command: async () => {
@@ -198,6 +210,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'publish',
label: t('subgraphStore.publish'),
icon: 'pi pi-upload',
command: async () => {
@@ -210,6 +223,7 @@ export function useWorkflowActionsMenu(
})
addItem({
+ id: 'delete',
label: isBlueprint
? t('breadcrumbsMenu.deleteBlueprint')
: t('breadcrumbsMenu.deleteWorkflow'),
diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts
index 8749ae6005..43a075bd9a 100644
--- a/src/platform/settings/constants/coreSettings.ts
+++ b/src/platform/settings/constants/coreSettings.ts
@@ -856,6 +856,13 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: false,
versionAdded: '1.37.0'
},
+ {
+ id: 'Comfy.WorkflowActions.SeenItems',
+ name: 'Seen new workflow action items',
+ type: 'hidden',
+ defaultValue: [] as string[],
+ versionAdded: '1.41.5'
+ },
{
id: 'Comfy.Execution.PreviewMethod',
category: ['Comfy', 'Execution', 'PreviewMethod'],
diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts
index deca94afdd..2f11ca13ae 100644
--- a/src/schemas/apiSchema.ts
+++ b/src/schemas/apiSchema.ts
@@ -373,6 +373,7 @@ const zSettings = z.object({
'Comfy.QueueButton.BatchCountLimit': z.number(),
'Comfy.Queue.MaxHistoryItems': z.number(),
'Comfy.Queue.History.Expanded': z.boolean(),
+ 'Comfy.WorkflowActions.SeenItems': z.array(z.string()),
'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding),
'Comfy.Keybinding.NewBindings': z.array(zKeybinding),
'Comfy.Extension.Disabled': z.array(z.string()),
diff --git a/src/types/workflowMenuItem.ts b/src/types/workflowMenuItem.ts
index 7908110c7d..ccc62910b0 100644
--- a/src/types/workflowMenuItem.ts
+++ b/src/types/workflowMenuItem.ts
@@ -8,10 +8,12 @@ interface WorkflowMenuSeparator {
export interface WorkflowMenuAction {
separator?: false
+ id: string
label: string
icon?: string
command?: () => void
disabled?: boolean
badge?: string
+ isNew?: boolean
overlayIcon?: OverlayIconProps
}