refactor: rename internal promptId/PromptId to jobId/JobId (#8730)

## Summary

Rename all internal TypeScript usage of legacy `promptId`/`PromptId`
naming to `jobId`/`JobId` across ~38 files for consistency with the
domain model.

## Changes

- **What**: Renamed internal variable names, type aliases, function
names, class getters, interface fields, and comments from
`promptId`/`PromptId` to `jobId`/`JobId`. Wire-protocol field names
(`prompt_id` in Zod schemas and `e.detail.prompt_id` accesses) are
intentionally preserved since they match the backend API contract.

## Review Focus

- All changes are pure renames with no behavioral changes
- Wire-protocol fields (`prompt_id`) are deliberately unchanged to
maintain backend compatibility
- Test fixtures updated to use consistent `job-id` naming

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8730-refactor-rename-internal-promptId-PromptId-to-jobId-JobId-3016d73d3650813ca40ce337f7c5271a)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2026-02-20 02:10:53 -08:00
committed by GitHub
parent 541ad387b9
commit 473713cf02
39 changed files with 455 additions and 402 deletions

View File

@@ -339,7 +339,7 @@ describe('TopMenuSection', () => {
const pinia = createTestingPinia({ createSpy: vi.fn }) const pinia = createTestingPinia({ createSpy: vi.fn })
configureSettings(pinia, true) configureSettings(pinia, true)
const executionStore = useExecutionStore(pinia) const executionStore = useExecutionStore(pinia)
executionStore.activePromptId = 'prompt-1' executionStore.activeJobId = 'job-1'
const ComfyActionbarStub = createComfyActionbarStub(actionbarTarget) const ComfyActionbarStub = createComfyActionbarStub(actionbarTarget)
@@ -429,7 +429,7 @@ describe('TopMenuSection', () => {
const pinia = createTestingPinia({ createSpy: vi.fn }) const pinia = createTestingPinia({ createSpy: vi.fn })
configureSettings(pinia, true) configureSettings(pinia, true)
const executionStore = useExecutionStore(pinia) const executionStore = useExecutionStore(pinia)
executionStore.activePromptId = 'prompt-1' executionStore.activeJobId = 'job-1'
const ComfyActionbarStub = createComfyActionbarStub(actionbarTarget) const ComfyActionbarStub = createComfyActionbarStub(actionbarTarget)

View File

@@ -290,12 +290,12 @@ const showQueueContextMenu = (event: MouseEvent) => {
} }
const handleClearQueue = async () => { const handleClearQueue = async () => {
const pendingPromptIds = queueStore.pendingTasks const pendingJobIds = queueStore.pendingTasks
.map((task) => task.promptId) .map((task) => task.jobId)
.filter((id): id is string => typeof id === 'string' && id.length > 0) .filter((id): id is string => typeof id === 'string' && id.length > 0)
await commandStore.execute('Comfy.ClearPendingTasks') await commandStore.execute('Comfy.ClearPendingTasks')
executionStore.clearInitializationByPromptIds(pendingPromptIds) executionStore.clearInitializationByJobIds(pendingJobIds)
} }
const openCustomNodeManager = async () => { const openCustomNodeManager = async () => {

View File

@@ -204,22 +204,22 @@ const {
const displayedJobGroups = computed(() => groupedJobItems.value) const displayedJobGroups = computed(() => groupedJobItems.value)
const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => { const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {
const promptId = item.taskRef?.promptId const jobId = item.taskRef?.jobId
if (!promptId) return if (!jobId) return
if (item.state === 'running' || item.state === 'initialization') { if (item.state === 'running' || item.state === 'initialization') {
// Running/initializing jobs: interrupt execution // Running/initializing jobs: interrupt execution
// Cloud backend uses deleteItem, local uses interrupt // Cloud backend uses deleteItem, local uses interrupt
if (isCloud) { if (isCloud) {
await api.deleteItem('queue', promptId) await api.deleteItem('queue', jobId)
} else { } else {
await api.interrupt(promptId) await api.interrupt(jobId)
} }
executionStore.clearInitializationByPromptId(promptId) executionStore.clearInitializationByJobId(jobId)
await queueStore.update() await queueStore.update()
} else if (item.state === 'pending') { } else if (item.state === 'pending') {
// Pending jobs: remove from queue // Pending jobs: remove from queue
await api.deleteItem('queue', promptId) await api.deleteItem('queue', jobId)
await queueStore.update() await queueStore.update()
} }
}) })
@@ -249,11 +249,11 @@ const openAssetsSidebar = () => {
const focusAssetInSidebar = async (item: JobListItem) => { const focusAssetInSidebar = async (item: JobListItem) => {
const task = item.taskRef const task = item.taskRef
const promptId = task?.promptId const jobId = task?.jobId
const preview = task?.previewOutput const preview = task?.previewOutput
if (!promptId || !preview) return if (!jobId || !preview) return
const assetId = String(promptId) const assetId = String(jobId)
openAssetsSidebar() openAssetsSidebar()
await nextTick() await nextTick()
await assetsStore.updateHistory() await assetsStore.updateHistory()
@@ -275,37 +275,37 @@ const inspectJobAsset = wrapWithErrorHandlingAsync(
) )
const cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => { const cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => {
// Capture pending promptIds before clearing // Capture pending jobIds before clearing
const pendingPromptIds = queueStore.pendingTasks const pendingJobIds = queueStore.pendingTasks
.map((task) => task.promptId) .map((task) => task.jobId)
.filter((id): id is string => typeof id === 'string' && id.length > 0) .filter((id): id is string => typeof id === 'string' && id.length > 0)
await commandStore.execute('Comfy.ClearPendingTasks') await commandStore.execute('Comfy.ClearPendingTasks')
// Clear initialization state for removed prompts // Clear initialization state for removed jobs
executionStore.clearInitializationByPromptIds(pendingPromptIds) executionStore.clearInitializationByJobIds(pendingJobIds)
}) })
const interruptAll = wrapWithErrorHandlingAsync(async () => { const interruptAll = wrapWithErrorHandlingAsync(async () => {
const tasks = queueStore.runningTasks const tasks = queueStore.runningTasks
const promptIds = tasks const jobIds = tasks
.map((task) => task.promptId) .map((task) => task.jobId)
.filter((id): id is string => typeof id === 'string' && id.length > 0) .filter((id): id is string => typeof id === 'string' && id.length > 0)
if (!promptIds.length) return if (!jobIds.length) return
// Cloud backend supports cancelling specific jobs via /queue delete, // Cloud backend supports cancelling specific jobs via /queue delete,
// while /interrupt always targets the "first" job. Use the targeted API // while /interrupt always targets the "first" job. Use the targeted API
// on cloud to ensure we cancel the workflow the user clicked. // on cloud to ensure we cancel the workflow the user clicked.
if (isCloud) { if (isCloud) {
await Promise.all(promptIds.map((id) => api.deleteItem('queue', id))) await Promise.all(jobIds.map((id) => api.deleteItem('queue', id)))
executionStore.clearInitializationByPromptIds(promptIds) executionStore.clearInitializationByJobIds(jobIds)
await queueStore.update() await queueStore.update()
return return
} }
await Promise.all(promptIds.map((id) => api.interrupt(id))) await Promise.all(jobIds.map((id) => api.interrupt(id)))
executionStore.clearInitializationByPromptIds(promptIds) executionStore.clearInitializationByJobIds(jobIds)
await queueStore.update() await queueStore.update()
}) })

View File

@@ -37,7 +37,7 @@ function resetStores() {
queue.runningTasks = [] queue.runningTasks = []
queue.historyTasks = [] queue.historyTasks = []
exec.nodeProgressStatesByPrompt = {} exec.nodeProgressStatesByJob = {}
} }
function makeTask( function makeTask(
@@ -145,10 +145,10 @@ export const Queued: Story = {
makePendingTask('job-older-2', 101, Date.now() - 30_000) makePendingTask('job-older-2', 101, Date.now() - 30_000)
) )
// Queued at (in metadata on prompt[4]) // Queued at (in metadata on job tuple)
// One running workflow // One running workflow
exec.nodeProgressStatesByPrompt = { exec.nodeProgressStatesByJob = {
p1: { p1: {
'1': { '1': {
value: 1, value: 1,
@@ -198,7 +198,7 @@ export const QueuedParallel: Story = {
] ]
// Two parallel workflows running // Two parallel workflows running
exec.nodeProgressStatesByPrompt = { exec.nodeProgressStatesByJob = {
p1: { p1: {
'1': { '1': {
value: 1, value: 1,
@@ -248,7 +248,7 @@ export const Running: Story = {
makeHistoryTask('hist-r3', 252, 60, true) makeHistoryTask('hist-r3', 252, 60, true)
] ]
exec.nodeProgressStatesByPrompt = { exec.nodeProgressStatesByJob = {
p1: { p1: {
'1': { '1': {
value: 5, value: 5,
@@ -293,7 +293,7 @@ export const QueuedZeroAheadSingleRunning: Story = {
queue.runningTasks = [makeRunningTaskWithStart('running-1', 505, 20)] queue.runningTasks = [makeRunningTaskWithStart('running-1', 505, 20)]
exec.nodeProgressStatesByPrompt = { exec.nodeProgressStatesByJob = {
p1: { p1: {
'1': { '1': {
value: 1, value: 1,
@@ -341,7 +341,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
makeRunningTaskWithStart('running-b', 507, 10) makeRunningTaskWithStart('running-b', 507, 10)
] ]
exec.nodeProgressStatesByPrompt = { exec.nodeProgressStatesByJob = {
p1: { p1: {
'1': { '1': {
value: 2, value: 2,

View File

@@ -139,7 +139,7 @@ const copyJobId = () => void copyToClipboard(jobIdValue.value)
const taskForJob = computed(() => { const taskForJob = computed(() => {
const pid = props.jobId const pid = props.jobId
const findIn = (arr: TaskItemImpl[]) => const findIn = (arr: TaskItemImpl[]) =>
arr.find((t) => String(t.promptId ?? '') === String(pid)) arr.find((t) => String(t.jobId ?? '') === String(pid))
return ( return (
findIn(queueStore.pendingTasks) || findIn(queueStore.pendingTasks) ||
findIn(queueStore.runningTasks) || findIn(queueStore.runningTasks) ||
@@ -151,9 +151,7 @@ const taskForJob = computed(() => {
const jobState = computed(() => { const jobState = computed(() => {
const task = taskForJob.value const task = taskForJob.value
if (!task) return null if (!task) return null
const isInitializing = executionStore.isPromptInitializing( const isInitializing = executionStore.isJobInitializing(String(task?.jobId))
String(task?.promptId)
)
return jobStateFromTask(task, isInitializing) return jobStateFromTask(task, isInitializing)
}) })

View File

@@ -8,13 +8,13 @@ import { useJobErrorReporting } from '@/components/queue/job/useJobErrorReportin
import type { ExecutionError } from '@/platform/remote/comfyui/jobs/jobTypes' import type { ExecutionError } from '@/platform/remote/comfyui/jobs/jobTypes'
const createTaskWithError = ( const createTaskWithError = (
promptId: string, jobId: string,
errorMessage?: string, errorMessage?: string,
executionError?: ExecutionError, executionError?: ExecutionError,
createTime?: number createTime?: number
): TaskItemImpl => ): TaskItemImpl =>
({ ({
promptId, jobId,
errorMessage, errorMessage,
executionError, executionError,
createTime: createTime ?? Date.now() createTime: createTime ?? Date.now()

View File

@@ -80,7 +80,7 @@ const sampleAssets: AssetItem[] = [
size: 1887437, size: 1887437,
tags: [], tags: [],
user_metadata: { user_metadata: {
promptId: 'job-running-1', jobId: 'job-running-1',
nodeId: 12, nodeId: 12,
executionTimeInSeconds: 1.84 executionTimeInSeconds: 1.84
} }

View File

@@ -9,7 +9,7 @@
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="font-bold">{{ $t('assetBrowser.jobId') }}:</span> <span class="font-bold">{{ $t('assetBrowser.jobId') }}:</span>
<span class="text-sm">{{ folderPromptId?.substring(0, 8) }}</span> <span class="text-sm">{{ folderJobId?.substring(0, 8) }}</span>
<button <button
class="m-0 cursor-pointer border-0 bg-transparent p-0 outline-0" class="m-0 cursor-pointer border-0 bg-transparent p-0 outline-0"
role="button" role="button"
@@ -273,10 +273,10 @@ const executionStore = useExecutionStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const activeTab = ref<'input' | 'output'>('output') const activeTab = ref<'input' | 'output'>('output')
const folderPromptId = ref<string | null>(null) const folderJobId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined) const folderExecutionTime = ref<number | undefined>(undefined)
const expectedFolderCount = ref(0) const expectedFolderCount = ref(0)
const isInFolderView = computed(() => folderPromptId.value !== null) const isInFolderView = computed(() => folderJobId.value !== null)
const viewMode = useStorage<'list' | 'grid'>( const viewMode = useStorage<'list' | 'grid'>(
'Comfy.Assets.Sidebar.ViewMode', 'Comfy.Assets.Sidebar.ViewMode',
'grid' 'grid'
@@ -559,13 +559,13 @@ const handleBulkDelete = async (assets: AssetItem[]) => {
} }
const handleClearQueue = async () => { const handleClearQueue = async () => {
const pendingPromptIds = queueStore.pendingTasks const pendingJobIds = queueStore.pendingTasks
.map((task) => task.promptId) .map((task) => task.jobId)
.filter((id): id is string => typeof id === 'string' && id.length > 0) .filter((id): id is string => typeof id === 'string' && id.length > 0)
await commandStore.execute('Comfy.ClearPendingTasks') await commandStore.execute('Comfy.ClearPendingTasks')
executionStore.clearInitializationByPromptIds(pendingPromptIds) executionStore.clearInitializationByJobIds(pendingJobIds)
} }
const handleBulkAddToWorkflow = async (assets: AssetItem[]) => { const handleBulkAddToWorkflow = async (assets: AssetItem[]) => {
@@ -628,14 +628,14 @@ const enterFolderView = async (asset: AssetItem) => {
return return
} }
const { promptId, executionTimeInSeconds } = metadata const { jobId, executionTimeInSeconds } = metadata
if (!promptId) { if (!jobId) {
console.warn('Missing required folder view data') console.warn('Missing required folder view data')
return return
} }
folderPromptId.value = promptId folderJobId.value = jobId
folderExecutionTime.value = executionTimeInSeconds folderExecutionTime.value = executionTimeInSeconds
expectedFolderCount.value = metadata.outputCount ?? 0 expectedFolderCount.value = metadata.outputCount ?? 0
@@ -653,7 +653,7 @@ const enterFolderView = async (asset: AssetItem) => {
} }
const exitFolderView = () => { const exitFolderView = () => {
folderPromptId.value = null folderJobId.value = null
folderExecutionTime.value = undefined folderExecutionTime.value = undefined
expectedFolderCount.value = 0 expectedFolderCount.value = 0
folderAssets.value = [] folderAssets.value = []
@@ -679,9 +679,9 @@ const handleEmptySpaceClick = () => {
} }
const copyJobId = async () => { const copyJobId = async () => {
if (folderPromptId.value) { if (folderJobId.value) {
try { try {
await navigator.clipboard.writeText(folderPromptId.value) await navigator.clipboard.writeText(folderJobId.value)
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: t('mediaAsset.jobIdToast.copied'), summary: t('mediaAsset.jobIdToast.copied'),

View File

@@ -6,10 +6,11 @@ import type { Ref } from 'vue'
import { useJobList } from '@/composables/queue/useJobList' import { useJobList } from '@/composables/queue/useJobList'
import type { JobState } from '@/types/queue' import type { JobState } from '@/types/queue'
import type { BuildJobDisplayCtx } from '@/utils/queueDisplay' import type { BuildJobDisplayCtx } from '@/utils/queueDisplay'
import { buildJobDisplay } from '@/utils/queueDisplay'
import type { TaskItemImpl } from '@/stores/queueStore' import type { TaskItemImpl } from '@/stores/queueStore'
type TestTask = { type TestTask = {
promptId: string jobId: string
queueIndex: number queueIndex: number
mockState: JobState mockState: JobState
executionTime?: number executionTime?: number
@@ -69,7 +70,7 @@ vi.mock('@/composables/queue/useQueueProgress', () => ({
vi.mock('@/utils/queueDisplay', () => ({ vi.mock('@/utils/queueDisplay', () => ({
buildJobDisplay: vi.fn( buildJobDisplay: vi.fn(
(task: TaskItemImpl, state: JobState, options: BuildJobDisplayCtx) => ({ (task: TaskItemImpl, state: JobState, options: BuildJobDisplayCtx) => ({
primary: `Job ${task.promptId}`, primary: `Job ${task.jobId}`,
secondary: `${state} meta`, secondary: `${state} meta`,
iconName: `${state}-icon`, iconName: `${state}-icon`,
iconImageUrl: undefined, iconImageUrl: undefined,
@@ -108,21 +109,21 @@ vi.mock('@/stores/queueStore', () => ({
})) }))
let executionStoreMock: { let executionStoreMock: {
activePromptId: string | null activeJobId: string | null
executingNode: null | { title?: string; type?: string } executingNode: null | { title?: string; type?: string }
isPromptInitializing: (promptId?: string | number) => boolean isJobInitializing: (jobId?: string | number) => boolean
} }
let isPromptInitializingMock: (promptId?: string | number) => boolean let isJobInitializingMock: (jobId?: string | number) => boolean
const ensureExecutionStore = () => { const ensureExecutionStore = () => {
if (!isPromptInitializingMock) { if (!isJobInitializingMock) {
isPromptInitializingMock = vi.fn(() => false) isJobInitializingMock = vi.fn(() => false)
} }
if (!executionStoreMock) { if (!executionStoreMock) {
executionStoreMock = reactive({ executionStoreMock = reactive({
activePromptId: null as string | null, activeJobId: null as string | null,
executingNode: null as null | { title?: string; type?: string }, executingNode: null as null | { title?: string; type?: string },
isPromptInitializing: (promptId?: string | number) => isJobInitializing: (jobId?: string | number) =>
isPromptInitializingMock(promptId) isJobInitializingMock(jobId)
}) })
} }
return executionStoreMock return executionStoreMock
@@ -172,8 +173,7 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
const createTask = ( const createTask = (
overrides: Partial<TestTask> & { mockState?: JobState } = {} overrides: Partial<TestTask> & { mockState?: JobState } = {}
): TestTask => ({ ): TestTask => ({
promptId: jobId: overrides.jobId ?? `task-${Math.random().toString(36).slice(2, 7)}`,
overrides.promptId ?? `task-${Math.random().toString(36).slice(2, 7)}`,
queueIndex: overrides.queueIndex ?? 0, queueIndex: overrides.queueIndex ?? 0,
mockState: overrides.mockState ?? 'pending', mockState: overrides.mockState ?? 'pending',
executionTime: overrides.executionTime, executionTime: overrides.executionTime,
@@ -201,7 +201,7 @@ const resetStores = () => {
queueStore.historyTasks = [] queueStore.historyTasks = []
const executionStore = ensureExecutionStore() const executionStore = ensureExecutionStore()
executionStore.activePromptId = null executionStore.activeJobId = null
executionStore.executingNode = null executionStore.executingNode = null
const jobPreviewStore = ensureJobPreviewStore() const jobPreviewStore = ensureJobPreviewStore()
@@ -219,9 +219,9 @@ const resetStores = () => {
localeRef.value = 'en-US' localeRef.value = 'en-US'
tMock.mockClear() tMock.mockClear()
if (isPromptInitializingMock) { if (isJobInitializingMock) {
vi.mocked(isPromptInitializingMock).mockReset() vi.mocked(isJobInitializingMock).mockReset()
vi.mocked(isPromptInitializingMock).mockReturnValue(false) vi.mocked(isJobInitializingMock).mockReturnValue(false)
} }
} }
@@ -255,10 +255,82 @@ describe('useJobList', () => {
return api! return api!
} }
it('tracks recently added pending jobs and clears the hint after expiry', async () => {
vi.useFakeTimers()
queueStoreMock.pendingTasks = [
createTask({ jobId: '1', queueIndex: 1, mockState: 'pending' })
]
const { jobItems } = initComposable()
await flush()
jobItems.value
expect(buildJobDisplay).toHaveBeenCalledWith(
expect.anything(),
'pending',
expect.objectContaining({ showAddedHint: true })
)
vi.mocked(buildJobDisplay).mockClear()
await vi.advanceTimersByTimeAsync(3000)
await flush()
jobItems.value
expect(buildJobDisplay).toHaveBeenCalledWith(
expect.anything(),
'pending',
expect.objectContaining({ showAddedHint: false })
)
})
it('removes pending hint immediately when the task leaves the queue', async () => {
vi.useFakeTimers()
const taskId = '2'
queueStoreMock.pendingTasks = [
createTask({ jobId: taskId, queueIndex: 1, mockState: 'pending' })
]
const { jobItems } = initComposable()
await flush()
jobItems.value
queueStoreMock.pendingTasks = []
await flush()
expect(vi.getTimerCount()).toBe(0)
vi.mocked(buildJobDisplay).mockClear()
queueStoreMock.pendingTasks = [
createTask({ jobId: taskId, queueIndex: 2, mockState: 'pending' })
]
await flush()
jobItems.value
expect(buildJobDisplay).toHaveBeenCalledWith(
expect.anything(),
'pending',
expect.objectContaining({ showAddedHint: true })
)
})
it('cleans up timeouts on unmount', async () => {
vi.useFakeTimers()
queueStoreMock.pendingTasks = [
createTask({ jobId: '3', queueIndex: 1, mockState: 'pending' })
]
initComposable()
await flush()
expect(vi.getTimerCount()).toBeGreaterThan(0)
wrapper?.unmount()
wrapper = null
await flush()
expect(vi.getTimerCount()).toBe(0)
})
it('sorts all tasks by create time', async () => { it('sorts all tasks by create time', async () => {
queueStoreMock.pendingTasks = [ queueStoreMock.pendingTasks = [
createTask({ createTask({
promptId: 'p', jobId: 'p',
queueIndex: 1, queueIndex: 1,
mockState: 'pending', mockState: 'pending',
createTime: 3000 createTime: 3000
@@ -266,7 +338,7 @@ describe('useJobList', () => {
] ]
queueStoreMock.runningTasks = [ queueStoreMock.runningTasks = [
createTask({ createTask({
promptId: 'r', jobId: 'r',
queueIndex: 5, queueIndex: 5,
mockState: 'running', mockState: 'running',
createTime: 2000 createTime: 2000
@@ -274,7 +346,7 @@ describe('useJobList', () => {
] ]
queueStoreMock.historyTasks = [ queueStoreMock.historyTasks = [
createTask({ createTask({
promptId: 'h', jobId: 'h',
queueIndex: 3, queueIndex: 3,
mockState: 'completed', mockState: 'completed',
createTime: 1000, createTime: 1000,
@@ -285,7 +357,7 @@ describe('useJobList', () => {
const { allTasksSorted } = initComposable() const { allTasksSorted } = initComposable()
await flush() await flush()
expect(allTasksSorted.value.map((task) => task.promptId)).toEqual([ expect(allTasksSorted.value.map((task) => task.jobId)).toEqual([
'p', 'p',
'r', 'r',
'h' 'h'
@@ -294,9 +366,9 @@ describe('useJobList', () => {
it('filters by job tab and resets failed tab when failures disappear', async () => { it('filters by job tab and resets failed tab when failures disappear', async () => {
queueStoreMock.historyTasks = [ queueStoreMock.historyTasks = [
createTask({ promptId: 'c', queueIndex: 3, mockState: 'completed' }), createTask({ jobId: 'c', queueIndex: 3, mockState: 'completed' }),
createTask({ promptId: 'f', queueIndex: 2, mockState: 'failed' }), createTask({ jobId: 'f', queueIndex: 2, mockState: 'failed' }),
createTask({ promptId: 'p', queueIndex: 1, mockState: 'pending' }) createTask({ jobId: 'p', queueIndex: 1, mockState: 'pending' })
] ]
const instance = initComposable() const instance = initComposable()
@@ -304,15 +376,15 @@ describe('useJobList', () => {
instance.selectedJobTab.value = 'Completed' instance.selectedJobTab.value = 'Completed'
await flush() await flush()
expect(instance.filteredTasks.value.map((t) => t.promptId)).toEqual(['c']) expect(instance.filteredTasks.value.map((t) => t.jobId)).toEqual(['c'])
instance.selectedJobTab.value = 'Failed' instance.selectedJobTab.value = 'Failed'
await flush() await flush()
expect(instance.filteredTasks.value.map((t) => t.promptId)).toEqual(['f']) expect(instance.filteredTasks.value.map((t) => t.jobId)).toEqual(['f'])
expect(instance.hasFailedJobs.value).toBe(true) expect(instance.hasFailedJobs.value).toBe(true)
queueStoreMock.historyTasks = [ queueStoreMock.historyTasks = [
createTask({ promptId: 'c', queueIndex: 3, mockState: 'completed' }) createTask({ jobId: 'c', queueIndex: 3, mockState: 'completed' })
] ]
await flush() await flush()
@@ -323,13 +395,13 @@ describe('useJobList', () => {
it('filters by active workflow when requested', async () => { it('filters by active workflow when requested', async () => {
queueStoreMock.pendingTasks = [ queueStoreMock.pendingTasks = [
createTask({ createTask({
promptId: 'wf-1', jobId: 'wf-1',
queueIndex: 2, queueIndex: 2,
mockState: 'pending', mockState: 'pending',
workflowId: 'workflow-1' workflowId: 'workflow-1'
}), }),
createTask({ createTask({
promptId: 'wf-2', jobId: 'wf-2',
queueIndex: 1, queueIndex: 1,
mockState: 'pending', mockState: 'pending',
workflowId: 'workflow-2' workflowId: 'workflow-2'
@@ -346,28 +418,26 @@ describe('useJobList', () => {
workflowStoreMock.activeWorkflow = { activeState: { id: 'workflow-1' } } workflowStoreMock.activeWorkflow = { activeState: { id: 'workflow-1' } }
await flush() await flush()
expect(instance.filteredTasks.value.map((t) => t.promptId)).toEqual([ expect(instance.filteredTasks.value.map((t) => t.jobId)).toEqual(['wf-1'])
'wf-1'
])
}) })
it('hydrates job items with active progress and compute hours', async () => { it('hydrates job items with active progress and compute hours', async () => {
queueStoreMock.runningTasks = [ queueStoreMock.runningTasks = [
createTask({ createTask({
promptId: 'active', jobId: 'active',
queueIndex: 3, queueIndex: 3,
mockState: 'running', mockState: 'running',
executionTime: 7_200_000 executionTime: 7_200_000
}), }),
createTask({ createTask({
promptId: 'other', jobId: 'other',
queueIndex: 2, queueIndex: 2,
mockState: 'running', mockState: 'running',
executionTime: 3_600_000 executionTime: 3_600_000
}) })
] ]
executionStoreMock.activePromptId = 'active' executionStoreMock.activeJobId = 'active'
executionStoreMock.executingNode = { title: 'Render Node' } executionStoreMock.executingNode = { title: 'Render Node' }
totalPercent.value = 80 totalPercent.value = 80
currentNodePercent.value = 40 currentNodePercent.value = 40
@@ -390,7 +460,7 @@ describe('useJobList', () => {
it('assigns preview urls for running jobs when previews enabled', async () => { it('assigns preview urls for running jobs when previews enabled', async () => {
queueStoreMock.runningTasks = [ queueStoreMock.runningTasks = [
createTask({ createTask({
promptId: 'live-preview', jobId: 'live-preview',
queueIndex: 1, queueIndex: 1,
mockState: 'running' mockState: 'running'
}) })
@@ -409,7 +479,7 @@ describe('useJobList', () => {
it('omits preview urls when previews are disabled', async () => { it('omits preview urls when previews are disabled', async () => {
queueStoreMock.runningTasks = [ queueStoreMock.runningTasks = [
createTask({ createTask({
promptId: 'disabled-preview', jobId: 'disabled-preview',
queueIndex: 1, queueIndex: 1,
mockState: 'running' mockState: 'running'
}) })
@@ -450,28 +520,28 @@ describe('useJobList', () => {
vi.setSystemTime(new Date('2024-01-10T12:00:00Z')) vi.setSystemTime(new Date('2024-01-10T12:00:00Z'))
queueStoreMock.historyTasks = [ queueStoreMock.historyTasks = [
createTask({ createTask({
promptId: 'today-small', jobId: 'today-small',
queueIndex: 4, queueIndex: 4,
mockState: 'completed', mockState: 'completed',
executionEndTimestamp: Date.now(), executionEndTimestamp: Date.now(),
executionTime: 2_000 executionTime: 2_000
}), }),
createTask({ createTask({
promptId: 'today-large', jobId: 'today-large',
queueIndex: 3, queueIndex: 3,
mockState: 'completed', mockState: 'completed',
executionEndTimestamp: Date.now(), executionEndTimestamp: Date.now(),
executionTime: 5_000 executionTime: 5_000
}), }),
createTask({ createTask({
promptId: 'yesterday', jobId: 'yesterday',
queueIndex: 2, queueIndex: 2,
mockState: 'failed', mockState: 'failed',
executionEndTimestamp: Date.now() - 86_400_000, executionEndTimestamp: Date.now() - 86_400_000,
executionTime: 1_000 executionTime: 1_000
}), }),
createTask({ createTask({
promptId: 'undated', jobId: 'undated',
queueIndex: 1, queueIndex: 1,
mockState: 'pending' mockState: 'pending'
}) })

View File

@@ -127,7 +127,7 @@ export function useJobList() {
watch( watch(
() => () =>
queueStore.pendingTasks queueStore.pendingTasks
.map((task) => taskIdToKey(task.promptId)) .map((task) => taskIdToKey(task.jobId))
.filter((id): id is string => !!id), .filter((id): id is string => !!id),
(pendingIds) => { (pendingIds) => {
const pendingSet = new Set(pendingIds) const pendingSet = new Set(pendingIds)
@@ -158,7 +158,7 @@ export function useJobList() {
const shouldShowAddedHint = (task: TaskItemImpl, state: JobState) => { const shouldShowAddedHint = (task: TaskItemImpl, state: JobState) => {
if (state !== 'pending') return false if (state !== 'pending') return false
const id = taskIdToKey(task.promptId) const id = taskIdToKey(task.jobId)
if (!id) return false if (!id) return false
return recentlyAddedPendingIds.value.has(id) return recentlyAddedPendingIds.value.has(id)
} }
@@ -183,8 +183,8 @@ export function useJobList() {
}) })
const undatedLabel = computed(() => t('queue.jobList.undated') || 'Undated') const undatedLabel = computed(() => t('queue.jobList.undated') || 'Undated')
const isJobInitializing = (promptId: string | number | undefined) => const isJobInitializing = (jobId: string | number | undefined) =>
executionStore.isPromptInitializing(promptId) executionStore.isJobInitializing(jobId)
const currentNodeName = computed(() => { const currentNodeName = computed(() => {
return resolveNodeDisplayName(executionStore.executingNode, { return resolveNodeDisplayName(executionStore.executingNode, {
@@ -212,7 +212,7 @@ export function useJobList() {
const tasksWithJobState = computed<TaskWithState[]>(() => const tasksWithJobState = computed<TaskWithState[]>(() =>
allTasksSorted.value.map((task) => ({ allTasksSorted.value.map((task) => ({
task, task,
state: jobStateFromTask(task, isJobInitializing(task?.promptId)) state: jobStateFromTask(task, isJobInitializing(task?.jobId))
})) }))
) )
@@ -255,10 +255,9 @@ export function useJobList() {
const jobItems = computed<JobListItem[]>(() => { const jobItems = computed<JobListItem[]>(() => {
return filteredTaskEntries.value.map(({ task, state }) => { return filteredTaskEntries.value.map(({ task, state }) => {
const isActive = const isActive =
String(task.promptId ?? '') === String(task.jobId ?? '') === String(executionStore.activeJobId ?? '')
String(executionStore.activePromptId ?? '')
const showAddedHint = shouldShowAddedHint(task, state) const showAddedHint = shouldShowAddedHint(task, state)
const promptKey = taskIdToKey(task.promptId) const promptKey = taskIdToKey(task.jobId)
const promptPreviewUrl = const promptPreviewUrl =
state === 'running' && jobPreviewStore.isPreviewEnabled && promptKey state === 'running' && jobPreviewStore.isPreviewEnabled && promptKey
? jobPreviewStore.previewsByPromptId[promptKey] ? jobPreviewStore.previewsByPromptId[promptKey]
@@ -277,7 +276,7 @@ export function useJobList() {
}) })
return { return {
id: String(task.promptId), id: String(task.jobId),
title: display.primary, title: display.primary,
meta: display.secondary, meta: display.secondary,
state, state,
@@ -334,7 +333,7 @@ export function useJobList() {
groupIdx = groups.length - 1 groupIdx = groups.length - 1
index.set(key, groupIdx) index.set(key, groupIdx)
} }
const ji = jobItemById.value.get(String(task.promptId)) const ji = jobItemById.value.get(String(task.jobId))
if (ji) groups[groupIdx].items.push(ji) if (ji) groups[groupIdx].items.push(ji)
} }

View File

@@ -72,8 +72,7 @@ const interruptMock = vi.fn()
const deleteItemMock = vi.fn() const deleteItemMock = vi.fn()
vi.mock('@/scripts/api', () => ({ vi.mock('@/scripts/api', () => ({
api: { api: {
interrupt: (runningPromptId: string | null) => interrupt: (runningJobId: string | null) => interruptMock(runningJobId),
interruptMock(runningPromptId),
deleteItem: (type: string, id: string) => deleteItemMock(type, id) deleteItem: (type: string, id: string) => deleteItemMock(type, id)
} }
})) }))
@@ -120,7 +119,7 @@ vi.mock('@/stores/queueStore', () => ({
})) }))
const executionStoreMock = { const executionStoreMock = {
clearInitializationByPromptId: vi.fn() clearInitializationByJobId: vi.fn()
} }
vi.mock('@/stores/executionStore', () => ({ vi.mock('@/stores/executionStore', () => ({
useExecutionStore: () => executionStoreMock useExecutionStore: () => executionStoreMock

View File

@@ -84,7 +84,7 @@ export function useJobMenu(
} else if (target.state === 'pending') { } else if (target.state === 'pending') {
await api.deleteItem('queue', target.id) await api.deleteItem('queue', target.id)
} }
executionStore.clearInitializationByPromptId(target.id) executionStore.clearInitializationByJobId(target.id)
await queueStore.update() await queueStore.update()
} }

View File

@@ -17,7 +17,7 @@ const executionStore = reactive<{
executingNode: unknown executingNode: unknown
executingNodeProgress: number executingNodeProgress: number
nodeProgressStates: Record<string, unknown> nodeProgressStates: Record<string, unknown>
activePrompt: { activeJob: {
workflow: { workflow: {
changeTracker: { changeTracker: {
activeState: { activeState: {
@@ -32,7 +32,7 @@ const executionStore = reactive<{
executingNode: null, executingNode: null,
executingNodeProgress: 0, executingNodeProgress: 0,
nodeProgressStates: {}, nodeProgressStates: {},
activePrompt: null activeJob: null
}) })
vi.mock('@/stores/executionStore', () => ({ vi.mock('@/stores/executionStore', () => ({
useExecutionStore: () => executionStore useExecutionStore: () => executionStore
@@ -76,7 +76,7 @@ describe('useBrowserTabTitle', () => {
executionStore.executingNode = null executionStore.executingNode = null
executionStore.executingNodeProgress = 0 executionStore.executingNodeProgress = 0
executionStore.nodeProgressStates = {} executionStore.nodeProgressStates = {}
executionStore.activePrompt = null executionStore.activeJob = null
// reset setting and workflow stores // reset setting and workflow stores
vi.mocked(settingStore.get).mockReturnValue('Enabled') vi.mocked(settingStore.get).mockReturnValue('Enabled')
@@ -187,7 +187,7 @@ describe('useBrowserTabTitle', () => {
executionStore.nodeProgressStates = { executionStore.nodeProgressStates = {
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' } '1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
} }
executionStore.activePrompt = { executionStore.activeJob = {
workflow: { workflow: {
changeTracker: { changeTracker: {
activeState: { activeState: {

View File

@@ -77,7 +77,7 @@ export const useBrowserTabTitle = () => {
const [nodeId, state] = runningNodes[0] const [nodeId, state] = runningNodes[0]
const progress = Math.round((state.value / state.max) * 100) const progress = Math.round((state.value / state.max) * 100)
const nodeType = const nodeType =
executionStore.activePrompt?.workflow?.changeTracker?.activeState.nodes.find( executionStore.activeJob?.workflow?.changeTracker?.activeState.nodes.find(
(n) => String(n.id) === nodeId (n) => String(n.id) === nodeId
)?.type || 'Node' )?.type || 'Node'

View File

@@ -312,7 +312,7 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Interrupt', label: 'Interrupt',
category: 'essentials' as const, category: 'essentials' as const,
function: async () => { function: async () => {
await api.interrupt(executionStore.activePromptId) await api.interrupt(executionStore.activeJobId)
toastStore.add({ toastStore.add({
severity: 'info', severity: 'info',
summary: t('g.interrupted'), summary: t('g.interrupted'),

View File

@@ -27,7 +27,7 @@ export function mapTaskOutputToAssetItem(
output: ResultItemImpl output: ResultItemImpl
): AssetItem { ): AssetItem {
const metadata: OutputAssetMetadata = { const metadata: OutputAssetMetadata = {
promptId: taskItem.promptId, jobId: taskItem.jobId,
nodeId: output.nodeId, nodeId: output.nodeId,
subfolder: output.subfolder, subfolder: output.subfolder,
executionTimeInSeconds: taskItem.executionTimeInSeconds, executionTimeInSeconds: taskItem.executionTimeInSeconds,
@@ -36,7 +36,7 @@ export function mapTaskOutputToAssetItem(
} }
return { return {
id: taskItem.promptId, id: taskItem.jobId,
name: output.filename, name: output.filename,
size: 0, size: 0,
created_at: taskItem.executionStartTimestamp created_at: taskItem.executionStartTimestamp

View File

@@ -46,12 +46,12 @@ export function useMediaAssetActions() {
assetType: string assetType: string
): Promise<void> => { ): Promise<void> => {
if (assetType === 'output') { if (assetType === 'output') {
const promptId = const jobId =
getOutputAssetMetadata(asset.user_metadata)?.promptId || asset.id getOutputAssetMetadata(asset.user_metadata)?.jobId || asset.id
if (!promptId) { if (!jobId) {
throw new Error('Unable to extract prompt ID from asset') throw new Error('Unable to extract job ID from asset')
} }
await api.deleteItem('history', promptId) await api.deleteItem('history', jobId)
} else { } else {
// Input assets can only be deleted in cloud environment // Input assets can only be deleted in cloud environment
if (!isCloud) { if (!isCloud) {
@@ -141,16 +141,16 @@ export function useMediaAssetActions() {
for (const asset of assets) { for (const asset of assets) {
if (getAssetType(asset) === 'output') { if (getAssetType(asset) === 'output') {
const metadata = getOutputAssetMetadata(asset.user_metadata) const metadata = getOutputAssetMetadata(asset.user_metadata)
const promptId = metadata?.promptId || asset.id const jobId = metadata?.jobId || asset.id
if (!jobIds.includes(promptId)) { if (!jobIds.includes(jobId)) {
jobIds.push(promptId) jobIds.push(jobId)
} }
if (metadata?.promptId && asset.name) { if (metadata?.jobId && asset.name) {
if (!jobAssetNameFilters[metadata.promptId]) { if (!jobAssetNameFilters[metadata.jobId]) {
jobAssetNameFilters[metadata.promptId] = [] jobAssetNameFilters[metadata.jobId] = []
} }
if (!jobAssetNameFilters[metadata.promptId].includes(asset.name)) { if (!jobAssetNameFilters[metadata.jobId].includes(asset.name)) {
jobAssetNameFilters[metadata.promptId].push(asset.name) jobAssetNameFilters[metadata.jobId].push(asset.name)
} }
} }
} else { } else {
@@ -191,11 +191,11 @@ export function useMediaAssetActions() {
if (!targetAsset) return if (!targetAsset) return
const metadata = getOutputAssetMetadata(targetAsset.user_metadata) const metadata = getOutputAssetMetadata(targetAsset.user_metadata)
const promptId = const jobId =
metadata?.promptId || metadata?.jobId ||
(getAssetType(targetAsset) === 'output' ? targetAsset.id : undefined) (getAssetType(targetAsset) === 'output' ? targetAsset.id : undefined)
if (!promptId) { if (!jobId) {
toast.add({ toast.add({
severity: 'warn', severity: 'warn',
summary: t('g.warning'), summary: t('g.warning'),
@@ -205,7 +205,7 @@ export function useMediaAssetActions() {
return return
} }
await copyToClipboard(promptId) await copyToClipboard(jobId)
} }
/** /**

View File

@@ -40,7 +40,7 @@ function createAsset(overrides: Partial<AssetItem> = {}): AssetItem {
tags: [], tags: [],
created_at: '2025-01-01T00:00:00.000Z', created_at: '2025-01-01T00:00:00.000Z',
user_metadata: { user_metadata: {
promptId: 'prompt-1', jobId: 'job-1',
nodeId: 'node-1', nodeId: 'node-1',
subfolder: 'outputs' subfolder: 'outputs'
}, },
@@ -74,7 +74,7 @@ describe('useOutputStacks', () => {
await toggleStack(parent) await toggleStack(parent)
expect(mocks.resolveOutputAssetItems).toHaveBeenCalledWith( expect(mocks.resolveOutputAssetItems).toHaveBeenCalledWith(
expect.objectContaining({ promptId: 'prompt-1' }), expect.objectContaining({ jobId: 'job-1' }),
{ {
createdAt: parent.created_at, createdAt: parent.created_at,
excludeOutputKey: 'node-1-outputs-parent.png' excludeOutputKey: 'node-1-outputs-parent.png'

View File

@@ -19,25 +19,25 @@ type UseOutputStacksOptions = {
} }
export function useOutputStacks({ assets }: UseOutputStacksOptions) { export function useOutputStacks({ assets }: UseOutputStacksOptions) {
const expandedStackPromptIds = ref<Set<string>>(new Set()) const expandedStackJobIds = ref<Set<string>>(new Set())
const stackChildrenByPromptId = ref<Record<string, AssetItem[]>>({}) const stackChildrenByJobId = ref<Record<string, AssetItem[]>>({})
const loadingStackPromptIds = ref<Set<string>>(new Set()) const loadingStackJobIds = ref<Set<string>>(new Set())
const assetItems = computed<OutputStackListItem[]>(() => { const assetItems = computed<OutputStackListItem[]>(() => {
const items: OutputStackListItem[] = [] const items: OutputStackListItem[] = []
for (const asset of assets.value) { for (const asset of assets.value) {
const promptId = getStackPromptId(asset) const jobId = getStackJobId(asset)
items.push({ items.push({
key: `asset-${asset.id}`, key: `asset-${asset.id}`,
asset asset
}) })
if (!promptId || !expandedStackPromptIds.value.has(promptId)) { if (!jobId || !expandedStackJobIds.value.has(jobId)) {
continue continue
} }
const children = stackChildrenByPromptId.value[promptId] ?? [] const children = stackChildrenByJobId.value[jobId] ?? []
for (const child of children) { for (const child of children) {
items.push({ items.push({
key: `asset-${child.id}`, key: `asset-${child.id}`,
@@ -54,55 +54,55 @@ export function useOutputStacks({ assets }: UseOutputStacksOptions) {
assetItems.value.map((item) => item.asset) assetItems.value.map((item) => item.asset)
) )
function getStackPromptId(asset: AssetItem): string | null { function getStackJobId(asset: AssetItem): string | null {
const metadata = getOutputAssetMetadata(asset.user_metadata) const metadata = getOutputAssetMetadata(asset.user_metadata)
return metadata?.promptId ?? null return metadata?.jobId ?? null
} }
function isStackExpanded(asset: AssetItem): boolean { function isStackExpanded(asset: AssetItem): boolean {
const promptId = getStackPromptId(asset) const jobId = getStackJobId(asset)
if (!promptId) return false if (!jobId) return false
return expandedStackPromptIds.value.has(promptId) return expandedStackJobIds.value.has(jobId)
} }
async function toggleStack(asset: AssetItem) { async function toggleStack(asset: AssetItem) {
const promptId = getStackPromptId(asset) const jobId = getStackJobId(asset)
if (!promptId) return if (!jobId) return
if (expandedStackPromptIds.value.has(promptId)) { if (expandedStackJobIds.value.has(jobId)) {
const next = new Set(expandedStackPromptIds.value) const next = new Set(expandedStackJobIds.value)
next.delete(promptId) next.delete(jobId)
expandedStackPromptIds.value = next expandedStackJobIds.value = next
return return
} }
if (!stackChildrenByPromptId.value[promptId]?.length) { if (!stackChildrenByJobId.value[jobId]?.length) {
if (loadingStackPromptIds.value.has(promptId)) { if (loadingStackJobIds.value.has(jobId)) {
return return
} }
const nextLoading = new Set(loadingStackPromptIds.value) const nextLoading = new Set(loadingStackJobIds.value)
nextLoading.add(promptId) nextLoading.add(jobId)
loadingStackPromptIds.value = nextLoading loadingStackJobIds.value = nextLoading
const children = await resolveStackChildren(asset) const children = await resolveStackChildren(asset)
const afterLoading = new Set(loadingStackPromptIds.value) const afterLoading = new Set(loadingStackJobIds.value)
afterLoading.delete(promptId) afterLoading.delete(jobId)
loadingStackPromptIds.value = afterLoading loadingStackJobIds.value = afterLoading
if (!children.length) { if (!children.length) {
return return
} }
stackChildrenByPromptId.value = { stackChildrenByJobId.value = {
...stackChildrenByPromptId.value, ...stackChildrenByJobId.value,
[promptId]: children [jobId]: children
} }
} }
const nextExpanded = new Set(expandedStackPromptIds.value) const nextExpanded = new Set(expandedStackJobIds.value)
nextExpanded.add(promptId) nextExpanded.add(jobId)
expandedStackPromptIds.value = nextExpanded expandedStackJobIds.value = nextExpanded
} }
async function resolveStackChildren(asset: AssetItem): Promise<AssetItem[]> { async function resolveStackChildren(asset: AssetItem): Promise<AssetItem[]> {

View File

@@ -6,7 +6,7 @@ import type { ResultItemImpl } from '@/stores/queueStore'
* Extends Record<string, unknown> for compatibility with AssetItem schema * Extends Record<string, unknown> for compatibility with AssetItem schema
*/ */
export interface OutputAssetMetadata extends Record<string, unknown> { export interface OutputAssetMetadata extends Record<string, unknown> {
promptId: string jobId: string
nodeId: string | number nodeId: string | number
subfolder: string subfolder: string
executionTimeInSeconds?: number executionTimeInSeconds?: number
@@ -24,7 +24,7 @@ function isOutputAssetMetadata(
): metadata is OutputAssetMetadata { ): metadata is OutputAssetMetadata {
if (!metadata) return false if (!metadata) return false
return ( return (
typeof metadata.promptId === 'string' && typeof metadata.jobId === 'string' &&
(typeof metadata.nodeId === 'string' || typeof metadata.nodeId === 'number') (typeof metadata.nodeId === 'string' || typeof metadata.nodeId === 'number')
) )
} }

View File

@@ -49,7 +49,7 @@ describe('resolveOutputAssetItems', () => {
url: 'https://example.com/b.png' url: 'https://example.com/b.png'
}) })
const metadata: OutputAssetMetadata = { const metadata: OutputAssetMetadata = {
promptId: 'prompt-1', jobId: 'job-1',
nodeId: '1', nodeId: '1',
subfolder: 'sub', subfolder: 'sub',
executionTimeInSeconds: 12.5, executionTimeInSeconds: 12.5,
@@ -66,7 +66,7 @@ describe('resolveOutputAssetItems', () => {
expect(results).toHaveLength(1) expect(results).toHaveLength(1)
expect(results[0]).toEqual( expect(results[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
id: 'prompt-1-1-sub-a.png', id: 'job-1-1-sub-a.png',
name: 'a.png', name: 'a.png',
created_at: '2025-01-01T00:00:00.000Z', created_at: '2025-01-01T00:00:00.000Z',
tags: ['output'], tags: ['output'],
@@ -75,7 +75,7 @@ describe('resolveOutputAssetItems', () => {
) )
expect(results[0].user_metadata).toEqual( expect(results[0].user_metadata).toEqual(
expect.objectContaining({ expect.objectContaining({
promptId: 'prompt-1', jobId: 'job-1',
nodeId: '1', nodeId: '1',
subfolder: 'sub', subfolder: 'sub',
executionTimeInSeconds: 12.5 executionTimeInSeconds: 12.5
@@ -95,7 +95,7 @@ describe('resolveOutputAssetItems', () => {
url: 'https://example.com/full.png' url: 'https://example.com/full.png'
}) })
const metadata: OutputAssetMetadata = { const metadata: OutputAssetMetadata = {
promptId: 'prompt-2', jobId: 'job-2',
nodeId: '1', nodeId: '1',
subfolder: 'sub', subfolder: 'sub',
outputCount: 3, outputCount: 3,
@@ -111,7 +111,7 @@ describe('resolveOutputAssetItems', () => {
const results = await resolveOutputAssetItems(metadata) const results = await resolveOutputAssetItems(metadata)
expect(mocks.getJobDetail).toHaveBeenCalledWith('prompt-2') expect(mocks.getJobDetail).toHaveBeenCalledWith('job-2')
expect(mocks.getPreviewableOutputsFromJobDetail).toHaveBeenCalledWith( expect(mocks.getPreviewableOutputsFromJobDetail).toHaveBeenCalledWith(
jobDetail jobDetail
) )
@@ -129,7 +129,7 @@ describe('resolveOutputAssetItems', () => {
url: 'https://example.com/root.png' url: 'https://example.com/root.png'
}) })
const metadata: OutputAssetMetadata = { const metadata: OutputAssetMetadata = {
promptId: 'prompt-root', jobId: 'job-root',
nodeId: '1', nodeId: '1',
subfolder: '', subfolder: '',
outputCount: 1, outputCount: 1,
@@ -144,7 +144,7 @@ describe('resolveOutputAssetItems', () => {
if (!asset) { if (!asset) {
throw new Error('Expected a root output asset') throw new Error('Expected a root output asset')
} }
expect(asset.id).toBe('prompt-root-1--root.png') expect(asset.id).toBe('job-root-1--root.png')
if (!asset.user_metadata) { if (!asset.user_metadata) {
throw new Error('Expected output metadata') throw new Error('Expected output metadata')
} }

View File

@@ -8,7 +8,7 @@ import {
import type { ResultItemImpl } from '@/stores/queueStore' import type { ResultItemImpl } from '@/stores/queueStore'
type OutputAssetMapOptions = { type OutputAssetMapOptions = {
promptId: string jobId: string
outputs: readonly ResultItemImpl[] outputs: readonly ResultItemImpl[]
createdAt?: string createdAt?: string
executionTimeInSeconds?: number executionTimeInSeconds?: number
@@ -51,7 +51,7 @@ export function getOutputKey({
} }
function mapOutputsToAssetItems({ function mapOutputsToAssetItems({
promptId, jobId,
outputs, outputs,
createdAt, createdAt,
executionTimeInSeconds, executionTimeInSeconds,
@@ -67,14 +67,14 @@ function mapOutputsToAssetItems({
} }
items.push({ items.push({
id: `${promptId}-${outputKey}`, id: `${jobId}-${outputKey}`,
name: output.filename, name: output.filename,
size: 0, size: 0,
created_at: createdAtValue, created_at: createdAtValue,
tags: ['output'], tags: ['output'],
preview_url: output.url, preview_url: output.url,
user_metadata: { user_metadata: {
promptId, jobId,
nodeId: output.nodeId, nodeId: output.nodeId,
subfolder: output.subfolder, subfolder: output.subfolder,
executionTimeInSeconds, executionTimeInSeconds,
@@ -92,7 +92,7 @@ export async function resolveOutputAssetItems(
): Promise<AssetItem[]> { ): Promise<AssetItem[]> {
let outputsToDisplay = metadata.allOutputs ?? [] let outputsToDisplay = metadata.allOutputs ?? []
if (shouldLoadFullOutputs(metadata.outputCount, outputsToDisplay.length)) { if (shouldLoadFullOutputs(metadata.outputCount, outputsToDisplay.length)) {
const jobDetail = await getJobDetail(metadata.promptId) const jobDetail = await getJobDetail(metadata.jobId)
const previewableOutputs = getPreviewableOutputsFromJobDetail(jobDetail) const previewableOutputs = getPreviewableOutputsFromJobDetail(jobDetail)
if (previewableOutputs.length) { if (previewableOutputs.length) {
outputsToDisplay = previewableOutputs outputsToDisplay = previewableOutputs
@@ -100,7 +100,7 @@ export async function resolveOutputAssetItems(
} }
return mapOutputsToAssetItems({ return mapOutputsToAssetItems({
promptId: metadata.promptId, jobId: metadata.jobId,
outputs: outputsToDisplay, outputs: outputsToDisplay,
createdAt, createdAt,
executionTimeInSeconds: metadata.executionTimeInSeconds, executionTimeInSeconds: metadata.executionTimeInSeconds,

View File

@@ -8,7 +8,7 @@
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema' import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { PromptId } from '@/schemas/apiSchema' import type { JobId } from '@/schemas/apiSchema'
import type { import type {
JobDetail, JobDetail,
@@ -119,19 +119,19 @@ export async function fetchQueue(
*/ */
export async function fetchJobDetail( export async function fetchJobDetail(
fetchApi: (url: string) => Promise<Response>, fetchApi: (url: string) => Promise<Response>,
promptId: PromptId jobId: JobId
): Promise<JobDetail | undefined> { ): Promise<JobDetail | undefined> {
try { try {
const res = await fetchApi(`/jobs/${encodeURIComponent(promptId)}`) const res = await fetchApi(`/jobs/${encodeURIComponent(jobId)}`)
if (!res.ok) { if (!res.ok) {
console.warn(`Job not found for prompt ${promptId}`) console.warn(`Job not found for job ${jobId}`)
return undefined return undefined
} }
return zJobDetail.parse(await res.json()) return zJobDetail.parse(await res.json())
} catch (error) { } catch (error) {
console.error(`Failed to fetch job detail for prompt ${promptId}:`, error) console.error(`Failed to fetch job detail for job ${jobId}:`, error)
return undefined return undefined
} }
} }

View File

@@ -21,7 +21,7 @@ const mockWorkflow: ComfyWorkflowJSON = {
// Jobs API detail response structure (matches actual /jobs/{id} response) // Jobs API detail response structure (matches actual /jobs/{id} response)
// workflow is nested at: workflow.extra_data.extra_pnginfo.workflow // workflow is nested at: workflow.extra_data.extra_pnginfo.workflow
const mockJobDetailResponse: JobDetail = { const mockJobDetailResponse: JobDetail = {
id: 'test-prompt-id', id: 'test-job-id',
status: 'completed', status: 'completed',
create_time: 1234567890, create_time: 1234567890,
update_time: 1234567900, update_time: 1234567900,
@@ -43,15 +43,15 @@ const mockJobDetailResponse: JobDetail = {
} }
describe('fetchJobDetail', () => { describe('fetchJobDetail', () => {
it('should fetch job detail from /jobs/{prompt_id} endpoint', async () => { it('should fetch job detail from /jobs/{job_id} endpoint', async () => {
const mockFetchApi = vi.fn().mockResolvedValue({ const mockFetchApi = vi.fn().mockResolvedValue({
ok: true, ok: true,
json: async () => mockJobDetailResponse json: async () => mockJobDetailResponse
}) })
await fetchJobDetail(mockFetchApi, 'test-prompt-id') await fetchJobDetail(mockFetchApi, 'test-job-id')
expect(mockFetchApi).toHaveBeenCalledWith('/jobs/test-prompt-id') expect(mockFetchApi).toHaveBeenCalledWith('/jobs/test-job-id')
}) })
it('should return job detail with workflow and outputs', async () => { it('should return job detail with workflow and outputs', async () => {
@@ -60,10 +60,10 @@ describe('fetchJobDetail', () => {
json: async () => mockJobDetailResponse json: async () => mockJobDetailResponse
}) })
const result = await fetchJobDetail(mockFetchApi, 'test-prompt-id') const result = await fetchJobDetail(mockFetchApi, 'test-job-id')
expect(result).toBeDefined() expect(result).toBeDefined()
expect(result?.id).toBe('test-prompt-id') expect(result?.id).toBe('test-job-id')
expect(result?.outputs).toEqual(mockJobDetailResponse.outputs) expect(result?.outputs).toEqual(mockJobDetailResponse.outputs)
expect(result?.workflow).toBeDefined() expect(result?.workflow).toBeDefined()
}) })
@@ -82,7 +82,7 @@ describe('fetchJobDetail', () => {
it('should handle fetch errors gracefully', async () => { it('should handle fetch errors gracefully', async () => {
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error')) const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
const result = await fetchJobDetail(mockFetchApi, 'test-prompt-id') const result = await fetchJobDetail(mockFetchApi, 'test-job-id')
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
@@ -95,7 +95,7 @@ describe('fetchJobDetail', () => {
} }
}) })
const result = await fetchJobDetail(mockFetchApi, 'test-prompt-id') const result = await fetchJobDetail(mockFetchApi, 'test-job-id')
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })

View File

@@ -19,7 +19,7 @@ import { getJobWorkflow } from '@/services/jobOutputCache'
* @returns WorkflowSource with workflow and generated filename * @returns WorkflowSource with workflow and generated filename
* *
* @example * @example
* const asset = { name: 'output.png', user_metadata: { promptId: '123' } } * const asset = { name: 'output.png', user_metadata: { jobId: '123' } }
* const { workflow, filename } = await extractWorkflowFromAsset(asset) * const { workflow, filename } = await extractWorkflowFromAsset(asset)
*/ */
export async function extractWorkflowFromAsset(asset: AssetItem): Promise<{ export async function extractWorkflowFromAsset(asset: AssetItem): Promise<{
@@ -30,8 +30,8 @@ export async function extractWorkflowFromAsset(asset: AssetItem): Promise<{
// For output assets: use jobs API (with caching and validation) // For output assets: use jobs API (with caching and validation)
const metadata = getOutputAssetMetadata(asset.user_metadata) const metadata = getOutputAssetMetadata(asset.user_metadata)
if (metadata?.promptId) { if (metadata?.jobId) {
const workflow = await getJobWorkflow(metadata.promptId) const workflow = await getJobWorkflow(metadata.jobId)
return { workflow: workflow ?? null, filename: baseFilename } return { workflow: workflow ?? null, filename: baseFilename }
} }

View File

@@ -135,7 +135,7 @@ function allOutputs(item?: AssetItem): MaybeRef<ResultItemImpl[]> {
return user_metadata.allOutputs return user_metadata.allOutputs
const outputRef = useAsyncState( const outputRef = useAsyncState(
getJobDetail(user_metadata.promptId).then((jobDetail) => { getJobDetail(user_metadata.jobId).then((jobDetail) => {
if (!jobDetail?.outputs) return [] if (!jobDetail?.outputs) return []
return Object.entries(jobDetail.outputs).flatMap(flattenNodeOutput) return Object.entries(jobDetail.outputs).flatMap(flattenNodeOutput)
}), }),

View File

@@ -8,8 +8,8 @@ import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes' import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
const zNodeType = z.string() const zNodeType = z.string()
const zPromptId = z.string() const zJobId = z.string()
export type PromptId = z.infer<typeof zPromptId> export type JobId = z.infer<typeof zJobId>
export const resultItemType = z.enum(['input', 'output', 'temp']) export const resultItemType = z.enum(['input', 'output', 'temp'])
export type ResultItemType = z.infer<typeof resultItemType> export type ResultItemType = z.infer<typeof resultItemType>
@@ -52,7 +52,7 @@ const zStatusWsMessage = z.object({
const zProgressWsMessage = z.object({ const zProgressWsMessage = z.object({
value: z.number().int(), value: z.number().int(),
max: z.number().int(), max: z.number().int(),
prompt_id: zPromptId, prompt_id: zJobId,
node: zNodeId node: zNodeId
}) })
@@ -61,21 +61,21 @@ const zNodeProgressState = z.object({
max: z.number(), max: z.number(),
state: z.enum(['pending', 'running', 'finished', 'error']), state: z.enum(['pending', 'running', 'finished', 'error']),
node_id: zNodeId, node_id: zNodeId,
prompt_id: zPromptId, prompt_id: zJobId,
display_node_id: zNodeId.optional(), display_node_id: zNodeId.optional(),
parent_node_id: zNodeId.optional(), parent_node_id: zNodeId.optional(),
real_node_id: zNodeId.optional() real_node_id: zNodeId.optional()
}) })
const zProgressStateWsMessage = z.object({ const zProgressStateWsMessage = z.object({
prompt_id: zPromptId, prompt_id: zJobId,
nodes: z.record(zNodeId, zNodeProgressState) nodes: z.record(zNodeId, zNodeProgressState)
}) })
const zExecutingWsMessage = z.object({ const zExecutingWsMessage = z.object({
node: zNodeId, node: zNodeId,
display_node: zNodeId, display_node: zNodeId,
prompt_id: zPromptId prompt_id: zJobId
}) })
const zExecutedWsMessage = zExecutingWsMessage.extend({ const zExecutedWsMessage = zExecutingWsMessage.extend({
@@ -84,7 +84,7 @@ const zExecutedWsMessage = zExecutingWsMessage.extend({
}) })
const zExecutionWsMessageBase = z.object({ const zExecutionWsMessageBase = z.object({
prompt_id: zPromptId, prompt_id: zJobId,
timestamp: z.number().int() timestamp: z.number().int()
}) })

View File

@@ -157,14 +157,14 @@ interface BackendApiCalls {
logs: LogsWsMessage logs: LogsWsMessage
/** Binary preview/progress data */ /** Binary preview/progress data */
b_preview: Blob b_preview: Blob
/** Binary preview with metadata (node_id, prompt_id) */ /** Binary preview with metadata (node_id, job_id) */
b_preview_with_metadata: { b_preview_with_metadata: {
blob: Blob blob: Blob
nodeId: string nodeId: string
parentNodeId: string parentNodeId: string
displayNodeId: string displayNodeId: string
realNodeId: string realNodeId: string
promptId: string jobId: string
} }
progress_text: ProgressTextWsMessage progress_text: ProgressTextWsMessage
progress_state: ProgressStateWsMessage progress_state: ProgressStateWsMessage
@@ -646,7 +646,7 @@ export class ComfyApi extends EventTarget {
displayNodeId: metadata.display_node_id, displayNodeId: metadata.display_node_id,
parentNodeId: metadata.parent_node_id, parentNodeId: metadata.parent_node_id,
realNodeId: metadata.real_node_id, realNodeId: metadata.real_node_id,
promptId: metadata.prompt_id jobId: metadata.prompt_id
}) })
// Also dispatch legacy b_preview for backward compatibility // Also dispatch legacy b_preview for backward compatibility
@@ -943,7 +943,7 @@ export class ComfyApi extends EventTarget {
/** /**
* Gets detailed job info including outputs and workflow * Gets detailed job info including outputs and workflow
* @param jobId The job/prompt ID * @param jobId The job ID
* @returns Full job details or undefined if not found * @returns Full job details or undefined if not found
*/ */
async getJobDetail(jobId: string): Promise<JobDetail | undefined> { async getJobDetail(jobId: string): Promise<JobDetail | undefined> {
@@ -996,14 +996,14 @@ export class ComfyApi extends EventTarget {
} }
/** /**
* Interrupts the execution of the running prompt. If runningPromptId is provided, * Interrupts the execution of the running job. If runningJobId is provided,
* it is included in the payload as a helpful hint to the backend. * it is included in the payload as a helpful hint to the backend.
* @param {string | null} [runningPromptId] Optional Running Prompt ID to interrupt * @param {string | null} [runningJobId] Optional Running Job ID to interrupt
*/ */
async interrupt(runningPromptId: string | null) { async interrupt(runningJobId: string | null) {
await this._postItem( await this._postItem(
'interrupt', 'interrupt',
runningPromptId ? { prompt_id: runningPromptId } : undefined runningJobId ? { prompt_id: runningJobId } : undefined
) )
} }

View File

@@ -725,11 +725,11 @@ export class ComfyApp {
api.addEventListener('b_preview_with_metadata', ({ detail }) => { api.addEventListener('b_preview_with_metadata', ({ detail }) => {
// Enhanced preview with explicit node context // Enhanced preview with explicit node context
const { blob, displayNodeId, promptId } = detail const { blob, displayNodeId, jobId } = detail
const { setNodePreviewsByExecutionId, revokePreviewsByExecutionId } = const { setNodePreviewsByExecutionId, revokePreviewsByExecutionId } =
useNodeOutputStore() useNodeOutputStore()
const blobUrl = createSharedObjectUrl(blob) const blobUrl = createSharedObjectUrl(blob)
useJobPreviewStore().setPreviewUrl(promptId, blobUrl) useJobPreviewStore().setPreviewUrl(jobId, blobUrl)
// Ensure clean up if `executing` event is missed. // Ensure clean up if `executing` event is missed.
revokePreviewsByExecutionId(displayNodeId) revokePreviewsByExecutionId(displayNodeId)
// Preview cleanup is handled in progress_state event to support multiple concurrent previews // Preview cleanup is handled in progress_state event to support multiple concurrent previews
@@ -1446,7 +1446,7 @@ export class ComfyApp {
} else { } else {
try { try {
if (res.prompt_id) { if (res.prompt_id) {
executionStore.storePrompt({ executionStore.storeJob({
id: res.prompt_id, id: res.prompt_id,
nodes: Object.keys(p.output), nodes: Object.keys(p.output),
workflow: useWorkspaceStore().workflow workflow: useWorkspaceStore().workflow

View File

@@ -339,7 +339,7 @@ export class ChangeTracker {
api.addEventListener('executed', (e: CustomEvent<ExecutedWsMessage>) => { api.addEventListener('executed', (e: CustomEvent<ExecutedWsMessage>) => {
const detail = e.detail const detail = e.detail
const workflow = const workflow =
useExecutionStore().queuedPrompts[detail.prompt_id]?.workflow useExecutionStore().queuedJobs[detail.prompt_id]?.workflow
const changeTracker = workflow?.changeTracker const changeTracker = workflow?.changeTracker
if (!changeTracker) return if (!changeTracker) return
changeTracker.nodeOutputs ??= {} changeTracker.nodeOutputs ??= {}

View File

@@ -46,7 +46,7 @@ export function findActiveIndex(
export async function getOutputsForTask( export async function getOutputsForTask(
task: TaskItemImpl task: TaskItemImpl
): Promise<ResultItemImpl[] | null> { ): Promise<ResultItemImpl[] | null> {
const requestId = String(task.promptId) const requestId = String(task.jobId)
latestTaskRequestId = requestId latestTaskRequestId = requestId
const outputsCount = task.outputsCount ?? 0 const outputsCount = task.outputsCount ?? 0

View File

@@ -90,10 +90,10 @@ vi.mock('@/stores/queueStore', () => ({
url: string url: string
} }
| undefined | undefined
public promptId: string public jobId: string
constructor(public job: JobListItem) { constructor(public job: JobListItem) {
this.promptId = job.id this.jobId = job.id
this.flatOutputs = [ this.flatOutputs = [
{ {
supportsPreview: true, supportsPreview: true,
@@ -123,9 +123,9 @@ vi.mock('@/platform/assets/composables/media/assetMappers', () => ({
preview_url: `http://test.com/${name}` preview_url: `http://test.com/${name}`
})), })),
mapTaskOutputToAssetItem: vi.fn((task, output) => { mapTaskOutputToAssetItem: vi.fn((task, output) => {
const index = parseInt(task.promptId.split('_')[1]) || 0 const index = parseInt(task.jobId.split('_')[1]) || 0
return { return {
id: task.promptId, id: task.jobId,
name: output.filename, name: output.filename,
size: 0, size: 0,
created_at: new Date(Date.now() - index * 1000).toISOString(), created_at: new Date(Date.now() - index * 1000).toISOString(),

View File

@@ -132,7 +132,7 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
}) })
}) })
describe('useExecutionStore - reconcileInitializingPrompts', () => { describe('useExecutionStore - reconcileInitializingJobs', () => {
let store: ReturnType<typeof useExecutionStore> let store: ReturnType<typeof useExecutionStore>
beforeEach(() => { beforeEach(() => {
@@ -141,36 +141,36 @@ describe('useExecutionStore - reconcileInitializingPrompts', () => {
store = useExecutionStore() store = useExecutionStore()
}) })
it('should remove prompt IDs not present in active jobs', () => { it('should remove job IDs not present in active jobs', () => {
store.initializingPromptIds = new Set(['job-1', 'job-2', 'job-3']) store.initializingJobIds = new Set(['job-1', 'job-2', 'job-3'])
store.reconcileInitializingPrompts(new Set(['job-1'])) store.reconcileInitializingJobs(new Set(['job-1']))
expect(store.initializingPromptIds).toEqual(new Set(['job-1'])) expect(store.initializingJobIds).toEqual(new Set(['job-1']))
}) })
it('should be a no-op when all initializing IDs are active', () => { it('should be a no-op when all initializing IDs are active', () => {
store.initializingPromptIds = new Set(['job-1', 'job-2']) store.initializingJobIds = new Set(['job-1', 'job-2'])
store.reconcileInitializingPrompts(new Set(['job-1', 'job-2', 'job-3'])) store.reconcileInitializingJobs(new Set(['job-1', 'job-2', 'job-3']))
expect(store.initializingPromptIds).toEqual(new Set(['job-1', 'job-2'])) expect(store.initializingJobIds).toEqual(new Set(['job-1', 'job-2']))
}) })
it('should be a no-op when there are no initializing prompts', () => { it('should be a no-op when there are no initializing jobs', () => {
store.initializingPromptIds = new Set() store.initializingJobIds = new Set()
store.reconcileInitializingPrompts(new Set(['job-1'])) store.reconcileInitializingJobs(new Set(['job-1']))
expect(store.initializingPromptIds).toEqual(new Set()) expect(store.initializingJobIds).toEqual(new Set())
}) })
it('should clear all initializing IDs when no active jobs exist', () => { it('should clear all initializing IDs when no active jobs exist', () => {
store.initializingPromptIds = new Set(['job-1', 'job-2']) store.initializingJobIds = new Set(['job-1', 'job-2'])
store.reconcileInitializingPrompts(new Set()) store.reconcileInitializingJobs(new Set())
expect(store.initializingPromptIds).toEqual(new Set()) expect(store.initializingJobIds).toEqual(new Set())
}) })
}) })

View File

@@ -37,7 +37,7 @@ import type { NodeLocatorId } from '@/types/nodeIdentification'
import { createNodeLocatorId } from '@/types/nodeIdentification' import { createNodeLocatorId } from '@/types/nodeIdentification'
import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil' import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil'
interface QueuedPrompt { interface QueuedJob {
/** /**
* The nodes that are queued to be executed. The key is the node id and the * The nodes that are queued to be executed. The key is the node id and the
* value is a boolean indicating if the node has been executed. * value is a boolean indicating if the node has been executed.
@@ -111,23 +111,23 @@ export const useExecutionStore = defineStore('execution', () => {
const canvasStore = useCanvasStore() const canvasStore = useCanvasStore()
const clientId = ref<string | null>(null) const clientId = ref<string | null>(null)
const activePromptId = ref<string | null>(null) const activeJobId = ref<string | null>(null)
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({}) const queuedJobs = ref<Record<NodeId, QueuedJob>>({})
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null) const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null) const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
const lastPromptError = ref<PromptError | null>(null) const lastPromptError = ref<PromptError | null>(null)
// This is the progress of all nodes in the currently executing workflow // This is the progress of all nodes in the currently executing workflow
const nodeProgressStates = ref<Record<string, NodeProgressState>>({}) const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
const nodeProgressStatesByPrompt = ref< const nodeProgressStatesByJob = ref<
Record<string, Record<string, NodeProgressState>> Record<string, Record<string, NodeProgressState>>
>({}) >({})
/** /**
* Map of prompt_id to workflow ID for quick lookup across the app. * Map of job ID to workflow ID for quick lookup across the app.
*/ */
const promptIdToWorkflowId = ref<Map<string, string>>(new Map()) const jobIdToWorkflowId = ref<Map<string, string>>(new Map())
const initializingPromptIds = ref<Set<string>>(new Set()) const initializingJobIds = ref<Set<string>>(new Set())
const mergeExecutionProgressStates = ( const mergeExecutionProgressStates = (
currentState: NodeProgressState | undefined, currentState: NodeProgressState | undefined,
@@ -201,7 +201,7 @@ export const useExecutionStore = defineStore('execution', () => {
const executingNode = computed<ComfyNode | null>(() => { const executingNode = computed<ComfyNode | null>(() => {
if (!executingNodeId.value) return null if (!executingNodeId.value) return null
const workflow: ComfyWorkflow | undefined = activePrompt.value?.workflow const workflow: ComfyWorkflow | undefined = activeJob.value?.workflow
if (!workflow) return null if (!workflow) return null
const canvasState: ComfyWorkflowJSON | null = const canvasState: ComfyWorkflowJSON | null =
@@ -222,24 +222,24 @@ export const useExecutionStore = defineStore('execution', () => {
: null : null
) )
const activePrompt = computed<QueuedPrompt | undefined>( const activeJob = computed<QueuedJob | undefined>(
() => queuedPrompts.value[activePromptId.value ?? ''] () => queuedJobs.value[activeJobId.value ?? '']
) )
const totalNodesToExecute = computed<number>(() => { const totalNodesToExecute = computed<number>(() => {
if (!activePrompt.value) return 0 if (!activeJob.value) return 0
return Object.values(activePrompt.value.nodes).length return Object.values(activeJob.value.nodes).length
}) })
const isIdle = computed<boolean>(() => !activePromptId.value) const isIdle = computed<boolean>(() => !activeJobId.value)
const nodesExecuted = computed<number>(() => { const nodesExecuted = computed<number>(() => {
if (!activePrompt.value) return 0 if (!activeJob.value) return 0
return Object.values(activePrompt.value.nodes).filter(Boolean).length return Object.values(activeJob.value.nodes).filter(Boolean).length
}) })
const executionProgress = computed<number>(() => { const executionProgress = computed<number>(() => {
if (!activePrompt.value) return 0 if (!activeJob.value) return 0
const total = totalNodesToExecute.value const total = totalNodesToExecute.value
const done = nodesExecuted.value const done = nodesExecuted.value
return total > 0 ? done / total : 0 return total > 0 ? done / total : 0
@@ -291,65 +291,64 @@ export const useExecutionStore = defineStore('execution', () => {
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) { function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
lastExecutionError.value = null lastExecutionError.value = null
lastPromptError.value = null lastPromptError.value = null
activePromptId.value = e.detail.prompt_id activeJobId.value = e.detail.prompt_id
queuedPrompts.value[activePromptId.value] ??= { nodes: {} } queuedJobs.value[activeJobId.value] ??= { nodes: {} }
clearInitializationByPromptId(activePromptId.value) clearInitializationByJobId(activeJobId.value)
} }
function handleExecutionCached(e: CustomEvent<ExecutionCachedWsMessage>) { function handleExecutionCached(e: CustomEvent<ExecutionCachedWsMessage>) {
if (!activePrompt.value) return if (!activeJob.value) return
for (const n of e.detail.nodes) { for (const n of e.detail.nodes) {
activePrompt.value.nodes[n] = true activeJob.value.nodes[n] = true
} }
} }
function handleExecutionInterrupted( function handleExecutionInterrupted(
e: CustomEvent<ExecutionInterruptedWsMessage> e: CustomEvent<ExecutionInterruptedWsMessage>
) { ) {
const pid = e.detail.prompt_id const jobId = e.detail.prompt_id
if (activePromptId.value) if (activeJobId.value) clearInitializationByJobId(activeJobId.value)
clearInitializationByPromptId(activePromptId.value) resetExecutionState(jobId)
resetExecutionState(pid)
} }
function handleExecuted(e: CustomEvent<ExecutedWsMessage>) { function handleExecuted(e: CustomEvent<ExecutedWsMessage>) {
if (!activePrompt.value) return if (!activeJob.value) return
activePrompt.value.nodes[e.detail.node] = true activeJob.value.nodes[e.detail.node] = true
} }
function handleExecutionSuccess(e: CustomEvent<ExecutionSuccessWsMessage>) { function handleExecutionSuccess(e: CustomEvent<ExecutionSuccessWsMessage>) {
if (isCloud && activePromptId.value) { if (isCloud && activeJobId.value) {
useTelemetry()?.trackExecutionSuccess({ useTelemetry()?.trackExecutionSuccess({
jobId: activePromptId.value jobId: activeJobId.value
}) })
} }
const pid = e.detail.prompt_id const jobId = e.detail.prompt_id
resetExecutionState(pid) resetExecutionState(jobId)
} }
function handleExecuting(e: CustomEvent<NodeId | null>): void { function handleExecuting(e: CustomEvent<NodeId | null>): void {
// Clear the current node progress when a new node starts executing // Clear the current node progress when a new node starts executing
_executingNodeProgress.value = null _executingNodeProgress.value = null
if (!activePrompt.value) return if (!activeJob.value) return
// Update the executing nodes list // Update the executing nodes list
if (typeof e.detail !== 'string') { if (typeof e.detail !== 'string') {
if (activePromptId.value) { if (activeJobId.value) {
delete queuedPrompts.value[activePromptId.value] delete queuedJobs.value[activeJobId.value]
} }
activePromptId.value = null activeJobId.value = null
} }
} }
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) { function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
const { nodes, prompt_id: pid } = e.detail const { nodes, prompt_id: jobId } = e.detail
// Revoke previews for nodes that are starting to execute // Revoke previews for nodes that are starting to execute
const previousForPrompt = nodeProgressStatesByPrompt.value[pid] || {} const previousForJob = nodeProgressStatesByJob.value[jobId] || {}
for (const nodeId in nodes) { for (const nodeId in nodes) {
const nodeState = nodes[nodeId] const nodeState = nodes[nodeId]
if (nodeState.state === 'running' && !previousForPrompt[nodeId]) { if (nodeState.state === 'running' && !previousForJob[nodeId]) {
// This node just started executing, revoke its previews // This node just started executing, revoke its previews
// Note that we're doing the *actual* node id instead of the display node id // Note that we're doing the *actual* node id instead of the display node id
// here intentionally. That way, we don't clear the preview every time a new node // here intentionally. That way, we don't clear the preview every time a new node
@@ -360,9 +359,9 @@ export const useExecutionStore = defineStore('execution', () => {
} }
// Update the progress states for all nodes // Update the progress states for all nodes
nodeProgressStatesByPrompt.value = { nodeProgressStatesByJob.value = {
...nodeProgressStatesByPrompt.value, ...nodeProgressStatesByJob.value,
[pid]: nodes [jobId]: nodes
} }
nodeProgressStates.value = nodes nodeProgressStates.value = nodes
@@ -401,13 +400,13 @@ export const useExecutionStore = defineStore('execution', () => {
error: e.detail.exception_message error: e.detail.exception_message
}) })
} }
clearInitializationByPromptId(e.detail.prompt_id) clearInitializationByJobId(e.detail.prompt_id)
resetExecutionState(e.detail.prompt_id) resetExecutionState(e.detail.prompt_id)
} }
/** /**
* Notification handler used for frontend/cloud initialization tracking. * Notification handler used for frontend/cloud initialization tracking.
* Marks a prompt as initializing when cloud notifies it is waiting for a machine. * Marks a job as initializing when cloud notifies it is waiting for a machine.
*/ */
function handleNotification(e: CustomEvent<NotificationWsMessage>) { function handleNotification(e: CustomEvent<NotificationWsMessage>) {
const payload = e.detail const payload = e.detail
@@ -416,62 +415,60 @@ export const useExecutionStore = defineStore('execution', () => {
if (!id) return if (!id) return
// Until cloud implements a proper message // Until cloud implements a proper message
if (text.includes('Waiting for a machine')) { if (text.includes('Waiting for a machine')) {
const next = new Set(initializingPromptIds.value) const next = new Set(initializingJobIds.value)
next.add(id) next.add(id)
initializingPromptIds.value = next initializingJobIds.value = next
} }
} }
function clearInitializationByPromptId(promptId: string | null) { function clearInitializationByJobId(jobId: string | null) {
if (!promptId) return if (!jobId) return
if (!initializingPromptIds.value.has(promptId)) return if (!initializingJobIds.value.has(jobId)) return
const next = new Set(initializingPromptIds.value) const next = new Set(initializingJobIds.value)
next.delete(promptId) next.delete(jobId)
initializingPromptIds.value = next initializingJobIds.value = next
} }
function clearInitializationByPromptIds(promptIds: string[]) { function clearInitializationByJobIds(jobIds: string[]) {
if (!promptIds.length) return if (!jobIds.length) return
const current = initializingPromptIds.value const current = initializingJobIds.value
const toRemove = promptIds.filter((id) => current.has(id)) const toRemove = jobIds.filter((id) => current.has(id))
if (!toRemove.length) return if (!toRemove.length) return
const next = new Set(current) const next = new Set(current)
for (const id of toRemove) { for (const id of toRemove) {
next.delete(id) next.delete(id)
} }
initializingPromptIds.value = next initializingJobIds.value = next
} }
function reconcileInitializingPrompts(activeJobIds: Set<string>) { function reconcileInitializingJobs(activeJobIds: Set<string>) {
const orphaned = [...initializingPromptIds.value].filter( const orphaned = [...initializingJobIds.value].filter(
(id) => !activeJobIds.has(id) (id) => !activeJobIds.has(id)
) )
clearInitializationByPromptIds(orphaned) clearInitializationByJobIds(orphaned)
} }
function isPromptInitializing( function isJobInitializing(jobId: string | number | undefined): boolean {
promptId: string | number | undefined if (!jobId) return false
): boolean { return initializingJobIds.value.has(String(jobId))
if (!promptId) return false
return initializingPromptIds.value.has(String(promptId))
} }
/** /**
* Reset execution-related state after a run completes or is stopped. * Reset execution-related state after a run completes or is stopped.
*/ */
function resetExecutionState(pid?: string | null) { function resetExecutionState(jobIdParam?: string | null) {
nodeProgressStates.value = {} nodeProgressStates.value = {}
const promptId = pid ?? activePromptId.value ?? null const jobId = jobIdParam ?? activeJobId.value ?? null
if (promptId) { if (jobId) {
const map = { ...nodeProgressStatesByPrompt.value } const map = { ...nodeProgressStatesByJob.value }
delete map[promptId] delete map[jobId]
nodeProgressStatesByPrompt.value = map nodeProgressStatesByJob.value = map
useJobPreviewStore().clearPreview(promptId) useJobPreviewStore().clearPreview(jobId)
} }
if (activePromptId.value) { if (activeJobId.value) {
delete queuedPrompts.value[activePromptId.value] delete queuedJobs.value[activeJobId.value]
} }
activePromptId.value = null activeJobId.value = null
_executingNodeProgress.value = null _executingNodeProgress.value = null
lastPromptError.value = null lastPromptError.value = null
} }
@@ -495,7 +492,7 @@ export const useExecutionStore = defineStore('execution', () => {
useNodeProgressText().showTextPreview(node, text) useNodeProgressText().showTextPreview(node, text)
} }
function storePrompt({ function storeJob({
nodes, nodes,
id, id,
workflow workflow
@@ -504,31 +501,28 @@ export const useExecutionStore = defineStore('execution', () => {
id: string id: string
workflow: ComfyWorkflow workflow: ComfyWorkflow
}) { }) {
queuedPrompts.value[id] ??= { nodes: {} } queuedJobs.value[id] ??= { nodes: {} }
const queuedPrompt = queuedPrompts.value[id] const queuedJob = queuedJobs.value[id]
queuedPrompt.nodes = { queuedJob.nodes = {
...nodes.reduce((p: Record<string, boolean>, n) => { ...nodes.reduce((p: Record<string, boolean>, n) => {
p[n] = false p[n] = false
return p return p
}, {}), }, {}),
...queuedPrompt.nodes ...queuedJob.nodes
} }
queuedPrompt.workflow = workflow queuedJob.workflow = workflow
const wid = workflow?.activeState?.id ?? workflow?.initialState?.id const wid = workflow?.activeState?.id ?? workflow?.initialState?.id
if (wid) { if (wid) {
promptIdToWorkflowId.value.set(String(id), String(wid)) jobIdToWorkflowId.value.set(String(id), String(wid))
} }
} }
/** /**
* Register or update a mapping from prompt_id to workflow ID. * Register or update a mapping from job ID to workflow ID.
*/ */
function registerPromptWorkflowIdMapping( function registerJobWorkflowIdMapping(jobId: string, workflowId: string) {
promptId: string, if (!jobId || !workflowId) return
workflowId: string jobIdToWorkflowId.value.set(String(jobId), String(workflowId))
) {
if (!promptId || !workflowId) return
promptIdToWorkflowId.value.set(String(promptId), String(workflowId))
} }
/** /**
@@ -543,11 +537,9 @@ export const useExecutionStore = defineStore('execution', () => {
return executionId return executionId
} }
const runningPromptIds = computed<string[]>(() => { const runningJobIds = computed<string[]>(() => {
const result: string[] = [] const result: string[] = []
for (const [pid, nodes] of Object.entries( for (const [pid, nodes] of Object.entries(nodeProgressStatesByJob.value)) {
nodeProgressStatesByPrompt.value
)) {
if (Object.values(nodes).some((n) => n.state === 'running')) { if (Object.values(nodes).some((n) => n.state === 'running')) {
result.push(pid) result.push(pid)
} }
@@ -556,7 +548,7 @@ export const useExecutionStore = defineStore('execution', () => {
}) })
const runningWorkflowCount = computed<number>( const runningWorkflowCount = computed<number>(
() => runningPromptIds.value.length () => runningJobIds.value.length
) )
/** Map of node errors indexed by locator ID. */ /** Map of node errors indexed by locator ID. */
@@ -699,8 +691,8 @@ export const useExecutionStore = defineStore('execution', () => {
return { return {
isIdle, isIdle,
clientId, clientId,
activePromptId, activeJobId,
queuedPrompts, queuedJobs,
lastNodeErrors, lastNodeErrors,
lastExecutionError, lastExecutionError,
lastPromptError, lastPromptError,
@@ -708,7 +700,7 @@ export const useExecutionStore = defineStore('execution', () => {
lastExecutionErrorNodeId, lastExecutionErrorNodeId,
executingNodeId, executingNodeId,
executingNodeIds, executingNodeIds,
activePrompt, activeJob,
totalNodesToExecute, totalNodesToExecute,
nodesExecuted, nodesExecuted,
executionProgress, executionProgress,
@@ -716,25 +708,25 @@ export const useExecutionStore = defineStore('execution', () => {
executingNodeProgress, executingNodeProgress,
nodeProgressStates, nodeProgressStates,
nodeLocationProgressStates, nodeLocationProgressStates,
nodeProgressStatesByPrompt, nodeProgressStatesByJob,
runningPromptIds, runningJobIds,
runningWorkflowCount, runningWorkflowCount,
initializingPromptIds, initializingJobIds,
isPromptInitializing, isJobInitializing,
clearInitializationByPromptId, clearInitializationByJobId,
clearInitializationByPromptIds, clearInitializationByJobIds,
reconcileInitializingPrompts, reconcileInitializingJobs,
bindExecutionEvents, bindExecutionEvents,
unbindExecutionEvents, unbindExecutionEvents,
storePrompt, storeJob,
registerPromptWorkflowIdMapping, registerJobWorkflowIdMapping,
uniqueExecutingNodeIdStrings, uniqueExecutingNodeIdStrings,
// Raw executing progress data for backward compatibility in ComfyApp. // Raw executing progress data for backward compatibility in ComfyApp.
_executingNodeProgress, _executingNodeProgress,
// NodeLocatorId conversion helpers // NodeLocatorId conversion helpers
executionIdToNodeLocatorId, executionIdToNodeLocatorId,
nodeLocatorIdToExecutionId, nodeLocatorIdToExecutionId,
promptIdToWorkflowId, jobIdToWorkflowId,
// Node error lookup helpers // Node error lookup helpers
getNodeErrors, getNodeErrors,
slotHasError, slotHasError,

View File

@@ -31,7 +31,7 @@ const mockWorkflow: ComfyWorkflowJSON = {
// Mock job detail response (matches actual /jobs/{id} API response structure) // Mock job detail response (matches actual /jobs/{id} API response structure)
// workflow is nested at: workflow.extra_data.extra_pnginfo.workflow // workflow is nested at: workflow.extra_data.extra_pnginfo.workflow
const mockJobDetail = { const mockJobDetail = {
id: 'test-prompt-id', id: 'test-job-id',
status: 'completed' as const, status: 'completed' as const,
create_time: Date.now(), create_time: Date.now(),
update_time: Date.now(), update_time: Date.now(),
@@ -86,7 +86,7 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => {
}) })
it('should fetch workflow from API for history tasks', async () => { it('should fetch workflow from API for history tasks', async () => {
const job = createHistoryJob('test-prompt-id') const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job) const task = new TaskItemImpl(job)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue( vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
@@ -95,12 +95,12 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => {
await task.loadWorkflow(mockApp) await task.loadWorkflow(mockApp)
expect(jobOutputCache.getJobDetail).toHaveBeenCalledWith('test-prompt-id') expect(jobOutputCache.getJobDetail).toHaveBeenCalledWith('test-job-id')
expect(mockApp.loadGraphData).toHaveBeenCalledWith(mockWorkflow) expect(mockApp.loadGraphData).toHaveBeenCalledWith(mockWorkflow)
}) })
it('should not load workflow when fetch returns undefined', async () => { it('should not load workflow when fetch returns undefined', async () => {
const job = createHistoryJob('test-prompt-id') const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job) const task = new TaskItemImpl(job)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(undefined) vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(undefined)
@@ -112,7 +112,7 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => {
}) })
it('should only fetch for history tasks, not running tasks', async () => { it('should only fetch for history tasks, not running tasks', async () => {
const job = createRunningJob('test-prompt-id') const job = createRunningJob('test-job-id')
const runningTask = new TaskItemImpl(job) const runningTask = new TaskItemImpl(job)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue( vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
@@ -126,7 +126,7 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => {
}) })
it('should handle fetch errors gracefully by returning undefined', async () => { it('should handle fetch errors gracefully by returning undefined', async () => {
const job = createHistoryJob('test-prompt-id') const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job) const task = new TaskItemImpl(job)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(undefined) vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(undefined)

View File

@@ -67,7 +67,7 @@ vi.mock('@/scripts/api', () => ({
describe('TaskItemImpl', () => { describe('TaskItemImpl', () => {
it('should remove animated property from outputs during construction', () => { it('should remove animated property from outputs during construction', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, { const taskItem = new TaskItemImpl(job, {
'node-1': { 'node-1': {
images: [{ filename: 'test.png', type: 'output', subfolder: '' }], images: [{ filename: 'test.png', type: 'output', subfolder: '' }],
@@ -83,7 +83,7 @@ describe('TaskItemImpl', () => {
}) })
it('should handle outputs without animated property', () => { it('should handle outputs without animated property', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, { const taskItem = new TaskItemImpl(job, {
'node-1': { 'node-1': {
images: [{ filename: 'test.png', type: 'output', subfolder: '' }] images: [{ filename: 'test.png', type: 'output', subfolder: '' }]
@@ -95,7 +95,7 @@ describe('TaskItemImpl', () => {
}) })
it('should recognize webm video from core', () => { it('should recognize webm video from core', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, { const taskItem = new TaskItemImpl(job, {
'node-1': { 'node-1': {
video: [{ filename: 'test.webm', type: 'output', subfolder: '' }] video: [{ filename: 'test.webm', type: 'output', subfolder: '' }]
@@ -112,7 +112,7 @@ describe('TaskItemImpl', () => {
// https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite/blob/0a75c7958fe320efcb052f1d9f8451fd20c730a8/videohelpersuite/nodes.py#L578-L590 // https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite/blob/0a75c7958fe320efcb052f1d9f8451fd20c730a8/videohelpersuite/nodes.py#L578-L590
it('should recognize webm video from VHS', () => { it('should recognize webm video from VHS', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, { const taskItem = new TaskItemImpl(job, {
'node-1': { 'node-1': {
gifs: [ gifs: [
@@ -136,7 +136,7 @@ describe('TaskItemImpl', () => {
}) })
it('should recognize mp4 video from core', () => { it('should recognize mp4 video from core', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, { const taskItem = new TaskItemImpl(job, {
'node-1': { 'node-1': {
images: [ images: [
@@ -167,7 +167,7 @@ describe('TaskItemImpl', () => {
audioFormats.forEach(({ extension, mimeType }) => { audioFormats.forEach(({ extension, mimeType }) => {
it(`should recognize ${extension} audio`, () => { it(`should recognize ${extension} audio`, () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, { const taskItem = new TaskItemImpl(job, {
'node-1': { 'node-1': {
audio: [ audio: [
@@ -193,14 +193,14 @@ describe('TaskItemImpl', () => {
describe('error extraction getters', () => { describe('error extraction getters', () => {
it('errorMessage returns undefined when no execution_error', () => { it('errorMessage returns undefined when no execution_error', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job) const taskItem = new TaskItemImpl(job)
expect(taskItem.errorMessage).toBeUndefined() expect(taskItem.errorMessage).toBeUndefined()
}) })
it('errorMessage returns the exception_message from execution_error', () => { it('errorMessage returns the exception_message from execution_error', () => {
const job: JobListItem = { const job: JobListItem = {
...createHistoryJob(0, 'prompt-id'), ...createHistoryJob(0, 'job-id'),
status: 'failed', status: 'failed',
execution_error: { execution_error: {
node_id: 'node-1', node_id: 'node-1',
@@ -217,7 +217,7 @@ describe('TaskItemImpl', () => {
}) })
it('executionError returns undefined when no execution_error', () => { it('executionError returns undefined when no execution_error', () => {
const job = createHistoryJob(0, 'prompt-id') const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job) const taskItem = new TaskItemImpl(job)
expect(taskItem.executionError).toBeUndefined() expect(taskItem.executionError).toBeUndefined()
}) })
@@ -234,7 +234,7 @@ describe('TaskItemImpl', () => {
current_outputs: {} current_outputs: {}
} }
const job: JobListItem = { const job: JobListItem = {
...createHistoryJob(0, 'prompt-id'), ...createHistoryJob(0, 'job-id'),
status: 'failed', status: 'failed',
execution_error: errorDetail execution_error: errorDetail
} }
@@ -292,9 +292,9 @@ describe('useQueueStore', () => {
expect(store.runningTasks).toHaveLength(1) expect(store.runningTasks).toHaveLength(1)
expect(store.pendingTasks).toHaveLength(2) expect(store.pendingTasks).toHaveLength(2)
expect(store.runningTasks[0].promptId).toBe('run-1') expect(store.runningTasks[0].jobId).toBe('run-1')
expect(store.pendingTasks[0].promptId).toBe('pend-2') expect(store.pendingTasks[0].jobId).toBe('pend-2')
expect(store.pendingTasks[1].promptId).toBe('pend-1') expect(store.pendingTasks[1].jobId).toBe('pend-1')
}) })
it('should load history tasks from API', async () => { it('should load history tasks from API', async () => {
@@ -307,8 +307,8 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(2) expect(store.historyTasks).toHaveLength(2)
expect(store.historyTasks[0].promptId).toBe('hist-1') expect(store.historyTasks[0].jobId).toBe('hist-1')
expect(store.historyTasks[1].promptId).toBe('hist-2') expect(store.historyTasks[1].jobId).toBe('hist-2')
}) })
it('should set loading state correctly', async () => { it('should set loading state correctly', async () => {
@@ -378,7 +378,7 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(1) expect(store.historyTasks).toHaveLength(1)
expect(store.historyTasks[0].promptId).toBe('prompt-uuid-aaa') expect(store.historyTasks[0].jobId).toBe('prompt-uuid-aaa')
const hist2 = createHistoryJob(51, 'prompt-uuid-bbb') const hist2 = createHistoryJob(51, 'prompt-uuid-bbb')
mockGetHistory.mockResolvedValue([hist2]) mockGetHistory.mockResolvedValue([hist2])
@@ -386,7 +386,7 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(1) expect(store.historyTasks).toHaveLength(1)
expect(store.historyTasks[0].promptId).toBe('prompt-uuid-bbb') expect(store.historyTasks[0].jobId).toBe('prompt-uuid-bbb')
expect(store.historyTasks[0].queueIndex).toBe(51) expect(store.historyTasks[0].queueIndex).toBe(51)
}) })
@@ -406,10 +406,10 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(2) expect(store.historyTasks).toHaveLength(2)
const promptIds = store.historyTasks.map((t) => t.promptId) const jobIds = store.historyTasks.map((t) => t.jobId)
expect(promptIds).toContain('second-prompt-at-101') expect(jobIds).toContain('second-prompt-at-101')
expect(promptIds).toContain('prompt-at-99') expect(jobIds).toContain('prompt-at-99')
expect(promptIds).not.toContain('first-prompt-at-100') expect(jobIds).not.toContain('first-prompt-at-100')
}) })
it('should handle multiple queueIndex collisions simultaneously', async () => { it('should handle multiple queueIndex collisions simultaneously', async () => {
@@ -430,13 +430,13 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(3) expect(store.historyTasks).toHaveLength(3)
const promptIds = store.historyTasks.map((t) => t.promptId) const jobIds = store.historyTasks.map((t) => t.jobId)
expect(promptIds).toEqual(['new-at-32', 'new-at-31', 'keep-at-30']) expect(jobIds).toEqual(['new-at-32', 'new-at-31', 'keep-at-30'])
}) })
}) })
describe('update() - history reconciliation', () => { describe('update() - history reconciliation', () => {
it('should keep existing items still on server (by promptId)', async () => { it('should keep existing items still on server (by jobId)', async () => {
const hist1 = createHistoryJob(10, 'existing-1') const hist1 = createHistoryJob(10, 'existing-1')
const hist2 = createHistoryJob(9, 'existing-2') const hist2 = createHistoryJob(9, 'existing-2')
@@ -452,9 +452,9 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(3) expect(store.historyTasks).toHaveLength(3)
expect(store.historyTasks.map((t) => t.promptId)).toContain('existing-1') expect(store.historyTasks.map((t) => t.jobId)).toContain('existing-1')
expect(store.historyTasks.map((t) => t.promptId)).toContain('existing-2') expect(store.historyTasks.map((t) => t.jobId)).toContain('existing-2')
expect(store.historyTasks.map((t) => t.promptId)).toContain('new-1') expect(store.historyTasks.map((t) => t.jobId)).toContain('new-1')
}) })
it('should remove items no longer on server', async () => { it('should remove items no longer on server', async () => {
@@ -472,7 +472,7 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(1) expect(store.historyTasks).toHaveLength(1)
expect(store.historyTasks[0].promptId).toBe('keep-me') expect(store.historyTasks[0].jobId).toBe('keep-me')
}) })
it('should add new items from server', async () => { it('should add new items from server', async () => {
@@ -490,8 +490,8 @@ describe('useQueueStore', () => {
await store.update() await store.update()
expect(store.historyTasks).toHaveLength(3) expect(store.historyTasks).toHaveLength(3)
expect(store.historyTasks.map((t) => t.promptId)).toContain('new-1') expect(store.historyTasks.map((t) => t.jobId)).toContain('new-1')
expect(store.historyTasks.map((t) => t.promptId)).toContain('new-2') expect(store.historyTasks.map((t) => t.jobId)).toContain('new-2')
}) })
it('should recreate TaskItemImpl when outputs_count changes', async () => { it('should recreate TaskItemImpl when outputs_count changes', async () => {
@@ -831,7 +831,7 @@ describe('useQueueStore', () => {
await secondUpdate await secondUpdate
expect(store.pendingTasks).toHaveLength(1) expect(store.pendingTasks).toHaveLength(1)
expect(store.pendingTasks[0].promptId).toBe('new-job') expect(store.pendingTasks[0].jobId).toBe('new-job')
resolveFirst!({ resolveFirst!({
Running: [], Running: [],
@@ -840,7 +840,7 @@ describe('useQueueStore', () => {
await firstUpdate await firstUpdate
expect(store.pendingTasks).toHaveLength(1) expect(store.pendingTasks).toHaveLength(1)
expect(store.pendingTasks[0].promptId).toBe('new-job') expect(store.pendingTasks[0].jobId).toBe('new-job')
}) })
it('should set isLoading to false only for the latest request', async () => { it('should set isLoading to false only for the latest request', async () => {
@@ -897,13 +897,13 @@ describe('useQueueStore', () => {
await secondUpdate await secondUpdate
expect(store.pendingTasks).toHaveLength(1) expect(store.pendingTasks).toHaveLength(1)
expect(store.pendingTasks[0].promptId).toBe('new-job') expect(store.pendingTasks[0].jobId).toBe('new-job')
expect(store.isLoading).toBe(false) expect(store.isLoading).toBe(false)
await expect(firstUpdate).rejects.toThrow('stale network error') await expect(firstUpdate).rejects.toThrow('stale network error')
expect(store.pendingTasks).toHaveLength(1) expect(store.pendingTasks).toHaveLength(1)
expect(store.pendingTasks[0].promptId).toBe('new-job') expect(store.pendingTasks[0].jobId).toBe('new-job')
expect(store.isLoading).toBe(false) expect(store.isLoading).toBe(false)
}) })
}) })

View File

@@ -309,14 +309,14 @@ export class TaskItemImpl {
} }
get key() { get key() {
return this.promptId + this.displayStatus return this.jobId + this.displayStatus
} }
get queueIndex() { get queueIndex() {
return this.job.priority return this.job.priority
} }
get promptId() { get jobId() {
return this.job.id return this.job.id
} }
@@ -405,7 +405,7 @@ export class TaskItemImpl {
if (!this.isHistory) { if (!this.isHistory) {
return this return this
} }
const jobDetail = await getJobDetail(this.promptId) const jobDetail = await getJobDetail(this.jobId)
if (!jobDetail?.outputs) { if (!jobDetail?.outputs) {
return this return this
@@ -421,7 +421,7 @@ export class TaskItemImpl {
} }
// Single fetch for both workflow and outputs (with caching) // Single fetch for both workflow and outputs (with caching)
const jobDetail = await getJobDetail(this.promptId) const jobDetail = await getJobDetail(this.jobId)
const workflowData = await extractWorkflow(jobDetail) const workflowData = await extractWorkflow(jobDetail)
if (!workflowData) { if (!workflowData) {
@@ -460,7 +460,7 @@ export class TaskItemImpl {
new TaskItemImpl( new TaskItemImpl(
{ {
...this.job, ...this.job,
id: `${this.promptId}-${i}` id: `${this.jobId}-${i}`
}, },
{ {
[output.nodeId]: { [output.nodeId]: {
@@ -527,13 +527,10 @@ export const useQueueStore = defineStore('queue', () => {
const appearedTasks = [...pendingTasks.value, ...runningTasks.value] const appearedTasks = [...pendingTasks.value, ...runningTasks.value]
const executionStore = useExecutionStore() const executionStore = useExecutionStore()
appearedTasks.forEach((task) => { appearedTasks.forEach((task) => {
const promptIdString = String(task.promptId) const jobIdString = String(task.jobId)
const workflowId = task.workflowId const workflowId = task.workflowId
if (workflowId && promptIdString) { if (workflowId && jobIdString) {
executionStore.registerPromptWorkflowIdMapping( executionStore.registerJobWorkflowIdMapping(jobIdString, workflowId)
promptIdString,
workflowId
)
} }
}) })
@@ -546,7 +543,7 @@ export const useQueueStore = defineStore('queue', () => {
...queue.Running.map((j) => j.id), ...queue.Running.map((j) => j.id),
...queue.Pending.map((j) => j.id) ...queue.Pending.map((j) => j.id)
]) ])
executionStore.reconcileInitializingPrompts(activeJobIds) executionStore.reconcileInitializingJobs(activeJobIds)
} }
// Sort by create_time descending and limit to maxItems // Sort by create_time descending and limit to maxItems
@@ -556,12 +553,12 @@ export const useQueueStore = defineStore('queue', () => {
// Reuse existing TaskItemImpl instances or create new // Reuse existing TaskItemImpl instances or create new
// Must recreate if outputs_count changed (e.g., API started returning it) // Must recreate if outputs_count changed (e.g., API started returning it)
const existingByPromptId = new Map( const existingByJobId = new Map(
currentHistory.map((impl) => [impl.promptId, impl]) currentHistory.map((impl) => [impl.jobId, impl])
) )
historyTasks.value = sortedHistory.map((job) => { historyTasks.value = sortedHistory.map((job) => {
const existing = existingByPromptId.get(job.id) const existing = existingByJobId.get(job.id)
if (!existing) return new TaskItemImpl(job) if (!existing) return new TaskItemImpl(job)
// Recreate if outputs_count changed to ensure lazy loading works // Recreate if outputs_count changed to ensure lazy loading works
if (existing.outputsCount !== (job.outputs_count ?? undefined)) { if (existing.outputsCount !== (job.outputs_count ?? undefined)) {
@@ -590,7 +587,7 @@ export const useQueueStore = defineStore('queue', () => {
} }
const deleteTask = async (task: TaskItemImpl) => { const deleteTask = async (task: TaskItemImpl) => {
await api.deleteItem(task.apiTaskType, task.promptId) await api.deleteItem(task.apiTaskType, task.jobId)
await update() await update()
} }

View File

@@ -43,7 +43,7 @@ export const iconForJobState = (state: JobState): string => {
const buildTitle = (task: TaskItemImpl, t: (k: string) => string): string => { const buildTitle = (task: TaskItemImpl, t: (k: string) => string): string => {
const prefix = t('g.job') const prefix = t('g.job')
const shortId = String(task.promptId ?? '').split('-')[0] const shortId = String(task.jobId ?? '').split('-')[0]
const idx = task.queueIndex const idx = task.queueIndex
if (typeof idx === 'number') return `${prefix} #${idx}` if (typeof idx === 'number') return `${prefix} #${idx}`
if (shortId) return `${prefix} ${shortId}` if (shortId) return `${prefix} ${shortId}`

View File

@@ -132,12 +132,10 @@ if (isDesktop) {
(newTasks, oldTasks) => { (newTasks, oldTasks) => {
// Report tasks that previously running but are now completed (i.e. in history) // Report tasks that previously running but are now completed (i.e. in history)
const oldRunningTaskIds = new Set( const oldRunningTaskIds = new Set(
oldTasks.filter((task) => task.isRunning).map((task) => task.promptId) oldTasks.filter((task) => task.isRunning).map((task) => task.jobId)
) )
newTasks newTasks
.filter( .filter((task) => oldRunningTaskIds.has(task.jobId) && task.isHistory)
(task) => oldRunningTaskIds.has(task.promptId) && task.isHistory
)
.forEach((task) => { .forEach((task) => {
electronAPI().Events.incrementUserProperty( electronAPI().Events.incrementUserProperty(
`execution:${task.displayStatus.toLowerCase()}`, `execution:${task.displayStatus.toLowerCase()}`,