diff --git a/src/platform/workflow/core/services/workflowService.ts b/src/platform/workflow/core/services/workflowService.ts index 2eba1345b..0cfa979f5 100644 --- a/src/platform/workflow/core/services/workflowService.ts +++ b/src/platform/workflow/core/services/workflowService.ts @@ -215,9 +215,15 @@ export const useWorkflowService = () => { if (workflowStore.openWorkflows.length === 1) { await loadDefaultWorkflow() } - // If this is the active workflow, load the next workflow + // If this is the active workflow, load the most recent workflow from history if (workflowStore.isActive(workflow)) { - await loadNextOpenedWorkflow() + const mostRecentWorkflow = workflowStore.getMostRecentWorkflow() + if (mostRecentWorkflow) { + await openWorkflow(mostRecentWorkflow) + } else { + // Fallback to next workflow if no history + await loadNextOpenedWorkflow() + } } await workflowStore.closeWorkflow(workflow) diff --git a/src/platform/workflow/management/stores/workflowStore.ts b/src/platform/workflow/management/stores/workflowStore.ts index 580bf793d..9037bf532 100644 --- a/src/platform/workflow/management/stores/workflowStore.ts +++ b/src/platform/workflow/management/stores/workflowStore.ts @@ -158,6 +158,7 @@ interface WorkflowStore { isActive: (workflow: ComfyWorkflow) => boolean openWorkflows: ComfyWorkflow[] openedWorkflowIndexShift: (shift: number) => ComfyWorkflow | null + getMostRecentWorkflow: () => ComfyWorkflow | null openWorkflow: (workflow: ComfyWorkflow) => Promise openWorkflowsInBackground: (paths: { left?: string[] @@ -201,6 +202,14 @@ interface WorkflowStore { } export const useWorkflowStore = defineStore('workflow', () => { + /** + * History of tab activations. Most recent at the end. + * Tracks the order in which tabs were activated to support "go to previous" behavior. + * Lazily cleaned on access. + */ + const tabActivationHistory = ref([]) + const MAX_HISTORY_SIZE = 32 + /** * Detach the workflow from the store. lightweight helper function. * @param workflow The workflow to detach. @@ -308,6 +317,18 @@ export const useWorkflowStore = defineStore('workflow', () => { const loadedWorkflow = await workflow.load() activeWorkflow.value = loadedWorkflow comfyApp.canvas.bg_tint = loadedWorkflow.tintCanvasBg + + // Track activation in history (move to end if already present) + const historyIndex = tabActivationHistory.value.indexOf(workflow.path) + if (historyIndex !== -1) { + tabActivationHistory.value.splice(historyIndex, 1) + } + tabActivationHistory.value.push(workflow.path) + // Trim history if too large + if (tabActivationHistory.value.length > MAX_HISTORY_SIZE) { + tabActivationHistory.value.shift() + } + return loadedWorkflow } @@ -405,6 +426,39 @@ export const useWorkflowStore = defineStore('workflow', () => { return null } + /** + * Get the most recently active workflow from history (excluding current). + * Lazily cleans invalid paths from history. + * @returns The most recent valid workflow or null if none found. + */ + const getMostRecentWorkflow = (): ComfyWorkflow | null => { + const currentPath = activeWorkflow.value?.path + const validPaths: string[] = [] + + // Scan backwards through history + for (let i = tabActivationHistory.value.length - 1; i >= 0; i--) { + const path = tabActivationHistory.value[i] + + // Skip current workflow + if (path === currentPath) continue + + // Check if workflow is still open + if (openWorkflowPathSet.value.has(path)) { + validPaths.unshift(path) + const workflow = workflowLookup.value[path] + if (workflow) { + // Lazy cleanup: keep only valid paths + tabActivationHistory.value = validPaths + return workflow + } + } + } + + // Cleanup: no valid workflows found, clear history + tabActivationHistory.value = [] + return null + } + const persistedWorkflows = computed(() => Array.from(workflows.value).filter( (workflow) => @@ -714,6 +768,7 @@ export const useWorkflowStore = defineStore('workflow', () => { isActive, openWorkflows, openedWorkflowIndexShift, + getMostRecentWorkflow, openWorkflow, openWorkflowsInBackground, isOpen, diff --git a/tests-ui/tests/store/workflowStore.test.ts b/tests-ui/tests/store/workflowStore.test.ts index 648d00fa8..6e82559fe 100644 --- a/tests-ui/tests/store/workflowStore.test.ts +++ b/tests-ui/tests/store/workflowStore.test.ts @@ -3,9 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import type { Subgraph } from '@/lib/litegraph/src/litegraph' -import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' +import type { + ComfyWorkflow, + LoadedComfyWorkflow +} from '@/platform/workflow/management/stores/workflowStore' import { - type LoadedComfyWorkflow, useWorkflowBookmarkStore, useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' @@ -723,4 +725,93 @@ describe('useWorkflowStore', () => { }) }) }) + + describe('Tab Activation History', () => { + let workflowA: ComfyWorkflow + let workflowB: ComfyWorkflow + let workflowC: ComfyWorkflow + + beforeEach(async () => { + await syncRemoteWorkflows(['a.json', 'b.json', 'c.json']) + workflowA = store.getWorkflowByPath('workflows/a.json')! + workflowB = store.getWorkflowByPath('workflows/b.json')! + workflowC = store.getWorkflowByPath('workflows/c.json')! + vi.mocked(api.getUserData).mockResolvedValue({ + status: 200, + text: () => Promise.resolve(defaultGraphJSON) + } as Response) + }) + + it('should return most recently active workflow', async () => { + // Open workflows in order: A -> B -> C + await store.openWorkflow(workflowA) + await store.openWorkflow(workflowB) + await store.openWorkflow(workflowC) + + // C is current, B should be most recent + const mostRecent = store.getMostRecentWorkflow() + expect(mostRecent?.path).toBe(workflowB.path) + }) + + it('should skip closed workflows (lazy cleanup)', async () => { + // Open workflows: A -> B -> C + await store.openWorkflow(workflowA) + await store.openWorkflow(workflowB) + await store.openWorkflow(workflowC) + + // Close B (the most recent before C) + await store.closeWorkflow(workflowB) + + // C is current, B is closed, so A should be returned + const mostRecent = store.getMostRecentWorkflow() + expect(mostRecent?.path).toBe(workflowA.path) + }) + + it('should return null when no valid history exists', async () => { + // Open only one workflow + await store.openWorkflow(workflowA) + + // No previous workflows, should return null + const mostRecent = store.getMostRecentWorkflow() + expect(mostRecent).toBeNull() + }) + + it('should track history when opening workflows', async () => { + // Open A, then B, then A again + await store.openWorkflow(workflowA) + await store.openWorkflow(workflowB) + await store.openWorkflow(workflowA) + + // A is current, B should be most recent + const mostRecent = store.getMostRecentWorkflow() + expect(mostRecent?.path).toBe(workflowB.path) + }) + + it('should handle workflow activated multiple times', async () => { + // Open: A -> B -> A -> C + await store.openWorkflow(workflowA) + await store.openWorkflow(workflowB) + await store.openWorkflow(workflowA) + await store.openWorkflow(workflowC) + + // C is current, A should be most recent (not B) + const mostRecent = store.getMostRecentWorkflow() + expect(mostRecent?.path).toBe(workflowA.path) + }) + + it('should clean up history when all previous workflows are closed', async () => { + // Open: A -> B -> C + await store.openWorkflow(workflowA) + await store.openWorkflow(workflowB) + await store.openWorkflow(workflowC) + + // Close A and B + await store.closeWorkflow(workflowA) + await store.closeWorkflow(workflowB) + + // C is current, no valid history + const mostRecent = store.getMostRecentWorkflow() + expect(mostRecent).toBeNull() + }) + }) })