mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 00:04:06 +00:00
feat: add KSampler live previews to assets sidebar jobs (#8723)
## Summary Show live KSampler previews on active job cards/list items in the Assets sidebar, while preserving existing fallback behavior. ## Changes - **What**: - Added a prompt-scoped job preview store (`jobPreviewStore`) gated by `Comfy.Execution.PreviewMethod`. - Wired `b_preview_with_metadata` handling to map previews by `promptId`. - Extended queue job view model with `livePreviewUrl` and consumed it in both sidebar list and grid active job UIs. - Cleared prompt previews on execution reset. - Added ref-counted shared blob URL lifecycle utility (`objectUrlUtil`) and updated preview stores to retain/release shared URLs so each preview event creates one object URL. - Added/updated unit coverage in `useJobList.test.ts` for preview enable/disable mapping. ## Review Focus - Object URL lifecycle correctness across node previews and job previews (retain/release behavior). - Preview gating behavior when `Comfy.Execution.PreviewMethod` is `none`. - Active job UI fallback behavior (`livePreviewUrl` -> `iconImageUrl`). ## Screenshots (if applicable) <img width="808" height="614" alt="image" src="https://github.com/user-attachments/assets/37c66eb2-8c28-4eb4-bb86-5679cb77d740" /> <img width="775" height="345" alt="image" src="https://github.com/user-attachments/assets/aa420642-b0d4-4ae6-b94a-e7934b5df9d6" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8723-feat-add-KSampler-live-previews-to-assets-sidebar-jobs-3006d73d365081aeb81dd8279bf99f94) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -134,6 +134,25 @@ vi.mock('@/stores/executionStore', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
let jobPreviewStoreMock: {
|
||||
previewsByPromptId: Record<string, string>
|
||||
isPreviewEnabled: boolean
|
||||
}
|
||||
const ensureJobPreviewStore = () => {
|
||||
if (!jobPreviewStoreMock) {
|
||||
jobPreviewStoreMock = reactive({
|
||||
previewsByPromptId: {} as Record<string, string>,
|
||||
isPreviewEnabled: true
|
||||
})
|
||||
}
|
||||
return jobPreviewStoreMock
|
||||
}
|
||||
vi.mock('@/stores/jobPreviewStore', () => ({
|
||||
useJobPreviewStore: () => {
|
||||
return ensureJobPreviewStore()
|
||||
}
|
||||
}))
|
||||
|
||||
let workflowStoreMock: {
|
||||
activeWorkflow: null | { activeState?: { id?: string } }
|
||||
}
|
||||
@@ -186,6 +205,10 @@ const resetStores = () => {
|
||||
executionStore.activePromptId = null
|
||||
executionStore.executingNode = null
|
||||
|
||||
const jobPreviewStore = ensureJobPreviewStore()
|
||||
jobPreviewStore.previewsByPromptId = {}
|
||||
jobPreviewStore.isPreviewEnabled = true
|
||||
|
||||
const workflowStore = ensureWorkflowStore()
|
||||
workflowStore.activeWorkflow = null
|
||||
|
||||
@@ -437,6 +460,44 @@ describe('useJobList', () => {
|
||||
expect(otherJob.computeHours).toBeCloseTo(1)
|
||||
})
|
||||
|
||||
it('assigns preview urls for running jobs when previews enabled', async () => {
|
||||
queueStoreMock.runningTasks = [
|
||||
createTask({
|
||||
promptId: 'live-preview',
|
||||
queueIndex: 1,
|
||||
mockState: 'running'
|
||||
})
|
||||
]
|
||||
jobPreviewStoreMock.previewsByPromptId = {
|
||||
'live-preview': 'blob:preview-url'
|
||||
}
|
||||
jobPreviewStoreMock.isPreviewEnabled = true
|
||||
|
||||
const { jobItems } = initComposable()
|
||||
await flush()
|
||||
|
||||
expect(jobItems.value[0].iconImageUrl).toBe('blob:preview-url')
|
||||
})
|
||||
|
||||
it('omits preview urls when previews are disabled', async () => {
|
||||
queueStoreMock.runningTasks = [
|
||||
createTask({
|
||||
promptId: 'disabled-preview',
|
||||
queueIndex: 1,
|
||||
mockState: 'running'
|
||||
})
|
||||
]
|
||||
jobPreviewStoreMock.previewsByPromptId = {
|
||||
'disabled-preview': 'blob:preview-url'
|
||||
}
|
||||
jobPreviewStoreMock.isPreviewEnabled = false
|
||||
|
||||
const { jobItems } = initComposable()
|
||||
await flush()
|
||||
|
||||
expect(jobItems.value[0].iconImageUrl).toBeUndefined()
|
||||
})
|
||||
|
||||
it('derives current node name from execution store fallbacks', async () => {
|
||||
const instance = initComposable()
|
||||
await flush()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { st } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobState } from '@/types/queue'
|
||||
@@ -96,6 +97,7 @@ export function useJobList() {
|
||||
const { t, locale } = useI18n()
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const jobPreviewStore = useJobPreviewStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const seenPendingIds = ref<Set<string>>(new Set())
|
||||
@@ -256,6 +258,11 @@ export function useJobList() {
|
||||
String(task.promptId ?? '') ===
|
||||
String(executionStore.activePromptId ?? '')
|
||||
const showAddedHint = shouldShowAddedHint(task, state)
|
||||
const promptKey = taskIdToKey(task.promptId)
|
||||
const promptPreviewUrl =
|
||||
state === 'running' && jobPreviewStore.isPreviewEnabled && promptKey
|
||||
? jobPreviewStore.previewsByPromptId[promptKey]
|
||||
: undefined
|
||||
|
||||
const display = buildJobDisplay(task, state, {
|
||||
t,
|
||||
@@ -275,7 +282,7 @@ export function useJobList() {
|
||||
meta: display.secondary,
|
||||
state,
|
||||
iconName: display.iconName,
|
||||
iconImageUrl: display.iconImageUrl,
|
||||
iconImageUrl: promptPreviewUrl ?? display.iconImageUrl,
|
||||
showClear: display.showClear,
|
||||
taskRef: task,
|
||||
progressTotalPercent:
|
||||
|
||||
Reference in New Issue
Block a user