Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 62 additions & 23 deletions src/composables/usePaste.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {
} from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { isImageNode } from '@/utils/litegraphUtil'
import { pasteImageNode, usePaste } from './usePaste'
import { createNode, isImageNode } from '@/utils/litegraphUtil'
import { pasteImageNode, pasteImageNodes, usePaste } from './usePaste'

function createMockNode() {
return {
Expand Down Expand Up @@ -86,6 +86,7 @@ vi.mock('@/lib/litegraph/src/litegraph', () => ({
}))

vi.mock('@/utils/litegraphUtil', () => ({
createNode: vi.fn(),
isAudioNode: vi.fn(),
isImageNode: vi.fn(),
isVideoNode: vi.fn()
Expand All @@ -103,30 +104,28 @@ describe('pasteImageNode', () => {
)
})

it('should create new LoadImage node when no image node provided', () => {
it('should create new LoadImage node when no image node provided', async () => {
const mockNode = createMockNode()
vi.mocked(LiteGraph.createNode).mockReturnValue(
mockNode as unknown as LGraphNode
)
vi.mocked(createNode).mockResolvedValue(mockNode as unknown as LGraphNode)

const file = createImageFile()
const dataTransfer = createDataTransfer([file])

pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items)
await pasteImageNode(
mockCanvas as unknown as LGraphCanvas,
dataTransfer.items
)

expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage')
expect(mockNode.pos).toEqual([100, 200])
expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode)
expect(mockCanvas.graph!.change).toHaveBeenCalled()
expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadImage')
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
})
Comment on lines +107 to 121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prefer Partial<LGraphNode> casts for mock nodes.

Several tests cast mock objects via as unknown as LGraphNode. Use Partial<LGraphNode> as LGraphNode to make incomplete mocks explicit and safer. Based on learnings, prefer Partial<Interface> as Interface for partial mocks in tests.

♻️ Proposed refactor
-    vi.mocked(createNode).mockResolvedValue(mockNode as unknown as LGraphNode)
+    vi.mocked(createNode).mockResolvedValue(
+      mockNode as Partial<LGraphNode> as LGraphNode
+    )
@@
-    const mockNode = {
-      is_selected: true,
-      pasteFile: vi.fn(),
-      pasteFiles: vi.fn()
-    } as unknown as Partial<LGraphNode> as LGraphNode
+    const mockNode = {
+      is_selected: true,
+      pasteFile: vi.fn(),
+      pasteFiles: vi.fn()
+    } as Partial<LGraphNode> as LGraphNode

Also applies to: 190-212, 237-250, 302-309

🤖 Prompt for AI Agents
In `@src/composables/usePaste.test.ts` around lines 107 - 121, Replace unsafe
casts like "as unknown as LGraphNode" used for test mocks with explicit partial
casts "Partial<LGraphNode> as LGraphNode"; update occurrences where
createMockNode returns mockNode and anywhere tests call createNode and assert
mockNode.pasteFile (including the other test blocks referenced) so the mocks are
typed as Partial<LGraphNode> as LGraphNode (e.g., change the mockNode
declaration/return and any similar casts around createMockNode, mockNode,
createNode, and pasteImageNode uses).


it('should use existing image node when provided', () => {
it('should use existing image node when provided', async () => {
const mockNode = createMockNode()
const file = createImageFile()
const dataTransfer = createDataTransfer([file])

pasteImageNode(
await pasteImageNode(
mockCanvas as unknown as LGraphCanvas,
dataTransfer.items,
mockNode as unknown as LGraphNode
Expand All @@ -136,13 +135,13 @@ describe('pasteImageNode', () => {
expect(mockNode.pasteFiles).toHaveBeenCalledWith([file])
})

it('should handle multiple image files', () => {
it('should handle multiple image files', async () => {
const mockNode = createMockNode()
const file1 = createImageFile('test1.png')
const file2 = createImageFile('test2.jpg', 'image/jpeg')
const dataTransfer = createDataTransfer([file1, file2])

pasteImageNode(
await pasteImageNode(
mockCanvas as unknown as LGraphCanvas,
dataTransfer.items,
mockNode as unknown as LGraphNode
Expand All @@ -152,11 +151,11 @@ describe('pasteImageNode', () => {
expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2])
})

it('should do nothing when no image files present', () => {
it('should do nothing when no image files present', async () => {
const mockNode = createMockNode()
const dataTransfer = createDataTransfer()

pasteImageNode(
await pasteImageNode(
mockCanvas as unknown as LGraphCanvas,
dataTransfer.items,
mockNode as unknown as LGraphNode
Expand All @@ -166,13 +165,13 @@ describe('pasteImageNode', () => {
expect(mockNode.pasteFiles).not.toHaveBeenCalled()
})

it('should filter non-image items', () => {
it('should filter non-image items', async () => {
const mockNode = createMockNode()
const imageFile = createImageFile()
const textFile = new File([''], 'test.txt', { type: 'text/plain' })
const dataTransfer = createDataTransfer([textFile, imageFile])

pasteImageNode(
await pasteImageNode(
mockCanvas as unknown as LGraphCanvas,
dataTransfer.items,
mockNode as unknown as LGraphNode
Expand All @@ -183,6 +182,48 @@ describe('pasteImageNode', () => {
})
})

describe('pasteImageNodes', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('should create multiple nodes for multiple files', async () => {
const mockNode1 = createMockNode()
const mockNode2 = createMockNode()
vi.mocked(createNode)
.mockResolvedValueOnce(mockNode1 as unknown as LGraphNode)
.mockResolvedValueOnce(mockNode2 as unknown as LGraphNode)

const file1 = createImageFile('test1.png')
const file2 = createImageFile('test2.jpg', 'image/jpeg')
const fileList = createDataTransfer([file1, file2]).files

const result = await pasteImageNodes(
mockCanvas as unknown as LGraphCanvas,
fileList
)

expect(createNode).toHaveBeenCalledTimes(2)
expect(createNode).toHaveBeenNthCalledWith(1, mockCanvas, 'LoadImage')
expect(createNode).toHaveBeenNthCalledWith(2, mockCanvas, 'LoadImage')
expect(mockNode1.pasteFile).toHaveBeenCalledWith(file1)
expect(mockNode2.pasteFile).toHaveBeenCalledWith(file2)
expect(result).toEqual([mockNode1, mockNode2])
})

it('should handle empty file list', async () => {
const fileList = createDataTransfer([]).files

const result = await pasteImageNodes(
mockCanvas as unknown as LGraphCanvas,
fileList
)

expect(createNode).not.toHaveBeenCalled()
expect(result).toEqual([])
})
})

describe('usePaste', () => {
beforeEach(() => {
vi.clearAllMocks()
Expand All @@ -195,9 +236,7 @@ describe('usePaste', () => {

it('should handle image paste', async () => {
const mockNode = createMockNode()
vi.mocked(LiteGraph.createNode).mockReturnValue(
mockNode as unknown as LGraphNode
)
vi.mocked(createNode).mockResolvedValue(mockNode as unknown as LGraphNode)

usePaste()

Expand All @@ -207,7 +246,7 @@ describe('usePaste', () => {
document.dispatchEvent(event)

await vi.waitFor(() => {
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage')
expect(createNode).toHaveBeenCalledWith(mockCanvas, 'LoadImage')
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
})
})
Expand Down
74 changes: 59 additions & 15 deletions src/composables/usePaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,42 @@ import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/w
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import { createNode, isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'

export function cloneDataTransfer(original: DataTransfer): DataTransfer {
const persistent = new DataTransfer();

// Copy string data
for (const type of original.types) {
const data = original.getData(type);
if (data) {
persistent.setData(type, data);
}
}

// Copy files
for (const file of original.files) {
persistent.items.add(file);
}

// Also handle any file-kind items that might not be in .files
for (const item of original.items) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
persistent.items.add(file);
}
}
}
Comment on lines +23 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Duplicate files added when cloning DataTransfer.

The function copies files from original.files (lines 24-26), then again from original.items where kind === 'file' (lines 29-35). Since .files already contains all file-kind items, this results in each file being added twice to the cloned DataTransfer.

🐛 Proposed fix: Remove the redundant files loop
-  // Copy files
-  for (const file of original.files) {
-    persistent.items.add(file);
-  }
-
-  // Also handle any file-kind items that might not be in .files
+  // Copy file-kind items
   for (const item of original.items) {
     if (item.kind === 'file') {
       const file = item.getAsFile();
       if (file) {
         persistent.items.add(file);
       }
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Copy files
for (const file of original.files) {
persistent.items.add(file);
}
// Also handle any file-kind items that might not be in .files
for (const item of original.items) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
persistent.items.add(file);
}
}
}
// Copy file-kind items
for (const item of original.items) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
persistent.items.add(file);
}
}
}
🤖 Prompt for AI Agents
In `@src/composables/usePaste.ts` around lines 23 - 36, The code currently adds
files twice by iterating original.files and then iterating original.items for
kind==='file'; remove the first loop that iterates original.files (the for
(const file of original.files) { persistent.items.add(file); } block) and rely
on the existing original.items loop that checks item.kind === 'file' and uses
item.getAsFile() before calling persistent.items.add(file), so each file is only
added once.


// Preserve dropEffect and effectAllowed
persistent.dropEffect = original.dropEffect;
persistent.effectAllowed = original.effectAllowed;

return persistent;
}

function pasteClipboardItems(data: DataTransfer): boolean {
const rawData = data.getData('text/html')
const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1]
Expand Down Expand Up @@ -48,27 +81,37 @@ function pasteItemsOnNode(
)
}

export function pasteImageNode(
export async function pasteImageNode(
canvas: LGraphCanvas,
items: DataTransferItemList,
imageNode: LGraphNode | null = null
): void {
const {
graph,
graph_mouse: [posX, posY]
} = canvas

): Promise<LGraphNode | null> {
if (!imageNode) {
// No image node selected: add a new one
const newNode = LiteGraph.createNode('LoadImage')
if (newNode) {
newNode.pos = [posX, posY]
imageNode = graph?.add(newNode) ?? null
}
graph?.change()
imageNode = await createNode(canvas, 'LoadImage')
}

pasteItemsOnNode(items, imageNode, 'image')
return imageNode
}

export async function pasteImageNodes(
canvas: LGraphCanvas,
fileList: FileList
): Promise<LGraphNode[]> {
const nodes: LGraphNode[] = [];

for (const file of fileList) {
const transfer = new DataTransfer()
transfer.items.add(file)
const imageNode = await pasteImageNode(canvas, transfer.items)

if (imageNode) {
nodes.push(imageNode)
}
}

return nodes
}

/**
Expand All @@ -93,6 +136,7 @@ export const usePaste = () => {
const { graph } = canvas
let data: DataTransfer | string | null = e.clipboardData
if (!data) throw new Error('No clipboard data on clipboard event')
data = cloneDataTransfer(data)

const { items } = data

Expand All @@ -114,7 +158,7 @@ export const usePaste = () => {
// Look for image paste data
for (const item of items) {
if (item.type.startsWith('image/')) {
pasteImageNode(canvas as LGraphCanvas, items, imageNode)
await pasteImageNode(canvas as LGraphCanvas, items, imageNode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle errors from async pasteImageNode to avoid unhandled rejections.

Line 127 now awaits an async path that can throw (e.g., node creation failure). Wrap the await in a try/catch and route errors through your existing UI error reporting mechanism; ensure any user-visible message uses i18n.

🤖 Prompt for AI Agents
In `@src/composables/usePaste.ts` at line 127, Awaiting pasteImageNode can throw
and must be wrapped in try/catch; modify the call in the function that currently
does "await pasteImageNode(canvas as LGraphCanvas, items, imageNode)" to catch
errors, send them to the existing UI error reporting function (use the project's
error reporting helper) and present a user-visible message via i18n (use the
current i18n instance/keys). Ensure the catch logs/debug-reports the original
error and shows a localized message to the user.

return
} else if (item.type.startsWith('video/')) {
if (!videoNode) {
Expand Down
51 changes: 48 additions & 3 deletions src/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
} from '@/utils/graphTraversalUtil'
import {
executeWidgetsCallback,
createNode,
fixLinkInputSlots,
isImageNode
} from '@/utils/litegraphUtil'
Expand All @@ -97,7 +98,7 @@ import { type ComfyWidgetConstructor } from './widgets'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { extractFileFromDragEvent } from '@/utils/eventUtils'
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
import { pasteImageNode } from '@/composables/usePaste'
import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste'

export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'

Expand Down Expand Up @@ -542,7 +543,13 @@ export class ComfyApp {
const workspace = useWorkspaceStore()
try {
workspace.spinner = true
await this.handleFile(fileMaybe, 'file_drop')
if (fileMaybe instanceof File) {
await this.handleFile(fileMaybe, 'file_drop')
}

if (fileMaybe instanceof FileList) {
await this.handleFileList(fileMaybe)
}
} finally {
workspace.spinner = false
}
Expand Down Expand Up @@ -1437,7 +1444,9 @@ export class ComfyApp {
if (file.type.startsWith('image')) {
const transfer = new DataTransfer()
transfer.items.add(file)
pasteImageNode(this.canvas, transfer.items)
console.log('transfer items', transfer.items)
const imageNode = await createNode(this.canvas, 'LoadImage')
await pasteImageNode(this.canvas, transfer.items, imageNode)
return
}

Expand Down Expand Up @@ -1516,6 +1525,42 @@ export class ComfyApp {
this.showErrorOnFileLoad(file)
}


/**
* Loads multiple files and connects to a batch node
* @param {FileList} fileList
*/
async handleFileList(fileList: FileList) {
if (fileList[0].type.startsWith('image')) {
const imageNodes = await pasteImageNodes(this.canvas, fileList)
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
if (!batchImagesNode) return

this.positionNodes(imageNodes, batchImagesNode)

Array.from(imageNodes).forEach((imageNode, index) => {
imageNode.connect(0, batchImagesNode, index)
})
}
}

/**
* Positions batched nodes in drag and drop
* @param nodes
* @param batchNode
*/
positionNodes(nodes: LGraphNode[], batchNode: LGraphNode) {
const [x, y, width, height] = nodes[0].getBounding()
batchNode.pos = [ x + width + 100, y + 30 ]
Comment on lines +1533 to +1554
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against empty FileList/imageNodes before indexing.

positionNodes reads nodes[0] unconditionally. If fileList is empty or pasteImageNodes returns an empty array (e.g., node creation failure), this will throw and abort the drop flow. Add early returns before accessing nodes[0].

🐛 Proposed fix
   async handleFileList(fileList: FileList) {
-    if (fileList[0].type.startsWith('image')) {
+    if (!fileList.length) return
+    if (fileList[0].type.startsWith('image')) {
       const imageNodes = await pasteImageNodes(this.canvas, fileList)
+      if (!imageNodes.length) return
       const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
       if (!batchImagesNode) return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async handleFileList(fileList: FileList) {
if (fileList[0].type.startsWith('image')) {
const imageNodes = await pasteImageNodes(this.canvas, fileList)
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
if (!batchImagesNode) return
this.positionNodes(imageNodes, batchImagesNode)
Array.from(imageNodes).forEach((imageNode, index) => {
imageNode.connect(0, batchImagesNode, index)
})
}
}
/**
* Positions batched nodes in drag and drop
* @param nodes
* @param batchNode
*/
positionNodes(nodes: LGraphNode[], batchNode: LGraphNode) {
const [x, y, width, height] = nodes[0].getBounding()
batchNode.pos = [ x + width + 100, y + 30 ]
async handleFileList(fileList: FileList) {
if (!fileList.length) return
if (fileList[0].type.startsWith('image')) {
const imageNodes = await pasteImageNodes(this.canvas, fileList)
if (!imageNodes.length) return
const batchImagesNode = await createNode(this.canvas, 'BatchImagesNode')
if (!batchImagesNode) return
this.positionNodes(imageNodes, batchImagesNode)
Array.from(imageNodes).forEach((imageNode, index) => {
imageNode.connect(0, batchImagesNode, index)
})
}
}
/**
* Positions batched nodes in drag and drop
* `@param` nodes
* `@param` batchNode
*/
positionNodes(nodes: LGraphNode[], batchNode: LGraphNode) {
const [x, y, width, height] = nodes[0].getBounding()
batchNode.pos = [ x + width + 100, y + 30 ]
}
🤖 Prompt for AI Agents
In `@src/scripts/app.ts` around lines 1531 - 1552, The code accesses nodes[0] in
positionNodes without guarding for empty arrays which can throw when
pasteImageNodes returns no nodes or fileList is empty; update handleFileList to
check fileList.length and that imageNodes is non-empty (and batchImagesNode
exists) before calling positionNodes, and add an early-return at the start of
positionNodes to return if nodes is falsy or nodes.length === 0; reference
functions/variables: handleFileList, pasteImageNodes, createNode, imageNodes,
batchImagesNode, and positionNodes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do that in the method that calls this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the drop handler that calls handleFileList and look for guards
rg -nP -A15 -B5 'handleFileList' src/scripts/app.ts

# Also check if there are any length checks before calling positionNodes
rg -nP -A3 -B3 'positionNodes' src/scripts/app.ts

# Check the implementation of pasteImageNodes to see if it can return empty array
rg -nP -A10 'export.*pasteImageNodes' src/composables/usePaste.ts

Length of output: 2381


🏁 Script executed:

#!/bin/bash
# Check extractFileFromDragEvent to see if it can return empty FileList
rg -nP -A30 'export.*extractFileFromDragEvent' src/utils/eventUtils.ts

Length of output: 1028


@JemiloII, I've reviewed the code flow and I don't see guards for empty arrays in the calling method. Here's what I found:

Drop handler (line 550-551):

if (fileMaybe instanceof FileList) {
  await this.handleFileList(fileMaybe)
}

This only checks the type, not the length.

The actual issue is in handleFileList itself:

  1. Line 1534: fileList[0].type.startsWith('image') - would throw if fileList is empty
  2. Line 1535: pasteImageNodes can return an empty array if all node creations fail (e.g., if createNode returns null for all files)
  3. Line 1539: positionNodes(imageNodes, ...) is called without checking if imageNodes is empty
  4. Line 1553 in positionNodes: nodes[0].getBounding() throws if nodes is empty

While extractFileFromDragEvent currently won't return an empty FileList (it requires files.length > 1), the more critical issue is that pasteImageNodes can legitimately return an empty array if node creation fails.

The suggested guards are needed to prevent runtime errors when imageNodes ends up empty.


🧬 Code Graph Analysis Results

File: src/utils/graphTraversalUtil.ts (lines 285-307)

export function getNodeByExecutionId(
  rootGraph: LGraph,
  executionId: string
): LGraphNode | null {
  if (!rootGraph) return null

  const localNodeId = getLocalNodeIdFromExecutionId(executionId)
  if (!localNodeId) return null

  const subgraphPath = getSubgraphPathFromExecutionId(executionId)

  // If no subgraph path, it's in the root graph
  if (subgraphPath.length === 0) {
    return rootGraph.getNodeById(localNodeId) || null
  }

  // Traverse to the target subgraph
  const targetGraph = traverseSubgraphPath(rootGraph, subgraphPath)
  if (!targetGraph) return null

  // Get the node from the target graph
  return targetGraph.getNodeById(localNodeId) || null
}

File: src/workbench/utils/modelMetadataUtil.ts (lines 22-51)

export function getSelectedModelsMetadata(node: {
  type: string
  widgets_values?: unknown[] | Record<string, unknown>
  properties?: { models?: ModelFile[] }
}): ModelFile[] | undefined {
  try {
    if (!node.properties?.models?.length) return
    if (!node.widgets_values) return

    const widgetValues = Array.isArray(node.widgets_values)
      ? node.widgets_values
      : Object.values(node.widgets_values)

    if (!widgetValues.length) return

    const stringWidgetValues = new Set<string>()
    for (const widgetValue of widgetValues) {
      if (typeof widgetValue === 'string' && widgetValue.trim()) {
        stringWidgetValues.add(widgetValue)
      }
    }

    // Return the node's models that are present in the widget values
    return node.properties.models.filter((model) =>
      stringWidgetValues.has(model.name)
    )
  } catch (error) {
    console.error('Error filtering models by current selection:', error)
  }
}

File: src/composables/usePaste.ts (lines 84-96)

export async function pasteImageNode(
  canvas: LGraphCanvas,
  items: DataTransferItemList,
  imageNode: LGraphNode | null = null
): Promise<LGraphNode | null> {
  if (!imageNode) {
    // No image node selected: add a new one
    imageNode = await createNode(canvas, 'LoadImage')
  }

  pasteItemsOnNode(items, imageNode, 'image')
  return imageNode
}

File: src/composables/usePaste.ts (lines 98-115)

export async function pasteImageNodes(
  canvas: LGraphCanvas,
  fileList: FileList
): Promise<LGraphNode[]> {
  const nodes: LGraphNode[] = [];

  for (const file of fileList) {
    const transfer = new DataTransfer()
    transfer.items.add(file)
    const imageNode = await pasteImageNode(canvas, transfer.items)

    if (imageNode) {
      nodes.push(imageNode)
    }
  }

  return nodes
}

File: src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.ts (lines 15-166)

export function ensureCorrectLayoutScale(
  renderer: RendererType = 'LG',
  targetGraph?: LGraph
) {
  const autoScaleLayoutSetting = useSettingStore().get(
    'Comfy.VueNodes.AutoScaleLayout'
  )

  if (!autoScaleLayoutSetting) return

  const canvas = comfyApp.canvas
  const graph = targetGraph ?? canvas?.graph

  if (!graph?.nodes) return

  const { shouldRenderVueNodes } = useVueFeatureFlags()

  const needsUpscale = renderer === 'LG' && shouldRenderVueNodes.value
  const needsDownscale = renderer === 'Vue' && !shouldRenderVueNodes.value

  if (!needsUpscale && !needsDownscale) {
    // Don't scale, but ensure workflowRendererVersion is set for future checks
    graph.extra.workflowRendererVersion ??= renderer
    return
  }

  const lgBounds = createBounds(graph.nodes)

  if (!lgBounds) return

  const [originX, originY] = lgBounds

  const lgNodesById = new Map(graph.nodes.map((node) => [node.id, node]))

  const yjsMoveNodeUpdates: NodeBoundsUpdate[] = []

  const scaleFactor = needsUpscale ? SCALE_FACTOR : 1 / SCALE_FACTOR

  const onActiveGraph = !targetGraph || targetGraph === canvas?.graph

  //TODO: once we remove the need for LiteGraph.NODE_TITLE_HEIGHT in vue nodes we nned to remove everything here.
  for (const node of graph.nodes) {
    const lgNode = lgNodesById.get(node.id)
    if (!lgNode) continue

    const [oldX, oldY] = lgNode.pos

    const relativeX = oldX - originX
    const relativeY = oldY - originY

    const scaledX = originX + relativeX * scaleFactor
    const scaledY = originY + relativeY * scaleFactor

    const scaledWidth = lgNode.width * scaleFactor

    const scaledHeight = needsUpscale
      ? lgNode.size[1] * scaleFactor + LiteGraph.NODE_TITLE_HEIGHT
      : (lgNode.size[1] - LiteGraph.NODE_TITLE_HEIGHT) * scaleFactor

    // Directly update LiteGraph node to ensure immediate consistency
    // Dont need to reference vue directly because the pos and dims are already in yjs
    lgNode.pos[0] = scaledX
    lgNode.pos[1] = scaledY
    lgNode.size[0] = scaledWidth
    lgNode.size[1] = scaledHeight

    // Track updates for layout store (only if this is the active graph)
    if (onActiveGraph) {
      yjsMoveNodeUpdates.push({
        nodeId: String(lgNode.id),
        bounds: {
          x: scaledX,
          y: scaledY,
          width: scaledWidth,
          height: scaledHeight
        }
      })
    }
  }

  if (onActiveGraph && yjsMoveNodeUpdates.length > 0) {
    layoutStore.batchUpdateNodeBounds(yjsMoveNodeUpdates)
  }

  for (const reroute of graph.reroutes.values()) {
    const [oldX, oldY] = reroute.pos

    const relativeX = oldX - originX
    const relativeY = oldY - originY

    const scaledX = originX + relativeX * scaleFactor
    const scaledY = originY + relativeY * scaleFactor

    reroute.pos = [scaledX, scaledY]

    if (onActiveGraph && shouldRenderVueNodes.value) {
      const layoutMutations = useLayoutMutations()
      layoutMutations.moveReroute(
        reroute.id,
        { x: scaledX, y: scaledY },
        { x: oldX, y: oldY }
      )
    }
  }

  if ('inputNode' in graph && 'outputNode' in graph) {
    const ioNodes = [
      graph.inputNode as SubgraphInputNode,
      graph.outputNode as SubgraphOutputNode
    ]
    for (const ioNode of ioNodes) {
      const [oldX, oldY] = ioNode.pos
      const [oldWidth, oldHeight] = ioNode.size

      const relativeX = oldX - originX
      const relativeY = oldY - originY

      const scaledX = originX + relativeX * scaleFactor
      const scaledY = originY + relativeY * scaleFactor

      const scaledWidth = oldWidth * scaleFactor
      const scaledHeight = oldHeight * scaleFactor

      ioNode.pos = [scaledX, scaledY]
      ioNode.size = [scaledWidth, scaledHeight]
    }
  }

  graph.groups.forEach((group) => {
    const [oldX, oldY] = group.pos
    const [oldWidth, oldHeight] = group.size

    const relativeX = oldX - originX
    const relativeY = oldY - originY

    const scaledX = originX + relativeX * scaleFactor
    const scaledY = originY + relativeY * scaleFactor

    const scaledWidth = oldWidth * scaleFactor
    const scaledHeight = oldHeight * scaleFactor

    group.pos = [scaledX, scaledY]
    group.size = [scaledWidth, scaledHeight]
  })

  if (onActiveGraph && canvas) {
    const originScreen = canvas.ds.convertOffsetToCanvas([originX, originY])
    canvas.ds.changeScale(canvas.ds.scale / scaleFactor, originScreen)
  }

  graph.extra.workflowRendererVersion = needsUpscale ? 'Vue' : 'LG'
}


nodes.forEach((node, index) => {
if (index > 0) {
node.pos = [ x, y + (height * index) + (25 * (index + 1))];
}
this.canvas.graph?.change()
});
}

// @deprecated
isApiJson(data: unknown): data is ComfyApiWorkflow {
if (!_.isObject(data) || Array.isArray(data)) {
Expand Down
Loading