Compare commits

..

1 Commits

Author SHA1 Message Date
Terry Jia
b8d2b8fad2 test 2025-12-10 08:40:20 -05:00
367 changed files with 5339 additions and 9597 deletions

View File

@@ -1,26 +0,0 @@
# Description: Runs shellcheck on tracked shell scripts when they change
name: "CI: Shell Validation"
on:
push:
branches:
- main
paths:
- '**/*.sh'
pull_request:
paths:
- '**/*.sh'
jobs:
shell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install shellcheck
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
- name: Run shellcheck
run: bash ./scripts/cicd/check-shell.sh

View File

@@ -16,10 +16,6 @@ on:
type: boolean
default: false
concurrency:
group: backport-${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
cancel-in-progress: false
jobs:
backport:
if: >

View File

@@ -124,16 +124,12 @@ jobs:
- name: Stage changed snapshot files
id: changed-snapshots
run: |
set -euo pipefail
echo "=========================================="
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
echo "=========================================="
# Get list of changed snapshot files (including untracked/new files)
changed_files=$( (
git diff --name-only browser_tests/ 2>/dev/null || true
git ls-files --others --exclude-standard browser_tests/ 2>/dev/null || true
) | sort -u | grep -E '\-snapshots/' || true )
# Get list of changed snapshot files
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
if [ -z "$changed_files" ]; then
echo "No snapshot changes in this shard"
@@ -155,11 +151,6 @@ jobs:
# Strip 'browser_tests/' prefix to avoid double nesting
echo "Copying changed files to staging directory..."
while IFS= read -r file; do
# Skip paths that no longer exist (e.g. deletions)
if [ ! -f "$file" ]; then
echo " → (skipped; not a file) $file"
continue
fi
# Remove 'browser_tests/' prefix
file_without_prefix="${file#browser_tests/}"
# Create parent directories
@@ -270,19 +261,11 @@ jobs:
echo "CHANGES SUMMARY"
echo "=========================================="
echo ""
echo "Changed files in browser_tests (including untracked):"
CHANGES=$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)
if [ -z "$CHANGES" ]; then
echo "No changes"
echo ""
echo "Total changes:"
echo "0"
else
echo "$CHANGES" | head -50
echo ""
echo "Total changes:"
echo "$CHANGES" | wc -l
fi
echo "Changed files in browser_tests:"
git diff --name-only browser_tests/ | head -20 || echo "No changes"
echo ""
echo "Total changes:"
git diff --name-only browser_tests/ | wc -l || echo "0"
- name: Commit updated expectations
id: commit
@@ -290,7 +273,7 @@ jobs:
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)" ]; then
if git diff --quiet browser_tests/; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0

View File

@@ -20,13 +20,6 @@ on:
required: true
default: 'main'
type: string
schedule:
# 00:00 UTC ≈ 4:00 PM PST / 5:00 PM PDT on the previous calendar day
- cron: '0 0 * * *'
concurrency:
group: release-version-bump
cancel-in-progress: true
jobs:
bump-version:
@@ -36,99 +29,15 @@ jobs:
pull-requests: write
steps:
- name: Prepare inputs
id: prepared-inputs
shell: bash
env:
RAW_VERSION_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version_type || '' }}
RAW_PRE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.pre_release || '' }}
RAW_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || '' }}
run: |
set -euo pipefail
VERSION_TYPE="$RAW_VERSION_TYPE"
PRE_RELEASE="$RAW_PRE_RELEASE"
TARGET_BRANCH="$RAW_BRANCH"
if [[ -z "$VERSION_TYPE" ]]; then
VERSION_TYPE='patch'
fi
if [[ -z "$TARGET_BRANCH" ]]; then
TARGET_BRANCH='main'
fi
{
echo "version_type=$VERSION_TYPE"
echo "pre_release=$PRE_RELEASE"
echo "branch=$TARGET_BRANCH"
} >> "$GITHUB_OUTPUT"
- name: Close stale nightly version bump PRs
if: github.event_name == 'schedule'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
const prefix = 'version-bump-'
const closed = []
const prs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
})
for (const pr of prs) {
if (!pr.head?.ref?.startsWith(prefix)) {
continue
}
if (pr.user?.login !== 'github-actions[bot]') {
continue
}
// Only clean up stale nightly PRs targeting main.
// Adjust here if other target branches should be cleaned.
if (pr.base?.ref !== 'main') {
continue
}
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
})
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${pr.head.ref}`
})
} catch (error) {
if (![404, 422].includes(error.status)) {
throw error
}
}
closed.push(pr.number)
}
core.info(`Closed ${closed.length} stale PR(s).`)
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ steps.prepared-inputs.outputs.branch }}
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
persist-credentials: false
- name: Validate branch exists
env:
TARGET_BRANCH: ${{ steps.prepared-inputs.outputs.branch }}
run: |
BRANCH="$TARGET_BRANCH"
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
@@ -142,7 +51,7 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
uses: pnpm/action-setup@v4
with:
version: 10
@@ -153,31 +62,16 @@ jobs:
- name: Bump version
id: bump-version
env:
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
PRE_RELEASE: ${{ steps.prepared-inputs.outputs.pre_release }}
run: |
set -euo pipefail
if [[ -n "$PRE_RELEASE" && ! "$VERSION_TYPE" =~ ^pre(major|minor|patch)$ && "$VERSION_TYPE" != "prerelease" ]]; then
echo "❌ pre_release was provided but version_type='$VERSION_TYPE' does not support --preid"
exit 1
fi
if [[ -n "$PRE_RELEASE" ]]; then
pnpm version "$VERSION_TYPE" --preid "$PRE_RELEASE" --no-git-tag-version
else
pnpm version "$VERSION_TYPE" --no-git-tag-version
fi
pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Format PR string
id: capitalised
env:
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
run: |
CAPITALISED_TYPE="$VERSION_TYPE"
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
CAPITALISED_TYPE=${{ github.event.inputs.version_type }}
echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
@@ -188,8 +82,8 @@ jobs:
body: |
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ steps.prepared-inputs.outputs.branch }}`
**Base branch:** `${{ github.event.inputs.branch }}`
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: ${{ steps.prepared-inputs.outputs.branch }}
base: ${{ github.event.inputs.branch }}
labels: |
Release

View File

@@ -10,7 +10,7 @@ module.exports = defineConfig({
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

1
.npmrc
View File

@@ -1,3 +1,2 @@
ignore-workspace-root-check=true
catalog-mode=prefer
public-hoist-pattern[]=@parcel/watcher

View File

@@ -2,98 +2,25 @@
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
".i18nrc.cjs",
".nx/*",
"components.d.ts",
"lint-staged.config.js",
"vitest.setup.ts",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*",
"components.d.ts",
"coverage/*",
"dist/*",
"packages/registry-types/src/comfyRegistryTypes.ts",
"playwright-report/*",
"src/extensions/core/*",
"src/scripts/*",
"src/types/generatedManagerTypes.ts",
"src/types/vue-shim.d.ts",
"test-results/*",
"vitest.setup.ts"
],
"plugins": [
"eslint",
"import",
"oxc",
"typescript",
"unicorn",
"vitest",
"vue"
"src/types/vue-shim.d.ts"
],
"rules": {
"no-async-promise-executor": "off",
"no-console": [
"error",
{
"allow": [
"warn",
"error"
]
}
],
"no-control-regex": "off",
"no-eval": "off",
"no-redeclare": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "primevue/calendar",
"message": "Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from 'primevue/datepicker'"
},
{
"name": "primevue/dropdown",
"message": "Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from 'primevue/select'"
},
{
"name": "primevue/inputswitch",
"message": "InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from 'primevue/toggleswitch'"
},
{
"name": "primevue/overlaypanel",
"message": "OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from 'primevue/popover'"
},
{
"name": "primevue/sidebar",
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
},
{
"name": "@/i18n--to-enable",
"importNames": [
"st",
"t",
"te",
"d"
],
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
}
]
}
],
"no-self-assign": "allow",
"no-unused-expressions": "off",
"no-unused-private-class-members": "off",
"no-useless-rename": "off",
"import/default": "error",
"import/export": "error",
"import/namespace": "error",
"import/no-duplicates": "error",
"import/consistent-type-specifier-style": [
"error",
"prefer-top-level"
],
"jest/expect-expect": "off",
"jest/no-conditional-expect": "off",
"jest/no-disabled-tests": "off",
"jest/no-standalone-expect": "off",
"jest/valid-title": "off",
"typescript/no-this-alias": "off",
"typescript/no-unnecessary-parameter-property-assignment": "off",
"typescript/no-unsafe-declaration-merging": "off",
@@ -112,18 +39,6 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},
"overrides": [
{
"files": [
"**/*.{stories,test,spec}.ts",
"**/*.stories.vue"
],
"rules": {
"no-console": "allow"
}
}
]
"vue/no-import-compiler-macros": "error"
}
}

View File

@@ -12,9 +12,6 @@
"declaration-property-value-no-unknown": [
true,
{
"typesSyntax": {
"radial-gradient()": "| <any-value>"
},
"ignoreProperties": {
"speak": ["none"],
"app-region": ["drag", "no-drag"],
@@ -59,7 +56,10 @@
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme", "v-bind"]
"ignoreFunctions": [
"theme",
"v-bind"
]
}
]
},

View File

@@ -51,6 +51,8 @@ defineProps<{
canProceed: boolean
/** Whether the location step should be disabled */
disableLocationStep: boolean
/** Whether the migration step should be disabled */
disableMigrationStep: boolean
/** Whether the settings step should be disabled */
disableSettingsStep: boolean
}>()

View File

@@ -1,150 +0,0 @@
{
"id": "e0cb1d7e-5437-4911-b574-c9603dfbeaee",
"revision": 0,
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 2,
"type": "8bfe4227-f272-49e1-a892-0a972a86867c",
"pos": [
-317,
-336
],
"size": [
210,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"proxyWidgets": [
[
"-1",
"batch_size"
]
]
},
"widgets_values": [
1
]
}
],
"links": [],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "8bfe4227-f272-49e1-a892-0a972a86867c",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 1,
"lastLinkId": 1,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [
-562,
-358,
120,
60
]
},
"outputNode": {
"id": -20,
"bounding": [
-52,
-358,
120,
40
]
},
"inputs": [
{
"id": "b4a8bc2a-8e9f-41aa-938d-c567a11d2c00",
"name": "batch_size",
"type": "INT",
"linkIds": [
1
],
"pos": [
-462,
-338
]
}
],
"outputs": [],
"widgets": [],
"nodes": [
{
"id": 1,
"type": "EmptyLatentImage",
"pos": [
-382,
-376
],
"size": [
270,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": 1
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
}
],
"groups": [],
"links": [
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 1,
"target_slot": 0,
"type": "INT"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"frontendVersion": "1.35.1"
},
"version": 0.4
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -585,15 +585,9 @@ export class ComfyPage {
fileName?: string
url?: string
dropPosition?: Position
waitForUpload?: boolean
} = {}
) {
const {
dropPosition = { x: 100, y: 100 },
fileName,
url,
waitForUpload = false
} = options
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
if (!fileName && !url)
throw new Error('Must provide either fileName or url')
@@ -630,14 +624,6 @@ export class ComfyPage {
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
if (url) evaluateParams.url = url
// Set up response waiter for file uploads before triggering the drop
const uploadResponsePromise = waitForUpload
? this.page.waitForResponse(
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
{ timeout: 10000 }
)
: null
// Execute the drag and drop in the browser
await this.page.evaluate(async (params) => {
const dataTransfer = new DataTransfer()
@@ -704,17 +690,12 @@ export class ComfyPage {
}
}, evaluateParams)
// Wait for file upload to complete
if (uploadResponsePromise) {
await uploadResponsePromise
}
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
options: { dropPosition?: Position } = {}
) {
return this.dragAndDropExternalResource({ fileName, ...options })
}

View File

@@ -160,7 +160,7 @@ export class VueNodeHelpers {
return {
input: widget.locator('input'),
incrementButton: widget.locator('button').first(),
decrementButton: widget.locator('button').nth(1)
decrementButton: widget.locator('button').last()
}
}
}

View File

@@ -1,5 +1,4 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import path from 'path'
import type {
@@ -9,20 +8,9 @@ import type {
export class ComfyTemplates {
readonly content: Locator
readonly allTemplateCards: Locator
constructor(readonly page: Page) {
this.content = page.getByTestId('template-workflows-content')
this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]')
}
async waitForMinimumCardCount(count: number) {
return await expect(async () => {
const cardCount = await this.allTemplateCards.count()
expect(cardCount).toBeGreaterThanOrEqual(count)
}).toPass({
timeout: 1_000
})
}
async loadTemplate(id: string) {

View File

@@ -77,7 +77,8 @@ test.describe('Background Image Upload', () => {
// Verify the URL input now has an API URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
const inputValue = await urlInput.inputValue()
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -36,10 +36,9 @@ test.describe('Execute to selected output nodes', () => {
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
})
})

View File

@@ -306,16 +306,14 @@ test.describe('Node Interaction', () => {
await comfyPage.canvas.click({
position: numberWidgetPos
})
const legacyPrompt = comfyPage.page.locator('.graphdialog')
await expect(legacyPrompt).toBeVisible()
await comfyPage.delay(300)
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png')
await comfyPage.canvas.click({
position: {
x: 10,
y: 10
}
})
await expect(legacyPrompt).toBeHidden()
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
})
test('Can close prompt dialog with canvas click (text widget)', async ({
@@ -329,16 +327,18 @@ test.describe('Node Interaction', () => {
await comfyPage.canvas.click({
position: textWidgetPos
})
const legacyPrompt = comfyPage.page.locator('.graphdialog')
await expect(legacyPrompt).toBeVisible()
await comfyPage.delay(300)
await expect(comfyPage.canvas).toHaveScreenshot(
'prompt-dialog-opened-text.png'
)
await comfyPage.canvas.click({
position: {
x: 10,
y: 10
}
})
await expect(legacyPrompt).toBeHidden()
await expect(comfyPage.canvas).toHaveScreenshot(
'prompt-dialog-closed-text.png'
)
})
test('Can double click node title to edit', async ({ comfyPage }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -12,7 +12,6 @@ test.describe('Load Workflow in Media', () => {
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow_prompt_parameters.png',
'workflow.webm',
// Skipped due to 3d widget unstable visual result.
// 3d widget shows grid after fully loaded.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -1,29 +0,0 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test'
test.describe('Mobile Baseline Snapshots', () => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
})
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -260,12 +260,6 @@ test.describe('Release context menu', () => {
test('Can trigger on link release', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
await expect(contextMenu.locator('.litemenu-title')).toContainText(
'CLIP | CLIP'
)
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(

View File

@@ -212,12 +212,8 @@ test.describe('Remote COMBO Widget', () => {
// Click on the canvas to trigger widget refresh
await comfyPage.page.mouse.click(400, 300)
await expect(async () => {
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
expect(refreshedOptions).not.toEqual(initialOptions)
}).toPass({
timeout: 2_000
})
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
expect(refreshedOptions).not.toEqual(initialOptions)
})
test('does not refresh when TTL is not set', async ({ comfyPage }) => {
@@ -325,12 +321,8 @@ test.describe('Remote COMBO Widget', () => {
await clickRefreshButton(comfyPage, nodeName)
// Verify the selected value of the widget is the first option in the refreshed list
await expect(async () => {
const refreshedValue = await getWidgetValue(comfyPage, nodeName)
expect(refreshedValue).toEqual('new first option')
}).toPass({
timeout: 2_000
})
const refreshedValue = await getWidgetValue(comfyPage, nodeName)
expect(refreshedValue).toEqual('new first option')
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -290,20 +290,16 @@ test.describe('Node library sidebar', () => {
await comfyPage.page.keyboard.insertText('bar')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(async () => {
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
).toEqual({
'bar/': {
icon: 'pi-folder',
color: '#007bff'
}
})
}).toPass({
timeout: 2_000
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
).toEqual({
'bar/': {
icon: 'pi-folder',
color: '#007bff'
}
})
})

View File

@@ -329,15 +329,6 @@ test.describe('Subgraph Operations', () => {
expect(newInputName).toBe(labelClickRenamedName)
expect(newInputName).not.toBe(initialInputLabel)
})
test('Can create widget from link with compressed target_slot', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot')
const step = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes[0].widgets[0].options.step
})
expect(step).toBe(10)
})
})
test.describe('Subgraph Creation and Deletion', () => {

View File

@@ -188,19 +188,22 @@ test.describe('Templates', () => {
.locator('header')
.filter({ hasText: 'Templates' })
await comfyPage.templates.waitForMinimumCardCount(1)
const cardCount = await comfyPage.page
.locator('[data-testid^="template-workflow-"]')
.count()
expect(cardCount).toBeGreaterThan(0)
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at desktop size
const mobileSize = { width: 640, height: 800 }
await comfyPage.page.setViewportSize(mobileSize)
await comfyPage.templates.waitForMinimumCardCount(1)
expect(cardCount).toBeGreaterThan(0)
await expect(templateGrid).toBeVisible()
await expect(nav).not.toBeVisible() // Nav should collapse at mobile size
const tabletSize = { width: 1024, height: 800 }
await comfyPage.page.setViewportSize(tabletSize)
await comfyPage.templates.waitForMinimumCardCount(1)
expect(cardCount).toBeGreaterThan(0)
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at tablet size
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,144 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../../../helpers/fitToView'
test.describe('Vue Node Bring to Front', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})
/**
* Helper to get the z-index of a node by its title
*/
async function getNodeZIndex(
comfyPage: ComfyPage,
title: string
): Promise<number> {
const node = comfyPage.vueNodes.getNodeByTitle(title)
const style = await node.getAttribute('style')
if (!style) {
throw new Error(
`Node "${title}" has no style attribute (observed: ${style})`
)
}
const match = style.match(/z-index:\s*(\d+)/)
if (!match) {
throw new Error(
`Node "${title}" has no z-index in style (observed: "${style}")`
)
}
return parseInt(match[1], 10)
}
/**
* Helper to get the bounding box center of a node
*/
async function getNodeCenter(
comfyPage: ComfyPage,
title: string
): Promise<{ x: number; y: number }> {
const node = comfyPage.vueNodes.getNodeByTitle(title)
const box = await node.boundingBox()
if (!box) throw new Error(`Node "${title}" not found`)
return { x: box.x + box.width / 2, y: box.y + box.height / 2 }
}
test('should bring overlapped node to front when clicking on it', async ({
comfyPage
}) => {
// Get initial positions
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
const ksamplerHeader = await comfyPage.page
.getByText('KSampler')
.boundingBox()
if (!ksamplerHeader) throw new Error('KSampler header not found')
// Drag KSampler on top of CLIP Text Encode
await comfyPage.dragAndDrop(
{ x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 },
clipCenter
)
await comfyPage.nextFrame()
// Screenshot showing KSampler on top of CLIP
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-overlapped-before.png'
)
// KSampler should be on top (higher z-index) after being dragged
const ksamplerZIndexBefore = await getNodeZIndex(comfyPage, 'KSampler')
const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
expect(ksamplerZIndexBefore).toBeGreaterThan(clipZIndexBefore)
// Click on CLIP Text Encode (underneath) - need to click on a visible part
// Since KSampler is on top, we click on the edge of CLIP that should still be visible
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
const clipBox = await clipNode.boundingBox()
if (!clipBox) throw new Error('CLIP node not found')
// Click on a visible edge of CLIP
await comfyPage.page.mouse.click(clipBox.x + 30, clipBox.y + 10)
await comfyPage.nextFrame()
// CLIP should now be on top - compare post-action z-indices
const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
const ksamplerZIndexAfter = await getNodeZIndex(comfyPage, 'KSampler')
expect(clipZIndexAfter).toBeGreaterThan(ksamplerZIndexAfter)
// Screenshot showing CLIP now on top
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-overlapped-after.png'
)
})
test('should bring overlapped node to front when clicking on its widget', async ({
comfyPage
}) => {
// Get CLIP Text Encode position (it has a text widget)
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
// Get VAE Decode position and drag it on top of CLIP
const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox()
if (!vaeHeader) throw new Error('VAE Decode header not found')
await comfyPage.dragAndDrop(
{ x: vaeHeader.x + 50, y: vaeHeader.y + 10 },
{ x: clipCenter.x - 50, y: clipCenter.y }
)
await comfyPage.nextFrame()
// VAE should be on top after drag
const vaeZIndexBefore = await getNodeZIndex(comfyPage, 'VAE Decode')
const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
expect(vaeZIndexBefore).toBeGreaterThan(clipZIndexBefore)
// Screenshot showing VAE on top
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-widget-overlapped-before.png'
)
// Click on the text widget of CLIP Text Encode
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
const clipBox = await clipNode.boundingBox()
if (!clipBox) throw new Error('CLIP node not found')
await comfyPage.page.mouse.click(clipBox.x + 170, clipBox.y + 80)
await comfyPage.nextFrame()
// CLIP should now be on top - compare post-action z-indices
const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
const vaeZIndexAfter = await getNodeZIndex(comfyPage, 'VAE Decode')
expect(clipZIndexAfter).toBeGreaterThan(vaeZIndexAfter)
// Screenshot showing CLIP now on top after widget click
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-widget-overlapped-after.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -15,9 +15,7 @@ test.describe('Vue Integer Widget', () => {
await comfyPage.loadWorkflow('vueNodes/linked-int-widget')
await comfyPage.vueNodes.waitForNodes()
const seedWidget = comfyPage.vueNodes
.getWidgetByName('KSampler', 'seed')
.first()
const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed')
const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget)
const initialValue = Number(await controls.input.inputValue())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -205,32 +205,6 @@ test.describe('Image widget', () => {
const filename = await fileComboWidget.getValue()
expect(filename).toBe('image32x32.webp')
})
test('Displays buttons when viewing single image of batch', async ({
comfyPage
}) => {
const [x, y] = await comfyPage.page.evaluate(() => {
const src =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='768' height='512' viewBox='0 0 1 1'%3E%3Crect width='1' height='1' stroke='black'/%3E%3C/svg%3E"
const image1 = new Image()
image1.src = src
const image2 = new Image()
image2.src = src
const targetNode = graph.nodes[6]
targetNode.imgs = [image1, image2]
targetNode.imageIndex = 1
app.canvas.setDirty(true)
const x = targetNode.pos[0] + targetNode.size[0] - 41
const y = targetNode.pos[1] + targetNode.widgets.at(-1).last_y + 30
return app.canvasPosToClientPos([x, y])
})
const clip = { x, y, width: 35, height: 35 }
await expect(comfyPage.page).toHaveScreenshot(
'image_preview_close_button.png',
{ clip }
)
})
})
test.describe('Animated image widget', () => {
@@ -278,8 +252,7 @@ test.describe('Animated image widget', () => {
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y },
waitForUpload: true
dropPosition: { x, y }
})
// Expect the filename combo value to be updated

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 B

View File

@@ -62,20 +62,16 @@ export default defineConfig([
{
ignores: [
'.i18nrc.cjs',
'.nx/*',
'components.d.ts',
'lint-staged.config.js',
'vitest.setup.ts',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
'components.d.ts',
'coverage/*',
'dist/*',
'packages/registry-types/src/comfyRegistryTypes.ts',
'playwright-report/*',
'src/extensions/core/*',
'src/scripts/*',
'src/types/generatedManagerTypes.ts',
'src/types/vue-shim.d.ts',
'test-results/*',
'vitest.setup.ts'
'src/types/vue-shim.d.ts'
]
},
{
@@ -107,17 +103,24 @@ export default defineConfig([
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Bad types in the plugin
pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
storybook.configs['flat/recommended'],
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
importX.flatConfigs.recommended,
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
importX.flatConfigs.typescript,
{
plugins: {
'unused-imports': unusedImports,
// @ts-expect-error Type incompatibility in i18n plugin
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility in i18n plugin
'@intlify/vue-i18n': pluginI18n
},
rules: {
@@ -132,24 +135,59 @@ export default defineConfig([
allowInterfaces: 'always'
}
],
'import-x/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import-x/no-useless-path-segments': 'error',
'import-x/no-relative-packages': 'error',
'unused-imports/no-unused-imports': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
'vue/no-v-html': 'off',
// Prohibit dark-theme: and dark: prefixes
'vue/no-restricted-class': ['error', '/^dark(-theme)?:/'],
'vue/multi-word-component-names': 'off', // TODO: fix
'vue/no-template-shadow': 'off', // TODO: fix
'vue/match-component-import-name': 'error',
/* Toggle on to do additional until we can clean up existing violations.
'vue/no-unused-emit-declarations': 'error',
'vue/no-unused-properties': 'error',
'vue/no-unused-refs': 'error',
'vue/no-useless-mustaches': 'error',
'vue/no-useless-v-bind': 'error',
'vue/no-unused-emit-declarations': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/one-component-per-file': 'error',
'vue/no-useless-v-bind': 'error',
// */
'vue/one-component-per-file': 'off', // TODO: fix
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
// Restrict deprecated PrimeVue components
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'primevue/calendar',
message:
'Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from "primevue/datepicker"'
},
{
name: 'primevue/dropdown',
message:
'Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from "primevue/select"'
},
{
name: 'primevue/inputswitch',
message:
'InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from "primevue/toggleswitch"'
},
{
name: 'primevue/overlaypanel',
message:
'OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from "primevue/popover"'
},
{
name: 'primevue/sidebar',
message:
'Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from "primevue/drawer"'
}
]
}
],
// i18n rules
'@intlify/vue-i18n/no-raw-text': [
'error',
@@ -237,6 +275,12 @@ export default defineConfig([
]
}
},
{
files: ['**/*.{test,spec,stories}.ts', '**/*.stories.vue'],
rules: {
'no-console': 'off'
}
},
{
files: ['scripts/**/*.js'],
languageOptions: {
@@ -253,14 +297,5 @@ export default defineConfig([
// Turn off ESLint rules that are already handled by oxlint
...oxlint.buildFromOxlintConfigFile(
path.resolve(import.meta.dirname, '.oxlintrc.json')
),
{
rules: {
'import-x/default': 'off',
'import-x/export': 'off',
'import-x/namespace': 'off',
'import-x/no-duplicates': 'off',
'import-x/consistent-type-specifier-style': 'off'
}
}
)
])

View File

@@ -5,6 +5,8 @@
<title>ComfyUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
<link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
<!-- Fullscreen mode on mobile browsers -->
<meta name="mobile-web-app-capable" content="yes">

17
lint-staged.config.js Normal file
View File

@@ -0,0 +1,17 @@
export default {
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
...formatAndEslint(stagedFiles),
'pnpm typecheck'
]
}
function formatAndEslint(fileNames) {
// Convert absolute paths to relative paths for better ESLint resolution
const relativePaths = fileNames.map((f) => f.replace(process.cwd() + '/', ''))
return [
`pnpm exec eslint --cache --fix ${relativePaths.join(' ')}`,
`pnpm exec prettier --cache --write ${relativePaths.join(' ')}`
]
}

View File

@@ -1,21 +0,0 @@
import path from 'node:path'
export default {
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
...formatAndEslint(stagedFiles),
'pnpm typecheck'
]
}
function formatAndEslint(fileNames: string[]) {
// Convert absolute paths to relative paths for better ESLint resolution
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
return [
`pnpm exec prettier --cache --write ${joinedPaths}`,
`pnpm exec oxlint --fix ${joinedPaths}`,
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.35.7",
"version": "1.35.1",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -19,7 +19,6 @@
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve",
"dev:desktop": "nx dev @comfyorg/desktop-ui",
"dev:electron": "nx serve --config vite.electron.config.mts",
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
"dev": "nx serve",
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",

View File

@@ -98,6 +98,7 @@
--color-bypass: #6a246a;
--color-error: #962a2a;
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
--color-interface-panel-job-progress-primary: var(--color-azure-300);
--color-interface-panel-job-progress-secondary: var(--color-alpha-azure-600-30);
@@ -235,7 +236,7 @@
--brand-yellow: var(--color-electric-400);
--brand-blue: var(--color-sapphire-700);
--secondary-background: var(--color-smoke-200);
--secondary-background-hover: var(--color-smoke-400);
--secondary-background-hover: var(--color-smoke-200);
--secondary-background-selected: var(--color-smoke-600);
--base-background: var(--color-white);
--primary-background: var(--color-azure-400);
@@ -260,8 +261,6 @@
--component-node-widget-background-selected: var(--secondary-background-selected);
--component-node-widget-background-disabled: var(--color-alpha-ash-500-20);
--component-node-widget-background-highlighted: var(--color-ash-500);
--component-node-widget-promoted: var(--color-purple-700);
--component-node-widget-advanced: var(--color-azure-400);
/* Default UI element color palette variables */
--palette-contrast-mix-color: #fff;
@@ -385,8 +384,6 @@
--component-node-widget-background-selected: var(--color-charcoal-100);
--component-node-widget-background-disabled: var(--color-alpha-charcoal-600-30);
--component-node-widget-background-highlighted: var(--color-graphite-400);
--component-node-widget-promoted: var(--color-purple-700);
--component-node-widget-advanced: var(--color-azure-600);
--modal-card-background: var(--secondary-background);
--modal-card-background-hovered: var(--secondary-background-hover);
@@ -437,11 +434,7 @@
--color-interface-button-hover-surface: var(
--interface-button-hover-surface
);
--color-comfy-input: var(--comfy-input-bg);
--color-comfy-input-foreground: var(--input-text);
--color-comfy-menu-bg: var(--comfy-menu-bg);
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
@@ -497,8 +490,6 @@
--color-component-node-widget-background-selected: var(--component-node-widget-background-selected);
--color-component-node-widget-background-disabled: var(--component-node-widget-background-disabled);
--color-component-node-widget-background-highlighted: var(--component-node-widget-background-highlighted);
--color-component-node-widget-promoted: var(--component-node-widget-promoted);
--color-component-node-widget-advanced: var(--component-node-widget-advanced);
/* Semantic tokens */
--color-base-foreground: var(--base-foreground);
@@ -1328,15 +1319,6 @@ audio.comfy-audio.empty-audio-widget {
font-size 0.1s ease;
}
/* Performance optimization during canvas interaction */
.transform-pane--interacting .lg-node * {
transition: none !important;
}
.transform-pane--interacting .lg-node {
will-change: transform;
}
/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {

View File

@@ -217,28 +217,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/verify-api-key": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Verify a ComfyUI API key and return customer details
* @description Validates a ComfyUI API key and returns the associated customer information.
* This endpoint is used by cloud.comfy.org to authenticate users via API keys
* instead of Firebase tokens.
*/
post: operations["VerifyApiKey"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/customers/{customer_id}/cloud-subscription-status": {
parameters: {
query?: never;
@@ -1967,40 +1945,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/proxy/kling/v1/images/omni-image": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** KlingAI Create Omni-Image Task */
post: operations["klingCreateOmniImage"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/proxy/kling/v1/images/omni-image/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** KlingAI Query Single Omni-Image Task */
get: operations["klingOmniImageQuerySingleTask"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/proxy/kling/v1/images/kolors-virtual-try-on": {
parameters: {
query?: never;
@@ -2176,26 +2120,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/proxy/bfl/flux-2-max/generate": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Proxy request to BFL Flux 2 Max for image generation
* @description Forwards image generation requests to BFL's Flux 2 Max API and returns the results. Supports image-to-image generation with up to 8 input images.
*/
post: operations["bflFlux2MaxGenerate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/proxy/bfl/flux-pro-1.0-expand/generate": {
parameters: {
query?: never;
@@ -3952,12 +3876,7 @@ export interface components {
* @description The subscription tier level
* @enum {string}
*/
SubscriptionTier: "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION";
/**
* @description The subscription billing duration
* @enum {string}
*/
SubscriptionDuration: "MONTHLY" | "ANNUAL";
SubscriptionTier: "STANDARD" | "CREATOR" | "PRO";
FeaturesResponse: {
/**
* @description The conversion rate for partner nodes
@@ -4804,13 +4723,13 @@ export interface components {
* @default kling-v1
* @enum {string}
*/
KlingTextToVideoModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1-master" | "kling-v2-5-turbo" | "kling-v2-6";
KlingTextToVideoModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1-master" | "kling-v2-5-turbo";
/**
* @description Model Name
* @default kling-v2-master
* @enum {string}
*/
KlingVideoGenModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1" | "kling-v2-1-master" | "kling-v2-5-turbo" | "kling-v2-6";
KlingVideoGenModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1" | "kling-v2-1-master" | "kling-v2-5-turbo";
/**
* @description Video generation mode. std: Standard Mode, which is cost-effective. pro: Professional Mode, generates videos with longer duration but higher quality output.
* @default std
@@ -4955,12 +4874,6 @@ export interface components {
camera_control?: components["schemas"]["KlingCameraControl"];
aspect_ratio?: components["schemas"]["KlingVideoGenAspectRatio"];
duration?: components["schemas"]["KlingVideoGenDuration"];
/**
* @description Whether to generate sound simultaneously when generating videos. Only V2.6 and subsequent versions of the model support this parameter.
* @default off
* @enum {string}
*/
sound: "on" | "off";
/**
* Format: uri
* @description The callback notification address
@@ -5023,12 +4936,6 @@ export interface components {
camera_control?: components["schemas"]["KlingCameraControl"];
aspect_ratio?: components["schemas"]["KlingVideoGenAspectRatio"];
duration?: components["schemas"]["KlingVideoGenDuration"];
/**
* @description Whether to generate sound simultaneously when generating videos. Only V2.6 and subsequent versions of the model support this parameter.
* @default off
* @enum {string}
*/
sound: "on" | "off";
/**
* Format: uri
* @description The callback notification address. Server will notify when the task status changes.
@@ -5189,71 +5096,6 @@ export interface components {
};
};
};
KlingOmniImageRequest: {
/**
* @description Model Name
* @default kling-image-o1
* @enum {string}
*/
model_name: "kling-image-o1";
/** @description Text prompt words, which can include positive and negative descriptions. Must not exceed 2,500 characters. The Omni model can achieve various capabilities through Prompt with elements and images. Specify an image in the format of <<<>>>, such as <<<image_1>>>. */
prompt: string;
/** @description Reference Image List. Supports inputting image Base64 encoding or image URL (ensure accessibility). Supported formats include .jpg/.jpeg/.png. File size cannot exceed 10MB. Width and height dimensions shall not be less than 300px, aspect ratio between 1:2.5 ~ 2.5:1. Maximum 10 images. */
image_list?: {
/** @description Image Base64 encoding or image URL (ensure accessibility) */
image?: string;
}[];
/**
* @description Image generation resolution. 1k is 1K standard, 2k is 2K high-res, 4k is 4K high-res.
* @default 1k
* @enum {string}
*/
resolution: "1k" | "2k" | "4k";
/**
* @description Number of generated images. Value range [1,9].
* @default 1
*/
n: number;
/**
* @description Aspect ratio of the generated images (width:height). auto is to intelligently generate images based on incoming content.
* @default auto
* @enum {string}
*/
aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3" | "21:9" | "auto";
/**
* Format: uri
* @description The callback notification address for the result of this task. If configured, the server will actively notify when the task status changes.
*/
callback_url?: string;
/** @description Customized Task ID. Must be unique within a single user account. */
external_task_id?: string;
};
KlingOmniImageResponse: {
/** @description Error code */
code?: number;
/** @description Error message */
message?: string;
/** @description Request ID */
request_id?: string;
data?: {
/** @description Task ID */
task_id?: string;
task_status?: components["schemas"]["KlingTaskStatus"];
/** @description Task status information, displaying the failure reason when the task fails (such as triggering the content risk control of the platform, etc.) */
task_status_msg?: string;
task_info?: {
/** @description Customer-defined task ID */
external_task_id?: string;
};
/** @description Task creation time, Unix timestamp in milliseconds */
created_at?: number;
/** @description Task update time, Unix timestamp in milliseconds */
updated_at?: number;
task_result?: {
images?: components["schemas"]["KlingImageResult"][];
};
};
};
KlingLipSyncInputObject: {
/** @description The ID of the video generated by Kling AI. Only supports 5-second and 10-second videos generated within the last 30 days. */
video_id?: string;
@@ -5818,7 +5660,7 @@ export interface components {
width: number;
/**
* @description Height of the image.
* @default 1024
* @default 768
*/
height: number;
/** @description Seed for reproducibility. */
@@ -5834,11 +5676,6 @@ export interface components {
* @enum {string}
*/
output_format: "jpeg" | "png";
/**
* @description Moderation tolerance level (Flux 2 Max only).
* @default 2
*/
safety_tolerance: number;
};
/** FluxProFillInputs */
BFLFluxProFillInputs: {
@@ -7037,10 +6874,6 @@ export interface components {
image_tokens?: number;
};
output_tokens?: number;
output_tokens_details?: {
text_tokens?: number;
image_tokens?: number;
};
total_tokens?: number;
};
};
@@ -10232,7 +10065,7 @@ export interface components {
};
BytePlusImageGenerationRequest: {
/** @enum {string} */
model: "seedream-3-0-t2i-250415" | "seededit-3-0-i2i-250628" | "seedream-4-0-250828" | "seedream-4-5-251128";
model: "seedream-3-0-t2i-250415" | "seededit-3-0-i2i-250628" | "seedream-4-0-250828";
/** @description Text description for image generation or transformation */
prompt: string;
/**
@@ -10337,10 +10170,10 @@ export interface components {
};
BytePlusVideoGenerationRequest: {
/**
* @description The ID of the model to call. Available models include seedance-1-0-pro-250528, seedance-1-0-pro-fast-251015, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428
* @description The ID of the model to call. Available models include seedance-1-0-pro-250528, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428
* @enum {string}
*/
model: "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428" | "seedance-1-0-pro-fast-251015";
model: "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428";
/** @description The input content for the model to generate a video */
content: components["schemas"]["BytePlusVideoGenerationContent"][];
/**
@@ -10424,76 +10257,40 @@ export interface components {
* @description The ID of the model to call
* @enum {string}
*/
model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview" | "wan2.6-t2v" | "wan2.6-i2v";
model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview";
/** @description Enter basic information, such as prompt words, etc. */
input: {
/**
* @description Text prompt words. Support Chinese and English, length not exceeding 800 characters.
* For wan2.6-r2v with multiple reference videos, use 'character1', 'character2', etc. to refer to subjects
* in the order of reference videos. Example: "Character1 sings on the roadside, Character2 dances beside it"
*/
/** @description Text prompt words. Support Chinese and English, length not exceeding 800 characters */
prompt: string;
/** @description Reverse prompt words are used to describe content that you do not want to see in the video screen */
negative_prompt?: string;
/** @description Audio file download URL. Supported formats: mp3 and wav. Cannot be used with reference_video_urls. */
/** @description Audio file download URL. Supported formats: mp3 and wav. */
audio_url?: string;
/** @description First frame image URL or Base64 encoded data. Required for I2V models. Image formats: JPEG, JPG, PNG, BMP, WEBP. Resolution: 360-2000 pixels. File size: max 10MB. */
img_url?: string;
/** @description Video effect template name. Optional. Currently supported: squish, flying, carousel. When used, prompt parameter is ignored. */
template?: string;
/**
* @description Reference video URLs for wan2.6-r2v model only. Array of 1-3 video URLs.
* Input restrictions:
* - Format: mp4, mov
* - Quantity: 1-3 videos
* - Single video length: 2-30 seconds
* - Single file size: max 30MB
* - Cannot be used with audio_url
* Reference duration: Single video max 5s, two videos max 2.5s each, three videos proportionally less.
* Billing: Based on actual reference duration used.
*/
reference_video_urls?: string[];
};
/** @description Video processing parameters */
parameters?: {
/**
* @description Video resolution in format width*height. Supported resolutions vary by model:
* For wan2.5 T2V: 480P (480*832, 832*480, 624*624), 720P, 1080P sizes
* For wan2.6 T2V/R2V (no 480P):
* 720P: 1280*720, 720*1280, 960*960, 1088*832, 832*1088
* 1080P: 1920*1080, 1080*1920, 1440*1440, 1632*1248, 1248*1632
*/
/** @description Used to specify the video resolution in the format of 宽*高. Supported resolutions vary by model (for T2V models) */
size?: string;
/**
* @description Resolution level for I2V models. Supported values vary by model:
* - wan2.5-i2v-preview: 480P, 720P, 1080P
* - wan2.6-i2v: 720P, 1080P only (no 480P support)
* @description Resolution level for I2V models. Supported values vary by model: 480P, 720P, 1080P
* @enum {string}
*/
resolution?: "480P" | "720P" | "1080P";
/**
* @description The duration of the video generated, in seconds:
* - wan2.5 models: 5 or 10 seconds
* - wan2.6-t2v, wan2.6-i2v: 5, 10, or 15 seconds
* - wan2.6-r2v: 5 or 10 seconds only (no 15s support)
* @description The duration of the video generated, in seconds
* @default 5
* @enum {integer}
*/
duration?: 5 | 10 | 15;
duration?: 5 | 10;
/**
* @description Is it enabled prompt intelligent rewriting. Default is true
* @default true
*/
prompt_extend?: boolean;
/**
* @description Intelligent multi-lens control. Only active when prompt_extend is enabled.
* For wan2.6 models only.
* - multi: Intelligent disassembly into multiple lenses (default)
* - single: Single lens generation
* @default multi
* @enum {string}
*/
shot_type?: "multi" | "single";
/** @description Random number seed, used to control the randomness of the model generated content */
seed?: number;
/**
@@ -11910,8 +11707,6 @@ export interface operations {
"application/json": {
/** @description Optional URL to redirect the customer after they're done with the billing portal */
return_url?: string;
/** @description Optional target subscription tier. When provided, creates a deep link directly to the subscription update confirmation screen with this tier pre-selected. */
target_tier?: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly";
};
};
};
@@ -12008,8 +11803,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description The subscription tier (standard, creator, or pro) with optional yearly billing (standard-yearly, creator-yearly, pro-yearly) */
tier: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly";
/** @description The subscription tier (standard, creator, or pro) */
tier: "standard" | "creator" | "pro";
};
cookie?: never;
};
@@ -12075,7 +11870,6 @@ export interface operations {
/** @description The active subscription ID if one exists */
subscription_id?: string | null;
subscription_tier?: components["schemas"]["SubscriptionTier"] | null;
subscription_duration?: components["schemas"]["SubscriptionDuration"] | null;
/** @description Whether the customer has funds/credits available */
has_fund?: boolean;
/**
@@ -12109,72 +11903,6 @@ export interface operations {
};
};
};
VerifyApiKey: {
parameters: {
query?: never;
header: {
/** @description Admin API secret used to authorize this request */
"X-Comfy-Admin-Secret": string;
};
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description The ComfyUI API key to verify (e.g., comfy_xxx...) */
api_key: string;
};
};
};
responses: {
/** @description API key is valid */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @description Whether the API key is valid */
valid: boolean;
/** @description The Firebase UID of the user */
firebase_uid: string;
/** @description The customer's email address */
email?: string;
/** @description The customer's name */
name?: string;
/** @description Whether the customer is an admin */
is_admin?: boolean;
};
};
};
/** @description Unauthorized or missing admin API secret */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description API key not found or invalid */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
GetAdminCustomerCloudSubscriptionStatus: {
parameters: {
query?: never;
@@ -12202,7 +11930,6 @@ export interface operations {
/** @description The active subscription ID if one exists */
subscription_id?: string | null;
subscription_tier?: components["schemas"]["SubscriptionTier"] | null;
subscription_duration?: components["schemas"]["SubscriptionDuration"] | null;
/** @description Whether the customer has funds/credits available */
has_fund?: boolean;
/**
@@ -12320,16 +12047,6 @@ export interface operations {
* @description The remaining balance from cloud credits in microamount
*/
cloud_credit_balance_micros?: number;
/**
* Format: double
* @description The total amount of pending/unbilled charges from draft invoices in microamount. Only included when the show_negative_balances feature flag is enabled.
*/
pending_charges_micros?: number;
/**
* Format: double
* @description The effective balance (total balance minus pending charges). Can be negative if pending charges exceed the balance. Only included when the show_negative_balances feature flag is enabled.
*/
effective_balance_micros?: number;
/** @description The currency code (e.g., "usd") */
currency: string;
};
@@ -12396,16 +12113,6 @@ export interface operations {
* @description The remaining balance from cloud credits in microamount
*/
cloud_credit_balance_micros?: number;
/**
* Format: double
* @description The total amount of pending/unbilled charges from draft invoices in microamount. Only included when the show_negative_balances feature flag is enabled.
*/
pending_charges_micros?: number;
/**
* Format: double
* @description The effective balance (total balance minus pending charges). Can be negative if pending charges exceed the balance. Only included when the show_negative_balances feature flag is enabled.
*/
effective_balance_micros?: number;
/** @description The currency code (e.g., "usd") */
currency: string;
};
@@ -14240,15 +13947,6 @@ export interface operations {
"application/json": components["schemas"]["Node"];
};
};
/** @description Redirect to node with normalized name match */
302: {
headers: {
/** @description URL of the node with the correct ID */
Location?: string;
[name: string]: unknown;
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
@@ -18647,198 +18345,6 @@ export interface operations {
};
};
};
klingCreateOmniImage: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Create task for generating omni-image */
requestBody: {
content: {
"application/json": components["schemas"]["KlingOmniImageRequest"];
};
};
responses: {
/** @description Successful response (Request successful) */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingOmniImageResponse"];
};
};
/** @description Invalid request parameters */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Authentication failed */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Unauthorized access to requested resource */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Account exception or Rate limit exceeded */
429: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Service temporarily unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Server timeout */
504: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
};
};
klingOmniImageQuerySingleTask: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Task ID or External Task ID. Can query by either task_id (generated by system) or external_task_id (customized task ID) */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful response (Request successful) */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingOmniImageResponse"];
};
};
/** @description Invalid request parameters */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Authentication failed */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Unauthorized access to requested resource */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Account exception or Rate limit exceeded */
429: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Service temporarily unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
/** @description Server timeout */
504: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["KlingErrorResponse"];
};
};
};
};
klingVirtualTryOnQueryTaskList: {
parameters: {
query?: {
@@ -19611,89 +19117,6 @@ export interface operations {
};
};
};
bflFlux2MaxGenerate: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["BFLFlux2ProGenerateRequest"];
};
};
responses: {
/** @description Successful response from BFL Flux 2 Max proxy */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BFLFluxProGenerateResponse"];
};
};
/** @description Bad Request (invalid input to proxy) */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Payment Required */
402: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Rate limit exceeded (either from proxy or BFL) */
429: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Internal Server Error (proxy or upstream issue) */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Bad Gateway (error communicating with BFL) */
502: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Gateway Timeout (BFL took too long to respond) */
504: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
BFLExpand_v1_flux_pro_1_0_expand_post: {
parameters: {
query?: never;

4119
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ packages:
catalog:
'@alloc/quick-lru': ^5.2.0
'@comfyorg/comfyui-electron-types': 0.5.5
'@eslint/js': ^9.39.1
'@eslint/js': ^9.35.0
'@iconify-json/lucide': ^1.1.178
'@iconify/json': ^2.2.380
'@iconify/tailwind': ^1.1.3
@@ -16,8 +16,8 @@ catalog:
'@nx/storybook': 21.4.1
'@nx/vite': 21.4.1
'@pinia/testing': ^0.1.5
'@playwright/test': ^1.57.0
'@prettier/plugin-oxc': ^0.1.3
'@playwright/test': ^1.52.0
'@prettier/plugin-oxc': ^0.0.4
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
@@ -48,15 +48,15 @@ catalog:
axios: ^1.8.2
cross-env: ^10.1.0
dotenv: ^16.4.5
eslint: ^9.39.1
eslint: ^9.34.0
eslint-config-prettier: ^10.1.8
eslint-import-resolver-typescript: ^4.4.4
eslint-plugin-import-x: ^4.16.1
eslint-plugin-oxlint: 1.25.0
eslint-plugin-prettier: ^5.5.4
eslint-plugin-storybook: ^9.1.16
eslint-plugin-unused-imports: ^4.3.0
eslint-plugin-vue: ^10.6.2
eslint-plugin-storybook: ^9.1.6
eslint-plugin-unused-imports: ^4.2.0
eslint-plugin-vue: ^10.4.0
firebase: ^11.6.0
globals: ^15.9.0
happy-dom: ^15.11.0
@@ -64,29 +64,29 @@ catalog:
jiti: 2.4.2
jsdom: ^26.1.0
knip: ^5.62.0
lint-staged: ^15.5.2
lint-staged: ^15.2.7
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 21.4.1
oxlint: ^1.32.0
oxlint-tsgolint: ^0.8.4
oxlint: ^1.25.0
oxlint-tsgolint: ^0.4.0
picocolors: ^1.1.1
pinia: ^2.1.7
postcss-html: ^1.8.0
prettier: ^3.7.4
prettier: ^3.6.2
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5
rollup-plugin-visualizer: ^6.0.4
storybook: ^9.1.16
stylelint: ^16.26.1
storybook: ^9.1.6
stylelint: ^16.24.0
tailwindcss: ^4.1.12
tailwindcss-primeui: ^0.6.1
tsx: ^4.15.6
tw-animate-css: ^1.3.8
typegpu: ^0.8.2
typescript: ^5.9.3
typescript-eslint: ^8.49.0
typescript: ^5.9.2
typescript-eslint: ^8.44.0
unplugin-icons: ^0.22.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^0.28.0
@@ -100,7 +100,7 @@ catalog:
vue-eslint-parser: ^10.2.0
vue-i18n: ^9.14.3
vue-router: ^4.4.3
vue-tsc: ^3.1.8
vue-tsc: ^3.0.7
vuefire: ^3.2.1
yjs: ^13.6.27
zod: ^3.23.8

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
if ! command -v shellcheck >/dev/null 2>&1; then
echo "Error: shellcheck is required but not installed" >&2
exit 127
fi
mapfile -t shell_files < <(git ls-files -- '*.sh')
if [[ ${#shell_files[@]} -eq 0 ]]; then
echo 'No shell scripts found.'
exit 0
fi
shellcheck --format=gcc "${shell_files[@]}"

View File

@@ -74,7 +74,7 @@ deploy_report() {
# Project name with dots converted to dashes for Cloudflare
sanitized_browser="${browser//./-}"
sanitized_browser=$(echo "$browser" | sed 's/\./-/g')
project="comfyui-playwright-${sanitized_browser}"
echo "Deploying $browser to project $project on branch $branch..." >&2
@@ -208,7 +208,7 @@ else
# Wait for all deployments to complete
for pid in $pids; do
wait "$pid"
wait $pid
done
# Collect URLs and counts in order
@@ -254,9 +254,9 @@ else
total_tests=0
# Parse counts and calculate totals
IFS='|' read -r -a counts_array <<< "$all_counts"
for counts_json in "${counts_array[@]}"; do
[ -z "$counts_json" ] && continue
IFS='|'
set -- $all_counts
for counts_json; do
if [ "$counts_json" != "{}" ] && [ -n "$counts_json" ]; then
# Parse JSON counts using simple grep/sed if jq is not available
if command -v jq > /dev/null 2>&1; then
@@ -324,12 +324,13 @@ $status_icon **$status_text**
# Add browser results with individual counts
i=0
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
IFS=' ' read -r -a url_array <<< "$urls"
for counts_json in "${counts_array[@]}"; do
[ -z "$counts_json" ] && { i=$((i + 1)); continue; }
browser="${browser_array[$i]:-}"
url="${url_array[$i]:-}"
IFS='|'
set -- $all_counts
for counts_json; do
# Get browser name
browser=$(echo "$BROWSERS" | cut -d' ' -f$((i + 1)))
# Get URL at position i
url=$(echo "$urls" | cut -d' ' -f$((i + 1)))
if [ "$url" != "failed" ] && [ -n "$url" ]; then
# Parse individual browser counts
@@ -373,4 +374,4 @@ $status_icon **$status_text**
🎉 Click on the links above to view detailed test results for each browser configuration."
post_comment "$comment"
fi
fi

View File

@@ -264,7 +264,7 @@ if (!releaseInfo) {
}
// Output as JSON for GitHub Actions
// eslint-disable-next-line no-console
console.log(JSON.stringify(releaseInfo, null, 2))
export { resolveRelease }

View File

@@ -28,9 +28,8 @@
)
"
/>
<Suspense @resolve="comfyRunButtonResolved">
<ComfyRunButton />
</Suspense>
<ComfyRunButton />
<IconButton
v-tooltip.bottom="cancelJobTooltipConfig"
type="transparent"
@@ -57,7 +56,7 @@ import {
import { clamp } from 'es-toolkit/compat'
import { storeToRefs } from 'pinia'
import Panel from 'primevue/panel'
import { computed, nextTick, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
@@ -140,14 +139,7 @@ const setInitialPosition = () => {
}
}
}
//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component.
//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition()
async function comfyRunButtonResolved() {
await nextTick()
setInitialPosition()
}
onMounted(setInitialPosition)
watch(visible, async (newVisible) => {
if (newVisible) {
await nextTick(setInitialPosition)

Some files were not shown because too many files have changed in this diff Show More