refactor: encapsulate error extraction in TaskItemImpl getters (#7650)

## Summary
- Add `errorMessage` and `executionError` getters to `TaskItemImpl` that
extract error info from status messages
- Update `useJobErrorReporting` composable to use these getters instead
of standalone function
- Remove the standalone `extractExecutionError` function

This encapsulates error extraction within `TaskItemImpl`, preparing for
the Jobs API migration where the underlying data format will change but
the getter interface will remain stable.

## Test plan
- [x] All existing tests pass
- [x] New tests added for `TaskItemImpl.errorMessage` and
`TaskItemImpl.executionError` getters
- [x] TypeScript, lint, and knip checks pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7650-refactor-encapsulate-error-extraction-in-TaskItemImpl-getters-2ce6d73d365081caae33dcc7e1e07720)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
ric-yu
2026-01-15 20:11:22 -08:00
committed by GitHub
parent 089295606a
commit c0a649ef43
46 changed files with 1650 additions and 3067 deletions

View File

@@ -10,6 +10,7 @@ import type {
} from '@/platform/assets/schemas/assetSchema'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { IFuseOptions } from 'fuse.js'
import {
type TemplateInfo,
type WorkflowTemplates
@@ -31,16 +32,13 @@ import type {
ExecutionSuccessWsMessage,
ExtensionsResponse,
FeatureFlagsWsMessage,
HistoryTaskItem,
LogsRawResponse,
LogsWsMessage,
NotificationWsMessage,
PendingTaskItem,
ProgressStateWsMessage,
ProgressTextWsMessage,
ProgressWsMessage,
PromptResponse,
RunningTaskItem,
Settings,
StatusWsMessage,
StatusWsMessageStatus,
@@ -49,12 +47,19 @@ import type {
UserDataFullInfo,
PreviewMethod
} from '@/schemas/apiSchema'
import type {
JobDetail,
JobListItem
} from '@/platform/remote/comfyui/jobs/jobTypes'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type { AuthHeader } from '@/types/authTypes'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { fetchHistory } from '@/platform/remote/comfyui/history'
import type { IFuseOptions } from 'fuse.js'
import {
fetchHistory,
fetchJobDetail,
fetchQueue
} from '@/platform/remote/comfyui/jobs/fetchJobs'
interface QueuePromptRequestBody {
client_id: string
@@ -670,7 +675,6 @@ export class ComfyApi extends EventTarget {
case 'logs':
case 'b_preview':
case 'notification':
case 'asset_download':
this.dispatchCustomEvent(msg.type, msg.data)
break
case 'feature_flags':
@@ -893,53 +897,13 @@ export class ComfyApi extends EventTarget {
* @returns The currently running and queued items
*/
async getQueue(): Promise<{
Running: RunningTaskItem[]
Pending: PendingTaskItem[]
Running: JobListItem[]
Pending: JobListItem[]
}> {
try {
const res = await this.fetchApi('/queue')
const data = await res.json()
// Normalize queue tuple shape across backends:
// - Backend (V1): [idx, prompt_id, inputs, extra_data(object), outputs_to_execute(array)]
// - Cloud: [idx, prompt_id, inputs, outputs_to_execute(array), metadata(object{create_time})]
const normalizeQueuePrompt = (prompt: any): any => {
if (!Array.isArray(prompt)) return prompt
// Ensure 5-tuple
const p = prompt.slice(0, 5)
const fourth = p[3]
const fifth = p[4]
// Cloud shape: 4th is array, 5th is metadata object
if (
Array.isArray(fourth) &&
fifth &&
typeof fifth === 'object' &&
!Array.isArray(fifth)
) {
const meta: any = fifth
const extraData = { ...meta }
return [p[0], p[1], p[2], extraData, fourth]
}
// V1 shape already: return as-is
return p
}
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt: any) => {
const np = normalizeQueuePrompt(prompt)
return {
taskType: 'Running',
prompt: np,
// prompt[1] is the prompt id
remove: { name: 'Cancel', cb: () => api.interrupt(np[1]) }
}
}),
Pending: data.queue_pending.map((prompt: any) => ({
taskType: 'Pending',
prompt: normalizeQueuePrompt(prompt)
}))
}
return await fetchQueue(this.fetchApi.bind(this))
} catch (error) {
console.error(error)
console.error('Failed to fetch queue:', error)
return { Running: [], Pending: [] }
}
}
@@ -951,7 +915,7 @@ export class ComfyApi extends EventTarget {
async getHistory(
max_items: number = 200,
options?: { offset?: number }
): Promise<{ History: HistoryTaskItem[] }> {
): Promise<JobListItem[]> {
try {
return await fetchHistory(
this.fetchApi.bind(this),
@@ -960,10 +924,19 @@ export class ComfyApi extends EventTarget {
)
} catch (error) {
console.error(error)
return { History: [] }
return []
}
}
/**
* Gets detailed job info including outputs and workflow
* @param jobId The job/prompt ID
* @returns Full job details or undefined if not found
*/
async getJobDetail(jobId: string): Promise<JobDetail | undefined> {
return fetchJobDetail(this.fetchApi.bind(this), jobId)
}
/**
* Gets system & device stats
* @returns System stats such as python version, OS, per device info
@@ -1273,29 +1246,6 @@ export class ComfyApi extends EventTarget {
}
}
/**
* Gets the Fuse options from the server.
*
* @returns The Fuse options, or null if not found or invalid
*/
async getFuseOptions(): Promise<IFuseOptions<TemplateInfo> | null> {
try {
const res = await axios.get(
this.fileURL('/templates/fuse_options.json'),
{
headers: {
'Content-Type': 'application/json'
}
}
)
const contentType = res.headers['content-type']
return contentType?.includes('application/json') ? res.data : null
} catch (error) {
console.error('Error loading fuse options:', error)
return null
}
}
/**
* Gets the custom nodes i18n data from the server.
*
@@ -1331,6 +1281,24 @@ export class ComfyApi extends EventTarget {
getServerFeatures(): Record<string, unknown> {
return { ...this.serverFeatureFlags }
}
async getFuseOptions(): Promise<IFuseOptions<TemplateInfo> | null> {
try {
const res = await axios.get(
this.fileURL('/templates/fuse_options.json'),
{
headers: {
'Content-Type': 'application/json'
}
}
)
const contentType = res.headers['content-type']
return contentType?.includes('application/json') ? res.data : null
} catch (error) {
console.error('Error loading fuse options:', error)
return null
}
}
}
export const api = new ComfyApi()

View File

@@ -1,6 +1,6 @@
import { useSettingStore } from '@/platform/settings/settingStore'
import { WORKFLOW_ACCEPT_STRING } from '@/platform/workflow/core/types/formats'
import { type StatusWsMessageStatus, type TaskItem } from '@/schemas/apiSchema'
import { type StatusWsMessageStatus } from '@/schemas/apiSchema'
import { useDialogService } from '@/services/dialogService'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
@@ -33,6 +33,17 @@ type Props = {
type Children = Element[] | Element | string | string[]
/**
* @deprecated Legacy queue item structure from old history API.
* Will be removed when ComfyList is migrated to Jobs API.
*/
interface LegacyQueueItem {
prompt: [unknown, string, unknown, { extra_pnginfo: { workflow: unknown } }]
outputs?: Record<string, unknown>
meta?: Record<string, { display_node?: string }>
remove?: { name: string; cb: () => Promise<void> | void }
}
type ElementType<K extends string> = K extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[K]
: HTMLElement
@@ -259,29 +270,28 @@ class ComfyList {
$el('div.comfy-list-items', [
// @ts-expect-error fixme ts strict error
...(this.#reverse ? items[section].reverse() : items[section]).map(
(item: TaskItem) => {
(item: LegacyQueueItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction =
'remove' in item
? item.remove
: {
name: 'Delete',
cb: () => api.deleteItem(this.#type, item.prompt[1])
}
const removeAction = item.remove ?? {
name: 'Delete',
cb: () => api.deleteItem(this.#type, item.prompt[1])
}
return $el('div', { textContent: item.prompt[0] + ': ' }, [
$el('button', {
textContent: 'Load',
onclick: async () => {
await app.loadGraphData(
// @ts-expect-error fixme ts strict error
item.prompt[3].extra_pnginfo.workflow,
item.prompt[3].extra_pnginfo.workflow as Parameters<
typeof app.loadGraphData
>[0],
true,
false
)
if ('outputs' in item) {
if ('outputs' in item && item.outputs) {
app.nodeOutputs = {}
for (const [key, value] of Object.entries(item.outputs)) {
const realKey = item['meta']?.[key]?.display_node ?? key
// @ts-expect-error fixme ts strict error
app.nodeOutputs[realKey] = value
}
}