From 00141c47a4d53f7eeedab174c6985212e721cfe5 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 16 Jan 2026 01:36:31 +0100 Subject: [PATCH 1/7] fix(api): replace any types with proper TypeScript types - Define V1RawPrompt and CloudRawPrompt tuple types for queue responses - Export QueueIndex, PromptInputs, ExtraData, OutputsToExecute from apiSchema - Type normalizeQueuePrompt with proper discriminated union - Type #postItem body as Record - Type storeUserData data as BodyInit | Record | null - Type getCustomNodesI18n return as Record --- src/schemas/apiSchema.ts | 6 +++ src/scripts/api.ts | 84 +++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 1d2d5e80bfa..6b6cf2440c4 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -296,6 +296,12 @@ export type TaskPrompt = z.infer export type TaskStatus = z.infer export type TaskOutput = z.infer +// Individual TaskPrompt components for raw queue response handling +export type QueueIndex = z.infer +export type PromptInputs = z.infer +export type ExtraData = z.infer +export type OutputsToExecute = z.infer + // `/queue` export type RunningTaskItem = z.infer export type PendingTaskItem = z.infer diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 822773c0d66..b35d2f8f268 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -30,24 +30,30 @@ import type { ExecutionStartWsMessage, ExecutionSuccessWsMessage, ExtensionsResponse, + ExtraData, FeatureFlagsWsMessage, HistoryTaskItem, LogsRawResponse, LogsWsMessage, NotificationWsMessage, + OutputsToExecute, PendingTaskItem, + PreviewMethod, ProgressStateWsMessage, ProgressTextWsMessage, ProgressWsMessage, + PromptId, + PromptInputs, PromptResponse, + QueueIndex, RunningTaskItem, Settings, StatusWsMessage, StatusWsMessageStatus, SystemStats, + TaskPrompt, User, - UserDataFullInfo, - PreviewMethod + UserDataFullInfo } from '@/schemas/apiSchema' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' import type { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' @@ -899,42 +905,56 @@ export class ComfyApi extends EventTarget { 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] + // Raw queue prompt tuple types from different backends: + // - V1 Backend: [idx, prompt_id, inputs, extra_data, outputs_to_execute] + // - Cloud: [idx, prompt_id, inputs, outputs_to_execute, metadata] + type V1RawPrompt = [ + QueueIndex, + PromptId, + PromptInputs, + ExtraData, + OutputsToExecute + ] + type CloudRawPrompt = [ + QueueIndex, + PromptId, + PromptInputs, + OutputsToExecute, + Record + ] + type RawQueuePrompt = V1RawPrompt | CloudRawPrompt + + const normalizeQueuePrompt = (prompt: RawQueuePrompt): TaskPrompt => { + if (!Array.isArray(prompt)) return prompt as TaskPrompt + const fourth = prompt[3] + // Cloud shape: 4th is array (outputs), 5th is metadata object + if (Array.isArray(fourth)) { + const cloudPrompt = prompt as CloudRawPrompt + const extraData: ExtraData = { ...cloudPrompt[4] } + return [ + cloudPrompt[0], + cloudPrompt[1], + cloudPrompt[2], + extraData, + cloudPrompt[3] + ] } - // V1 shape already: return as-is - return p + // V1 shape already matches TaskPrompt + return prompt as V1RawPrompt } return { // Running action uses a different endpoint for cancelling - Running: data.queue_running.map((prompt: any) => { + Running: data.queue_running.map((prompt: RawQueuePrompt) => { const np = normalizeQueuePrompt(prompt) return { - taskType: 'Running', + taskType: 'Running' as const, prompt: np, // prompt[1] is the prompt id - remove: { name: 'Cancel', cb: () => api.interrupt(np[1]) } + remove: { name: 'Cancel' as const, cb: () => api.interrupt(np[1]) } } }), - Pending: data.queue_pending.map((prompt: any) => ({ - taskType: 'Pending', + Pending: data.queue_pending.map((prompt: RawQueuePrompt) => ({ + taskType: 'Pending' as const, prompt: normalizeQueuePrompt(prompt) })) } @@ -978,7 +998,7 @@ export class ComfyApi extends EventTarget { * @param {*} type The endpoint to post to * @param {*} body Optional POST data */ - async #postItem(type: string, body: any) { + async #postItem(type: string, body?: Record) { try { await this.fetchApi('/' + type, { method: 'POST', @@ -1101,7 +1121,7 @@ export class ComfyApi extends EventTarget { */ async storeUserData( file: string, - data: any, + data: BodyInit | Record | null, options: RequestInit & { overwrite?: boolean stringify?: boolean @@ -1118,7 +1138,7 @@ export class ComfyApi extends EventTarget { `/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}&full_info=${options.full_info}`, { method: 'POST', - body: options?.stringify ? JSON.stringify(data) : data, + body: options?.stringify ? JSON.stringify(data) : (data as BodyInit), ...options } ) @@ -1301,7 +1321,7 @@ export class ComfyApi extends EventTarget { * * @returns The custom nodes i18n data */ - async getCustomNodesI18n(): Promise> { + async getCustomNodesI18n(): Promise> { return (await axios.get(this.apiURL('/i18n'))).data } From 7f0f0305e6f06751100dd09236586d91cf2c62d3 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 16 Jan 2026 03:12:09 +0100 Subject: [PATCH 2/7] fix(types): remove all @ts-expect-error from groupNodeManage.ts - Add GroupNodeConfigEntry interface to LGraph.ts for config typing - Extend GroupNodeWorkflowData with title and widgets_values node props - Type config as Record instead of unknown - Export new types from litegraph barrel file - Fix all class properties with definite assignment assertions - Type all method parameters (changeTab, changeNode, changeGroup, etc.) - Fix event handlers with proper Event and CustomEvent types - Type storeModification, getEditElement, and build*Page methods - Fix save button callback with proper generic types for node ordering - Add override modifier to show method - Use optional chaining for node.recreate() call --- src/extensions/core/groupNode.ts | 5 +- src/extensions/core/groupNodeManage.ts | 464 ++++++++++++------------- src/lib/litegraph/src/LGraph.ts | 9 +- src/lib/litegraph/src/litegraph.ts | 2 + 4 files changed, 241 insertions(+), 239 deletions(-) diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index a7af7361a30..db543c30a04 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -368,7 +368,7 @@ export class GroupNodeConfig { } getNodeDef( - node: GroupNodeData + node: GroupNodeData | GroupNodeWorkflowData['nodes'][number] ): GroupNodeDef | ComfyNodeDef | null | undefined { if (node.type) { const def = globalDefs[node.type] @@ -386,7 +386,8 @@ export class GroupNodeConfig { let type: string | number | null = linksFrom[0]?.[0]?.[5] ?? null if (type === 'COMBO') { // Use the array items - const source = node.outputs?.[0]?.widget?.name + const output = node.outputs?.[0] as GroupNodeOutput | undefined + const source = output?.widget?.name const nodeIdx = linksFrom[0]?.[0]?.[2] if (source && nodeIdx != null) { const fromTypeName = this.nodeData.nodes[Number(nodeIdx)]?.type diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 0e91af317ff..0373715b745 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -1,9 +1,11 @@ import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants' -import { - type LGraphNode, - type LGraphNodeConstructor, - LiteGraph +import type { + GroupNodeConfigEntry, + GroupNodeWorkflowData, + LGraphNode, + LGraphNodeConstructor } from '@/lib/litegraph/src/litegraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { useToastStore } from '@/platform/updates/common/toastStore' import { type ComfyApp, app } from '../../scripts/app' @@ -15,15 +17,17 @@ import './groupNodeManage.css' const ORDER: symbol = Symbol() -// @ts-expect-error fixme ts strict error -function merge(target, source) { +function merge( + target: Record, + source: Record +): Record { if (typeof target === 'object' && typeof source === 'object') { for (const key in source) { const sv = source[key] - if (typeof sv === 'object') { - let tv = target[key] + if (typeof sv === 'object' && sv !== null) { + let tv = target[key] as Record | undefined if (!tv) tv = target[key] = {} - merge(tv, source[key]) + merge(tv as Record, sv as Record) } else { target[key] = sv } @@ -34,8 +38,7 @@ function merge(target, source) { } export class ManageGroupDialog extends ComfyDialog { - // @ts-expect-error fixme ts strict error - tabs: Record< + tabs!: Record< 'Inputs' | 'Outputs' | 'Widgets', { tab: HTMLAnchorElement; page: HTMLElement } > @@ -52,31 +55,22 @@ export class ManageGroupDialog extends ComfyDialog { > > > = {} - // @ts-expect-error fixme ts strict error - nodeItems: any[] + nodeItems!: HTMLLIElement[] app: ComfyApp - // @ts-expect-error fixme ts strict error - groupNodeType: LGraphNodeConstructor - groupNodeDef: any - groupData: any - - // @ts-expect-error fixme ts strict error - innerNodesList: HTMLUListElement - // @ts-expect-error fixme ts strict error - widgetsPage: HTMLElement - // @ts-expect-error fixme ts strict error - inputsPage: HTMLElement - // @ts-expect-error fixme ts strict error - outputsPage: HTMLElement - draggable: any - - get selectedNodeInnerIndex() { - // @ts-expect-error fixme ts strict error - return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex + groupNodeType!: LGraphNodeConstructor + groupData!: GroupNodeConfig + + innerNodesList!: HTMLUListElement + widgetsPage!: HTMLElement + inputsPage!: HTMLElement + outputsPage!: HTMLElement + draggable: DraggableList | undefined + + get selectedNodeInnerIndex(): number { + return +this.nodeItems[this.selectedNodeIndex!]!.dataset.nodeindex! } - // @ts-expect-error fixme ts strict error - constructor(app) { + constructor(app: ComfyApp) { super() this.app = app this.element = $el('dialog.comfy-group-manage', { @@ -84,19 +78,15 @@ export class ManageGroupDialog extends ComfyDialog { }) as HTMLDialogElement } - // @ts-expect-error fixme ts strict error - changeTab(tab) { + changeTab(tab: keyof ManageGroupDialog['tabs']): void { this.tabs[this.selectedTab].tab.classList.remove('active') this.tabs[this.selectedTab].page.classList.remove('active') - // @ts-expect-error fixme ts strict error this.tabs[tab].tab.classList.add('active') - // @ts-expect-error fixme ts strict error this.tabs[tab].page.classList.add('active') this.selectedTab = tab } - // @ts-expect-error fixme ts strict error - changeNode(index, force?) { + changeNode(index: number, force?: boolean): void { if (!force && this.selectedNodeIndex === index) return if (this.selectedNodeIndex != null) { @@ -122,43 +112,41 @@ export class ManageGroupDialog extends ComfyDialog { this.groupNodeType = LiteGraph.registered_node_types[ `${PREFIX}${SEPARATOR}` + this.selectedGroup ] as unknown as LGraphNodeConstructor - this.groupNodeDef = this.groupNodeType.nodeData - this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType) + this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)! } - // @ts-expect-error fixme ts strict error - changeGroup(group, reset = true) { + changeGroup(group: string, reset = true): void { this.selectedGroup = group this.getGroupData() const nodes = this.groupData.nodeData.nodes - // @ts-expect-error fixme ts strict error - this.nodeItems = nodes.map((n, i) => - $el( - 'li.draggable-item', - { - dataset: { - nodeindex: n.index + '' - }, - onclick: () => { - this.changeNode(i) - } - }, - [ - $el('span.drag-handle'), - $el( - 'div', - { - textContent: n.title ?? n.type + this.nodeItems = nodes.map( + (n, i) => + $el( + 'li.draggable-item', + { + dataset: { + nodeindex: n.index + '' }, - n.title - ? $el('span', { - textContent: n.type - }) - : [] - ) - ] - ) + onclick: () => { + this.changeNode(i) + } + }, + [ + $el('span.drag-handle'), + $el( + 'div', + { + textContent: n.title ?? n.type + }, + n.title + ? $el('span', { + textContent: n.type + }) + : [] + ) + ] + ) as HTMLLIElement ) this.innerNodesList.replaceChildren(...this.nodeItems) @@ -167,47 +155,46 @@ export class ManageGroupDialog extends ComfyDialog { this.selectedNodeIndex = null this.changeNode(0) } else { - const items = this.draggable.getAllItems() - // @ts-expect-error fixme ts strict error - let index = items.findIndex((item) => item.classList.contains('selected')) - if (index === -1) index = this.selectedNodeIndex + const items = this.draggable!.getAllItems() + let index = items.findIndex((item: Element) => + item.classList.contains('selected') + ) + if (index === -1) index = this.selectedNodeIndex! this.changeNode(index, true) } const ordered = [...nodes] this.draggable?.dispose() this.draggable = new DraggableList(this.innerNodesList, 'li') - this.draggable.addEventListener( - 'dragend', - // @ts-expect-error fixme ts strict error - ({ detail: { oldPosition, newPosition } }) => { - if (oldPosition === newPosition) return - ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]) - for (let i = 0; i < ordered.length; i++) { - this.storeModification({ - nodeIndex: ordered[i].index, - section: ORDER, - prop: 'order', - value: i - }) - } + this.draggable.addEventListener('dragend', (e: Event) => { + const { oldPosition, newPosition } = (e as CustomEvent).detail + if (oldPosition === newPosition) return + ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]) + for (let i = 0; i < ordered.length; i++) { + this.storeModification({ + nodeIndex: ordered[i].index, + section: ORDER, + prop: 'order', + value: i + }) } - ) + }) } storeModification(props: { nodeIndex?: number section: symbol prop: string - value: any + value: unknown }) { const { nodeIndex, section, prop, value } = props - // @ts-expect-error fixme ts strict error - const groupMod = (this.modifications[this.selectedGroup] ??= {}) - const nodesMod = (groupMod.nodes ??= {}) + const groupKey = this.selectedGroup! + const groupMod = (this.modifications[groupKey] ??= {}) + const nodesMod = ((groupMod as Record).nodes ??= + {}) as Record>> const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {}) const typeMod = (nodeMod[section] ??= {}) - if (typeof value === 'object') { + if (typeof value === 'object' && value !== null) { const objMod = (typeMod[prop] ??= {}) Object.assign(objMod, value) } else { @@ -215,35 +202,45 @@ export class ManageGroupDialog extends ComfyDialog { } } - // @ts-expect-error fixme ts strict error - getEditElement(section, prop, value, placeholder, checked, checkable = true) { - if (value === placeholder) value = '' - - const mods = - // @ts-expect-error fixme ts strict error - this.modifications[this.selectedGroup]?.nodes?.[ - this.selectedNodeInnerIndex - ]?.[section]?.[prop] - if (mods) { - if (mods.name != null) { - value = mods.name + getEditElement( + section: string, + prop: string | number, + value: unknown, + placeholder: string, + checked: boolean, + checkable = true + ): HTMLDivElement { + let displayValue = value === placeholder ? '' : value + + const groupKey = this.selectedGroup! + const mods = ( + this.modifications[groupKey] as Record | undefined + )?.nodes as + | Record< + number, + Record> + > + | undefined + const modEntry = mods?.[this.selectedNodeInnerIndex]?.[section]?.[prop] + if (modEntry) { + if (modEntry.name != null) { + displayValue = modEntry.name } - if (mods.visible != null) { - checked = mods.visible + if (modEntry.visible != null) { + checked = modEntry.visible } } return $el('div', [ $el('input', { - value, + value: displayValue as string, placeholder, type: 'text', - // @ts-expect-error fixme ts strict error - onchange: (e) => { + onchange: (e: Event) => { this.storeModification({ - section, - prop, - value: { name: e.target.value } + section: section as unknown as symbol, + prop: String(prop), + value: { name: (e.target as HTMLInputElement).value } }) } }), @@ -252,25 +249,23 @@ export class ManageGroupDialog extends ComfyDialog { type: 'checkbox', checked, disabled: !checkable, - // @ts-expect-error fixme ts strict error - onchange: (e) => { + onchange: (e: Event) => { this.storeModification({ - section, - prop, - value: { visible: !!e.target.checked } + section: section as unknown as symbol, + prop: String(prop), + value: { visible: !!(e.target as HTMLInputElement).checked } }) } }) ]) - ]) + ]) as HTMLDivElement } buildWidgetsPage() { const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex] const items = Object.keys(widgets ?? {}) - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[this.selectedGroup] + const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]! const config = type.config?.[this.selectedNodeInnerIndex]?.input this.widgetsPage.replaceChildren( ...items.map((oldName) => { @@ -289,28 +284,25 @@ export class ManageGroupDialog extends ComfyDialog { buildInputsPage() { const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex] const items = Object.keys(inputs ?? {}) - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[this.selectedGroup] + const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]! const config = type.config?.[this.selectedNodeInnerIndex]?.input - this.inputsPage.replaceChildren( - // @ts-expect-error fixme ts strict error - ...items - .map((oldName) => { - let value = inputs[oldName] - if (!value) { - return - } + const elements = items + .map((oldName) => { + const value = inputs[oldName] + if (!value) { + return null + } - return this.getEditElement( - 'input', - oldName, - value, - oldName, - config?.[oldName]?.visible !== false - ) - }) - .filter(Boolean) - ) + return this.getEditElement( + 'input', + oldName, + value, + oldName, + config?.[oldName]?.visible !== false + ) + }) + .filter((el): el is HTMLDivElement => el !== null) + this.inputsPage.replaceChildren(...elements) return !!items.length } @@ -323,38 +315,32 @@ export class ManageGroupDialog extends ComfyDialog { const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex] - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[this.selectedGroup] + const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]! const config = type.config?.[this.selectedNodeInnerIndex]?.output const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex] const checkable = node.type !== 'PrimitiveNode' - this.outputsPage.replaceChildren( - ...outputs - // @ts-expect-error fixme ts strict error - .map((type, slot) => { - const groupOutputIndex = groupOutputs?.[slot] - const oldName = innerNodeDef.output_name?.[slot] ?? type - let value = config?.[slot]?.name - const visible = config?.[slot]?.visible || groupOutputIndex != null - if (!value || value === oldName) { - value = '' - } - return this.getEditElement( - 'output', - slot, - value, - oldName, - visible, - checkable - ) - }) - .filter(Boolean) - ) + const elements = outputs.map((outputType: unknown, slot: number) => { + const groupOutputIndex = groupOutputs?.[slot] + const oldName = innerNodeDef?.output_name?.[slot] ?? String(outputType) + let value = config?.[slot]?.name + const visible = config?.[slot]?.visible || groupOutputIndex != null + if (!value || value === oldName) { + value = '' + } + return this.getEditElement( + 'output', + slot, + value, + oldName, + visible, + checkable + ) + }) + this.outputsPage.replaceChildren(...elements) return !!outputs.length } - // @ts-expect-error fixme ts strict error - show(type?) { + override show(type?: string): void { const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort( (a, b) => a.localeCompare(b) ) @@ -371,24 +357,27 @@ export class ManageGroupDialog extends ComfyDialog { this.outputsPage ]) - this.tabs = [ + type TabName = 'Inputs' | 'Widgets' | 'Outputs' + const tabEntries: [TabName, HTMLElement][] = [ ['Inputs', this.inputsPage], ['Widgets', this.widgetsPage], ['Outputs', this.outputsPage] - // @ts-expect-error fixme ts strict error - ].reduce((p, [name, page]: [string, HTMLElement]) => { - // @ts-expect-error fixme ts strict error - p[name] = { - tab: $el('a', { - onclick: () => { - this.changeTab(name) - }, - textContent: name - }), - page - } - return p - }, {}) as any + ] + this.tabs = tabEntries.reduce( + (p, [name, page]) => { + p[name] = { + tab: $el('a', { + onclick: () => { + this.changeTab(name) + }, + textContent: name + }) as HTMLAnchorElement, + page + } + return p + }, + {} as ManageGroupDialog['tabs'] + ) const outer = $el('div.comfy-group-manage-outer', [ $el('header', [ @@ -396,9 +385,8 @@ export class ManageGroupDialog extends ComfyDialog { $el( 'select', { - // @ts-expect-error fixme ts strict error - onchange: (e) => { - this.changeGroup(e.target.value) + onchange: (e: Event) => { + this.changeGroup((e.target as HTMLSelectElement).value) } }, groupNodes.map((g) => @@ -439,8 +427,7 @@ export class ManageGroupDialog extends ComfyDialog { `Are you sure you want to remove the node: "${this.selectedGroup}"` ) ) { - // @ts-expect-error fixme ts strict error - delete app.rootGraph.extra.groupNodes[this.selectedGroup] + delete app.rootGraph.extra.groupNodes![this.selectedGroup!] LiteGraph.unregisterNodeType( `${PREFIX}${SEPARATOR}` + this.selectedGroup ) @@ -454,97 +441,102 @@ export class ManageGroupDialog extends ComfyDialog { 'button.comfy-btn', { onclick: async () => { - let nodesByType - let recreateNodes = [] - const types = {} + type NodesByType = Record + let nodesByType: NodesByType | undefined + const recreateNodes: LGraphNode[] = [] + const types: Record = {} for (const g in this.modifications) { - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[g] - let config = (type.config ??= {}) - - let nodeMods = this.modifications[g]?.nodes + const groupNodeData = app.rootGraph.extra.groupNodes![g]! + let config = (groupNodeData.config ??= {}) + + type NodeMods = Record< + string, + Record> + > + let nodeMods = this.modifications[g]?.nodes as + | NodeMods + | undefined if (nodeMods) { const keys = Object.keys(nodeMods) - // @ts-expect-error fixme ts strict error - if (nodeMods[keys[0]][ORDER]) { + if (nodeMods[keys[0]]?.[ORDER]) { // If any node is reordered, they will all need sequencing - const orderedNodes = [] - const orderedMods = {} - const orderedConfig = {} + const orderedNodes: GroupNodeWorkflowData['nodes'] = [] + const orderedMods: NodeMods = {} + const orderedConfig: Record = + {} for (const n of keys) { - // @ts-expect-error fixme ts strict error - const order = nodeMods[n][ORDER].order - orderedNodes[order] = type.nodes[+n] - // @ts-expect-error fixme ts strict error + const order = (nodeMods[n][ORDER] as { order: number }) + .order + orderedNodes[order] = groupNodeData.nodes[+n] orderedMods[order] = nodeMods[n] orderedNodes[order].index = order } // Rewrite links - for (const l of type.links) { - // @ts-expect-error l[0]/l[2] used as node index - if (l[0] != null) l[0] = type.nodes[l[0]].index - // @ts-expect-error l[0]/l[2] used as node index - if (l[2] != null) l[2] = type.nodes[l[2]].index + for (const l of groupNodeData.links) { + if (l[0] != null) + l[0] = groupNodeData.nodes[l[0] as number].index! + if (l[2] != null) + l[2] = groupNodeData.nodes[l[2] as number].index! } // Rewrite externals - if (type.external) { - for (const ext of type.external) { + if (groupNodeData.external) { + for (const ext of groupNodeData.external) { if (ext[0] != null) { - // @ts-expect-error ext[0] used as node index - ext[0] = type.nodes[ext[0]].index + ext[0] = groupNodeData.nodes[ext[0] as number].index! } } } // Rewrite modifications for (const id of keys) { - // @ts-expect-error id used as node index - if (config[id]) { - // @ts-expect-error fixme ts strict error - orderedConfig[type.nodes[id].index] = config[id] + if (config[+id]) { + orderedConfig[groupNodeData.nodes[+id].index!] = + config[+id] } - // @ts-expect-error id used as config key - delete config[id] + delete config[+id] } - type.nodes = orderedNodes + groupNodeData.nodes = orderedNodes nodeMods = orderedMods - type.config = config = orderedConfig + groupNodeData.config = config = orderedConfig } - merge(config, nodeMods) + merge( + config as Record, + nodeMods as Record + ) } - // @ts-expect-error fixme ts strict error - types[g] = type + types[g] = groupNodeData if (!nodesByType) { - nodesByType = app.rootGraph.nodes.reduce((p, n) => { - // @ts-expect-error fixme ts strict error - p[n.type] ??= [] - // @ts-expect-error fixme ts strict error - p[n.type].push(n) - return p - }, {}) + nodesByType = app.rootGraph.nodes.reduce( + (p, n) => { + const nodeType = n.type ?? '' + p[nodeType] ??= [] + p[nodeType].push(n) + return p + }, + {} + ) } - // @ts-expect-error fixme ts strict error - const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g] - if (nodes) recreateNodes.push(...nodes) + const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g] + if (groupTypeNodes) recreateNodes.push(...groupTypeNodes) } await GroupNodeConfig.registerFromWorkflow(types, []) for (const node of recreateNodes) { - node.recreate() + node.recreate?.() } this.modifications = {} this.app.canvas.setDirty(true, true) - this.changeGroup(this.selectedGroup, false) + this.changeGroup(this.selectedGroup!, false) } }, 'Save' diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 04f2a4a4fe8..4f358303598 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -102,16 +102,23 @@ export interface LGraphConfig { links_ontop?: boolean } +export interface GroupNodeConfigEntry { + input?: Record + output?: Record +} + export interface GroupNodeWorkflowData { external: (number | string)[][] links: SerialisedLLinkArray[] nodes: { index?: number type?: string + title?: string inputs?: unknown[] outputs?: unknown[] + widgets_values?: unknown[] }[] - config?: Record + config?: Record } export interface LGraphExtra extends Dictionary { diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index 0b24eb47b80..59d62a84fca 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -104,6 +104,8 @@ export type { } from './interfaces' export { LGraph, + type GroupNodeConfigEntry, + type GroupNodeWorkflowData, type LGraphTriggerAction, type LGraphTriggerParam } from './LGraph' From 5bcf9e7be01231555da985b4a84d7be8702407d2 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 16 Jan 2026 03:19:07 +0100 Subject: [PATCH 3/7] fix(changeTracker): type nodeOutputs and prompt callback with proper types - Type nodeOutputs as Record - Import CanvasPointerEvent from litegraph - Type prompt callback value as string | number - Type prompt callback function as (v: string) => void - Type event parameter as CanvasPointerEvent --- src/scripts/changeTracker.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/scripts/changeTracker.ts b/src/scripts/changeTracker.ts index fa09ad49af0..4db14677aa7 100644 --- a/src/scripts/changeTracker.ts +++ b/src/scripts/changeTracker.ts @@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat' import * as jsondiffpatch from 'jsondiffpatch' import log from 'loglevel' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph' import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph' import { ComfyWorkflow, @@ -40,7 +41,7 @@ export class ChangeTracker { _restoringState: boolean = false ds?: { scale: number; offset: [number, number] } - nodeOutputs?: Record + nodeOutputs?: Record private subgraphState?: { navigation: string[] @@ -303,11 +304,11 @@ export class ChangeTracker { const prompt = LGraphCanvas.prototype.prompt LGraphCanvas.prototype.prompt = function ( title: string, - value: any, - callback: (v: any) => void, - event: any + value: string | number, + callback: (v: string) => void, + event: CanvasPointerEvent ) { - const extendedCallback = (v: any) => { + const extendedCallback = (v: string) => { callback(v) checkState() } From 7321c6e19ec6966c665c3d51cdaaa56e0bf7cd0f Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 16 Jan 2026 03:29:26 +0100 Subject: [PATCH 4/7] fix(types): type ComfyDialog and ComfyAsyncDialog properly ComfyDialog: - Type constructor buttons parameter as HTMLButtonElement[] | null - Type show method parameter as string | HTMLElement | HTMLElement[] - Use definite assignment for textElement ComfyAsyncDialog: - Make class generic with DialogAction type - Type resolve function, show/showModal return types - Fix button creation with proper HTMLButtonElement cast - Add generic static prompt method ManageGroupDialog: - Update show method signature to match base class - Extract nodeType from parameter for string comparisons --- src/extensions/core/groupNodeManage.ts | 11 +++++--- src/scripts/ui/components/asyncDialog.ts | 36 ++++++++++++------------ src/scripts/ui/dialog.ts | 8 ++---- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 0373715b745..bcff3717088 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -340,7 +340,10 @@ export class ManageGroupDialog extends ComfyDialog { return !!outputs.length } - override show(type?: string): void { + override show(groupNodeType?: string | HTMLElement | HTMLElement[]): void { + // Extract string type - this method repurposes the show signature + const nodeType = + typeof groupNodeType === 'string' ? groupNodeType : undefined const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort( (a, b) => a.localeCompare(b) ) @@ -392,7 +395,7 @@ export class ManageGroupDialog extends ComfyDialog { groupNodes.map((g) => $el('option', { textContent: g, - selected: `${PREFIX}${SEPARATOR}${g}` === type, + selected: `${PREFIX}${SEPARATOR}${g}` === nodeType, value: g }) ) @@ -551,8 +554,8 @@ export class ManageGroupDialog extends ComfyDialog { this.element.replaceChildren(outer) this.changeGroup( - type - ? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === type) ?? + nodeType + ? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === nodeType) ?? groupNodes[0]) : groupNodes[0] ) diff --git a/src/scripts/ui/components/asyncDialog.ts b/src/scripts/ui/components/asyncDialog.ts index 1da1a5a4efe..7f77a15d187 100644 --- a/src/scripts/ui/components/asyncDialog.ts +++ b/src/scripts/ui/components/asyncDialog.ts @@ -1,28 +1,28 @@ import { $el } from '../../ui' import { ComfyDialog } from '../dialog' -export class ComfyAsyncDialog extends ComfyDialog { - // @ts-expect-error fixme ts strict error - #resolve: (value: any) => void +type DialogAction = string | { value?: T; text: string } - constructor(actions?: Array) { +export class ComfyAsyncDialog< + T = string | null +> extends ComfyDialog { + #resolve!: (value: T | null) => void + + constructor(actions?: Array>) { super( 'dialog.comfy-dialog.comfyui-dialog', - // @ts-expect-error fixme ts strict error actions?.map((opt) => { - if (typeof opt === 'string') { - opt = { text: opt } - } + const action = typeof opt === 'string' ? { text: opt } : opt return $el('button.comfyui-button', { type: 'button', - textContent: opt.text, - onclick: () => this.close(opt.value ?? opt.text) - }) + textContent: action.text, + onclick: () => this.close((action.value ?? action.text) as T) + }) as HTMLButtonElement }) ) } - override show(html: string | HTMLElement | HTMLElement[]) { + override show(html: string | HTMLElement | HTMLElement[]): Promise { this.element.addEventListener('close', () => { this.close() }) @@ -34,7 +34,7 @@ export class ComfyAsyncDialog extends ComfyDialog { }) } - showModal(html: string | HTMLElement | HTMLElement[]) { + showModal(html: string | HTMLElement | HTMLElement[]): Promise { this.element.addEventListener('close', () => { this.close() }) @@ -47,22 +47,22 @@ export class ComfyAsyncDialog extends ComfyDialog { }) } - override close(result = null) { + override close(result: T | null = null) { this.#resolve(result) this.element.close() super.close() } - static async prompt({ + static async prompt({ title = null, message, actions }: { title: string | null message: string - actions: Array - }) { - const dialog = new ComfyAsyncDialog(actions) + actions: Array> + }): Promise { + const dialog = new ComfyAsyncDialog(actions) const content = [$el('span', message)] if (title) { content.unshift($el('h3', title)) diff --git a/src/scripts/ui/dialog.ts b/src/scripts/ui/dialog.ts index 23d43c2bd3d..12ca4a2e555 100644 --- a/src/scripts/ui/dialog.ts +++ b/src/scripts/ui/dialog.ts @@ -4,11 +4,10 @@ export class ComfyDialog< T extends HTMLElement = HTMLElement > extends EventTarget { element: T - // @ts-expect-error fixme ts strict error - textElement: HTMLElement + textElement!: HTMLElement #buttons: HTMLButtonElement[] | null - constructor(type = 'div', buttons = null) { + constructor(type = 'div', buttons: HTMLButtonElement[] | null = null) { super() this.#buttons = buttons this.element = $el(type + '.comfy-modal', { parent: document.body }, [ @@ -35,8 +34,7 @@ export class ComfyDialog< this.element.style.display = 'none' } - // @ts-expect-error fixme ts strict error - show(html) { + show(html: string | HTMLElement | HTMLElement[]): void { if (typeof html === 'string') { this.textElement.innerHTML = html } else { From c1d763a6fa200caf62801229fc20ea213b1323f9 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 16 Jan 2026 03:55:18 +0100 Subject: [PATCH 5/7] fix(types): address CodeRabbit review comments groupNodeManage.ts: - Add bounds checking before accessing node indices in link/external loops - Fix selectedNodeInnerIndex getter with proper runtime guards - Change storeModification section param to string | symbol, removing casts asyncDialog.ts: - Initialize #resolve with no-op function for defensive safety api.ts: - Add warning log for unexpected non-array queue prompts --- src/extensions/core/groupNodeManage.ts | 28 +++++++++++++++--------- src/scripts/api.ts | 5 ++++- src/scripts/ui/components/asyncDialog.ts | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index bcff3717088..f41180596e8 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -67,7 +67,11 @@ export class ManageGroupDialog extends ComfyDialog { draggable: DraggableList | undefined get selectedNodeInnerIndex(): number { - return +this.nodeItems[this.selectedNodeIndex!]!.dataset.nodeindex! + const index = this.selectedNodeIndex + if (index == null) throw new Error('No node selected') + const item = this.nodeItems[index] + if (!item?.dataset.nodeindex) throw new Error('Invalid node item') + return +item.dataset.nodeindex } constructor(app: ComfyApp) { @@ -183,7 +187,7 @@ export class ManageGroupDialog extends ComfyDialog { storeModification(props: { nodeIndex?: number - section: symbol + section: string | symbol prop: string value: unknown }) { @@ -238,7 +242,7 @@ export class ManageGroupDialog extends ComfyDialog { type: 'text', onchange: (e: Event) => { this.storeModification({ - section: section as unknown as symbol, + section, prop: String(prop), value: { name: (e.target as HTMLInputElement).value } }) @@ -251,7 +255,7 @@ export class ManageGroupDialog extends ComfyDialog { disabled: !checkable, onchange: (e: Event) => { this.storeModification({ - section: section as unknown as symbol, + section, prop: String(prop), value: { visible: !!(e.target as HTMLInputElement).checked } }) @@ -477,18 +481,22 @@ export class ManageGroupDialog extends ComfyDialog { } // Rewrite links + const nodesLen = groupNodeData.nodes.length for (const l of groupNodeData.links) { - if (l[0] != null) - l[0] = groupNodeData.nodes[l[0] as number].index! - if (l[2] != null) - l[2] = groupNodeData.nodes[l[2] as number].index! + const srcIdx = l[0] as number + const dstIdx = l[2] as number + if (srcIdx != null && srcIdx < nodesLen) + l[0] = groupNodeData.nodes[srcIdx].index! + if (dstIdx != null && dstIdx < nodesLen) + l[2] = groupNodeData.nodes[dstIdx].index! } // Rewrite externals if (groupNodeData.external) { for (const ext of groupNodeData.external) { - if (ext[0] != null) { - ext[0] = groupNodeData.nodes[ext[0] as number].index! + const extIdx = ext[0] as number + if (extIdx != null && extIdx < nodesLen) { + ext[0] = groupNodeData.nodes[extIdx].index! } } } diff --git a/src/scripts/api.ts b/src/scripts/api.ts index b35d2f8f268..e5f88aa2607 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -925,7 +925,10 @@ export class ComfyApi extends EventTarget { type RawQueuePrompt = V1RawPrompt | CloudRawPrompt const normalizeQueuePrompt = (prompt: RawQueuePrompt): TaskPrompt => { - if (!Array.isArray(prompt)) return prompt as TaskPrompt + if (!Array.isArray(prompt)) { + console.warn('Unexpected non-array queue prompt:', prompt) + return prompt as TaskPrompt + } const fourth = prompt[3] // Cloud shape: 4th is array (outputs), 5th is metadata object if (Array.isArray(fourth)) { diff --git a/src/scripts/ui/components/asyncDialog.ts b/src/scripts/ui/components/asyncDialog.ts index 7f77a15d187..d1f45a9deab 100644 --- a/src/scripts/ui/components/asyncDialog.ts +++ b/src/scripts/ui/components/asyncDialog.ts @@ -6,7 +6,7 @@ type DialogAction = string | { value?: T; text: string } export class ComfyAsyncDialog< T = string | null > extends ComfyDialog { - #resolve!: (value: T | null) => void + #resolve: (value: T | null) => void = () => {} constructor(actions?: Array>) { super( From 7712ada4950c2596680d14b05bb76da385ceaca7 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu Date: Sat, 17 Jan 2026 22:34:57 +0100 Subject: [PATCH 6/7] fix: add schema for custom nodes i18n and simplify storeUserData type - Add zCustomNodesI18n schema and CustomNodesI18n type to apiSchema.ts - Update getCustomNodesI18n to use new CustomNodesI18n type - Simplify storeUserData data parameter from BodyInit | Record | null to unknown --- src/schemas/apiSchema.ts | 3 +++ src/scripts/api.ts | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 64b0b5208fb..37833cf2fd3 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -13,6 +13,9 @@ export type PromptId = z.infer export const resultItemType = z.enum(['input', 'output', 'temp']) export type ResultItemType = z.infer +const zCustomNodesI18n = z.record(z.string(), z.unknown()) +export type CustomNodesI18n = z.infer + const zResultItem = z.object({ filename: z.string().optional(), subfolder: z.string().optional(), diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 9e096888ce7..41310b0bdec 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -22,6 +22,7 @@ import type { } from '@/platform/workflow/validation/schemas/workflowSchema' import type { AssetDownloadWsMessage, + CustomNodesI18n, EmbeddingsResponse, ExecutedWsMessage, ExecutingWsMessage, @@ -1074,7 +1075,7 @@ export class ComfyApi extends EventTarget { */ async storeUserData( file: string, - data: BodyInit | Record | null, + data: unknown, options: RequestInit & { overwrite?: boolean stringify?: boolean @@ -1251,7 +1252,7 @@ export class ComfyApi extends EventTarget { * * @returns The custom nodes i18n data */ - async getCustomNodesI18n(): Promise> { + async getCustomNodesI18n(): Promise { return (await axios.get(this.apiURL('/i18n'))).data } From 66199d48c8136871ecabcf705ab79ecb4357c2a6 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 20 Jan 2026 01:35:29 +0100 Subject: [PATCH 7/7] refactor: simplify merge function to reduce type casts --- src/extensions/core/groupNodeManage.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index f41180596e8..38bddb129e9 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -21,16 +21,16 @@ function merge( target: Record, source: Record ): Record { - if (typeof target === 'object' && typeof source === 'object') { - for (const key in source) { - const sv = source[key] - if (typeof sv === 'object' && sv !== null) { - let tv = target[key] as Record | undefined - if (!tv) tv = target[key] = {} - merge(tv as Record, sv as Record) - } else { - target[key] = sv + for (const key in source) { + const sv = source[key] + if (typeof sv === 'object' && sv !== null) { + let tv = target[key] as Record | undefined + if (!tv) { + tv = target[key] = {} } + merge(tv, sv as Record) + } else { + target[key] = sv } }