Compare commits

..

27 Commits

Author SHA1 Message Date
Comfy Org PR Bot
28d9be45e2 1.34.9 (#7534)
Patch version increment to 1.34.9

**Base branch:** `core/1.34`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7534-1-34-9-2cb6d73d3650816cafd7c0eb92482d1f)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-15 19:05:31 -07:00
Comfy Org PR Bot
56fde56960 [backport core/1.34] fix: collapsed nodes getting extra height based on contents (#7527)
Backport of #7490 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7527-backport-core-1-34-fix-collapsed-nodes-getting-extra-height-based-on-contents-2ca6d73d3650818da7cdc7e38bfcfb28)
by [Unito](https://www.unito.io)

Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
2025-12-15 17:22:42 -07:00
Comfy Org PR Bot
37e8f0fe9f [backport core/1.34] fix: prevent middle mouse button from triggering node resize in vueNodes mode (#7528)
Backport of #7511 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7528-backport-core-1-34-fix-prevent-middle-mouse-button-from-triggering-node-resize-in-vueN-2ca6d73d36508190af3fc437346f397f)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-12-15 17:22:01 -07:00
Christian Byrne
c5ebb479a2 [backport core/1.34] feat: bring node to front when clicking on any widget (#7520)
## Summary
Backport of #7202 to core/1.34

- Adds a capture-phase pointerdown handler to NodeWidgets that calls
`bringNodeToFront` whenever any widget is clicked
- Improves UX by ensuring the interacted node is always visible on top,
without requiring the node itself to be selected

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7131

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7520-backport-core-1-34-feat-bring-node-to-front-when-clicking-on-any-widget-2ca6d73d365081609bc0e2732a99f5d1)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-12-15 17:20:55 -07:00
Christian Byrne
72eee5bfe9 [backport core/1.34] style-fix: Don't add body padding with no body. (#7523)
## Summary

Backport of #7424 to core/1.34.

Small fix for collapsed nodes - don't add body padding when node is
collapsed.

### Changes
- Removed unconditional `pb-1` from node base classes
- Added conditional `!isCollapsed && ' pb-1'` to only apply padding when
node is expanded

Original PR: #7424

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

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-12-15 14:52:50 -07:00
Christian Byrne
0c56b7b178 [backport core/1.34] fix: inner groups being moved double when moving outer group (in vue mode) (#7525)
## Summary

Backport of #7447 to core/1.34.

Fixes issue when dragging a group that had inner groups when in vue
mode.

When dragging the outer group in Vue mode:

1. getAllNestedItems(selected) returns ALL items: outer group + inner
groups + nodes
2. moveChildNodesInGroupVueMode loops through all items
3. For outer group G1: calls G1.move(delta, true) then
moveGroupChildren(G1, ...)
4. moveGroupChildren calls G2.move(delta) (no skipChildren) - this moves
G2 AND G2's children!
5. Then the loop reaches G2: calls G2.move(delta, true) - moves G2 again
6. Plus moveGroupChildren(G2, ...) processes G2's children again

This PR fixes it by adding `skipChildren=true` to the `move` call.
2025-12-15 14:52:41 -07:00
Christian Byrne
ea94d4e547 [backport core/1.34] Feat: Remove the Nodes 2.0 Trial Banner (#7522)
## Summary
Backport of #7390 to core/1.34

The option to try it out is still in the Menu if you're looking for it.

## Conflicts Resolved
- All test snapshot conflicts: accepted PR version (banner removal
changes snapshots)
- 4 bringToFront snapshots: re-added (deleted in target, modified in PR)

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

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2025-12-15 14:52:14 -07:00
Christian Byrne
2212f50afb [backport core/1.34] fix: refreshing assets causes entire panel to re-render (enter loading state) (#7526)
Backport of #7449 to core/1.34.

## Original PR
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7449

## Changes
- Only show loading spinner when `loading && !displayAssets.length`
instead of just `loading`
- Prevents jarring re-render when refreshing assets
2025-12-15 14:51:46 -07:00
Christian Byrne
16628f3798 [backport core/1.34] feat: add live preview method setting for prompt execution (#7521)
## Summary

Backport of #7385 to core/1.34.

Add frontend setting to override live preview method per prompt
execution.

- **What**: New setting `Comfy.Execution.PreviewMethod` allows users to
override preview method (default/none/auto/latent2rgb/taesd) from
frontend.

## Conflict Resolution

- `src/schemas/apiSchema.ts`: Kept both `Comfy.Queue.ImageFit` (target
branch) and `Comfy.Execution.PreviewMethod` (PR addition)

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-15 14:51:27 -07:00
Comfy Org PR Bot
5007190c3a [backport core/1.34] fix: text-white usage causes video dimensions to be invisible on light theme (#7524)
Backport of #7408 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7524-backport-core-1-34-fix-text-white-usage-causes-video-dimensions-to-be-invisible-on-lig-2ca6d73d365081bf9622e5beba5e68f0)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-12-15 14:51:13 -07:00
Comfy Org PR Bot
9516100185 [backport core/1.34] fix: loading api-format workflow that contains "parameters" string (#7516)
Backport of #7411 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7516-backport-core-1-34-fix-loading-api-format-workflow-that-contains-parameters-string-2ca6d73d365081438d19fd6ed3272684)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-15 14:30:39 -07:00
Comfy Org PR Bot
56985acc25 [backport core/1.34] fix: work around Chrome GPU bug causing severe lag when dragging links (#7515)
Backport of #7394 to `core/1.34`

Automatically created by backport workflow.

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-12-15 14:17:44 -07:00
Comfy Org PR Bot
55bcb79ed5 [backport core/1.34] feat: improve search alg in templates (#7514)
Backport of #7377 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7514-backport-core-1-34-feat-improve-search-alg-in-templates-2ca6d73d365081dfb99fef48cb5e3abe)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-12-15 14:17:13 -07:00
Comfy Org PR Bot
0e19ed2f53 [backport core/1.34] add warning when using legacy mask editor (indicating it will be removed in next version) (#7513)
Backport of #7332 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7513-backport-core-1-34-add-warning-when-using-legacy-mask-editor-indicating-it-will-be-rem-2ca6d73d36508154937dd5b12e39ea47)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-12-15 14:16:51 -07:00
Comfy Org PR Bot
29fb0e0d1d [backport core/1.34] fix: node shape not reactive in vueNodes mode (#7512)
Backport of #7302 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7512-backport-core-1-34-fix-node-shape-not-reactive-in-vueNodes-mode-2ca6d73d365081de894fe4e3327791ad)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-12-15 14:16:31 -07:00
Comfy Org PR Bot
3f6111947b [backport core/1.34] Topbar: add Custom Nodes Manager button (#7495)
Backport of #7400 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7495-backport-core-1-34-Topbar-add-Custom-Nodes-Manager-button-2ca6d73d36508193897cdda51b5953f0)
by [Unito](https://www.unito.io)

Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-12-15 12:53:57 -08:00
Comfy Org PR Bot
02b3509c48 [backport core/1.34] Fix compatibility with older browsers (#7506)
Backport of #7205 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7506-backport-core-1-34-Fix-compatibility-with-older-browsers-2ca6d73d36508177bb41fa59ce6d9563)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-12-15 12:32:26 -07:00
Comfy Org PR Bot
92de66ccc0 [backport core/1.34] fix: move selected groups when dragging nodes in vueNodes mode (#7505)
Backport of #7306 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7505-backport-core-1-34-fix-move-selected-groups-when-dragging-nodes-in-vueNodes-mode-2ca6d73d365081e2af2fd6a5e9599941)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-12-15 12:32:01 -07:00
Comfy Org PR Bot
2290cef175 [backport core/1.34] hotfix: stabilize flaky workflow sidebar browser tests (#7504)
Backport of #7280 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7504-backport-core-1-34-hotfix-stabilize-flaky-workflow-sidebar-browser-tests-2ca6d73d3650810ab183d3f0e9316b70)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Luke Mino-Altherr <luke@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-12-15 12:31:47 -07:00
Comfy Org PR Bot
9e4025a341 [backport core/1.34] fix: loading state to show loader only if it takes more than 250ms (#7503)
Backport of #7268 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7503-backport-core-1-34-fix-loading-state-to-show-loader-only-if-it-takes-more-than-250ms-2ca6d73d365081a791e8f38454c74c28)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
2025-12-15 12:31:15 -07:00
Comfy Org PR Bot
d1a95e2fc7 [backport core/1.34] fix: Note/MarkdownNote node color change not reactive in vueNodes mode (#7502)
Backport of #7294 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7502-backport-core-1-34-fix-Note-MarkdownNote-node-color-change-not-reactive-in-vueNodes-mo-2ca6d73d365081e5bbf7db392b3a481f)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-12-15 12:30:44 -07:00
Comfy Org PR Bot
dfcb83479e [backport core/1.34] fix: mouse accidentally sticks and drag the node (#7500)
Backport of #7186 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7500-backport-core-1-34-fix-mouse-accidentally-sticks-and-drag-the-node-2ca6d73d3650814694b4cfc6700027c2)
by [Unito](https://www.unito.io)

Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
2025-12-15 12:30:22 -07:00
Comfy Org PR Bot
a7d365062b [backport core/1.34] Fix desktop menu docs links regression (#7501)
Backport of #7181 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7501-backport-core-1-34-Fix-desktop-menu-docs-links-regression-2ca6d73d365081bc8785ecc142e9c729)
by [Unito](https://www.unito.io)

Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-12-15 12:29:51 -07:00
Comfy Org PR Bot
c071c0b80e [backport core/1.34] feat(server-config): add legacy manager UI toggle (#7481)
Backport of #7478 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7481-backport-core-1-34-feat-server-config-add-legacy-manager-UI-toggle-2ca6d73d365081718d41e9cbd9f28bf9)
by [Unito](https://www.unito.io)

Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-12-14 19:00:09 -08:00
Benjamin Lu
39037da56c 1.34.8 (#7310)
Patch version increment to 1.34.8
2025-12-09 19:20:22 -08:00
Benjamin Lu
bcd20507b8 [backport core/1.34] Move cancel button into actionbar (#7297) (#7298)
## Summary
Backport of 2903560416 to core/1.34 to
place the cancel control alongside the run controls.

## Changes
- **What**: Move the interrupt button into the actionbar and remove the
duplicate from the top menu.
- **Breaking**: none
- **Dependencies**: none

## Review Focus
Check spacing/hover states of the new cancel control in docked vs
floating layouts.

## Screenshots (if applicable)
- n/a

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7298-backport-core-1-34-Move-cancel-button-into-actionbar-7297-2c46d73d36508188ad42ceaec0c2d469)
by [Unito](https://www.unito.io)
2025-12-09 20:16:48 -07:00
Comfy Org PR Bot
eab11fbafd [backport core/1.34] Add label to open subgraph button (#7289)
Backport of #7244 to `core/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7289-backport-core-1-34-Add-label-to-open-subgraph-button-2c46d73d365081fe8dd1e9fcb81c9c8e)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-12-09 03:22:06 -07:00
463 changed files with 11593 additions and 15838 deletions

View File

@@ -14,25 +14,25 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# Allow dev server access from remote IP addresses.
# If true, the vite dev server will listen on all addresses, including LAN
# and public addresses.
# VITE_REMOTE_DEV=true
VITE_REMOTE_DEV=false
# The directory containing the ComfyUI installation used to run Playwright tests.
# If you aren't using a separate install for testing, point this to your regular install.
TEST_COMFYUI_DIR=/home/ComfyUI
# Whether to enable minification of the frontend code.
# ENABLE_MINIFY=true
ENABLE_MINIFY=true
# Whether to disable proxying the `/templates` route. If true, allows you to
# serve templates from the ComfyUI_frontend/public/templates folder (for
# locally testing changes to templates). When false or nonexistent, the
# templates are served via the normal method from the server's python site
# packages.
# DISABLE_TEMPLATES_PROXY=true
DISABLE_TEMPLATES_PROXY=false
# If playwright tests are being run via vite dev server, Vue plugins will
# invalidate screenshots. When `true`, vite plugins will not be loaded.
# DISABLE_VUE_PLUGINS=true
DISABLE_VUE_PLUGINS=false
# Algolia credentials required for developing with the new custom node manager.
ALGOLIA_APP_ID=4E0RO38HS8

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: >
@@ -361,42 +357,6 @@ jobs:
if: steps.filter-targets.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
env:
GH_TOKEN: ${{ github.token }}
BACKPORT_AGENT_PROMPT_TEMPLATE: |
Backport PR #${PR_NUMBER} (${PR_URL}) to ${target}.
Cherry-pick merge commit ${MERGE_COMMIT} onto new branch
${BACKPORT_BRANCH} from origin/${target}.
Resolve conflicts in: ${CONFLICTS_INLINE}.
For test snapshots (browser_tests/**/*-snapshots/), accept PR version if
changed in original PR, else keep target. For package.json versions, keep
target branch. For pnpm-lock.yaml, regenerate with pnpm install.
Ask user for non-obvious conflicts.
Create PR titled "[backport ${target}] <original title>" with label "backport".
See .github/workflows/pr-backport.yaml for workflow details.
COMMENT_BODY_TEMPLATE: |
### ⚠️ Backport to `${target}` failed
**Reason:** Merge conflicts detected during cherry-pick of `${MERGE_COMMIT_SHORT}`
<details>
<summary>📄 Conflicting files</summary>
```
${CONFLICTS_BLOCK}
```
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
${AGENT_PROMPT}
```
</details>
---
cc @${PR_AUTHOR}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
@@ -419,27 +379,10 @@ jobs:
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
elif [ "${reason}" = "conflicts" ]; then
CONFLICTS_INLINE=$(echo "${conflicts}" | tr ',' ' ')
SAFE_TARGET=$(echo "$target" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
PR_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}"
export PR_NUMBER PR_URL MERGE_COMMIT target BACKPORT_BRANCH CONFLICTS_INLINE
# envsubst is provided by gettext-base
if ! command -v envsubst >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y gettext-base
fi
AGENT_PROMPT=$(envsubst '${PR_NUMBER} ${PR_URL} ${target} ${MERGE_COMMIT} ${BACKPORT_BRANCH} ${CONFLICTS_INLINE}' <<<"$BACKPORT_AGENT_PROMPT_TEMPLATE")
# Use fenced code block for conflicts to handle special chars in filenames
CONFLICTS_BLOCK=$(echo "${conflicts}" | tr ',' '\n')
MERGE_COMMIT_SHORT="${MERGE_COMMIT:0:7}"
export target MERGE_COMMIT_SHORT CONFLICTS_BLOCK AGENT_PROMPT PR_AUTHOR
COMMENT_BODY=$(envsubst '${target} ${MERGE_COMMIT_SHORT} ${CONFLICTS_BLOCK} ${AGENT_PROMPT} ${PR_AUTHOR}' <<<"$COMMENT_BODY_TEMPLATE")
# Convert comma-separated conflicts back to newlines for display
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
fi
done

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

@@ -1,12 +1,12 @@
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
name: "Release: Bi-weekly ComfyUI"
# Automated weekly workflow to bump ComfyUI frontend RC releases
name: "Release: Weekly ComfyUI"
on:
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
schedule:
- cron: '0 20 * * 1'
# Allow manual triggering (bypasses bi-weekly check)
# Allow manual triggering
workflow_dispatch:
inputs:
comfyui_fork:
@@ -16,39 +16,7 @@ on:
type: string
jobs:
check-release-week:
runs-on: ubuntu-latest
outputs:
is_release_week: ${{ steps.check.outputs.is_release_week }}
steps:
- name: Check if release week
id: check
run: |
# Anchor date: first bi-weekly release Monday
ANCHOR="2025-12-22"
ANCHOR_EPOCH=$(date -d "$ANCHOR" +%s)
NOW_EPOCH=$(date +%s)
WEEKS_SINCE=$(( (NOW_EPOCH - ANCHOR_EPOCH) / 604800 ))
if [ $((WEEKS_SINCE % 2)) -eq 0 ]; then
echo "Release week (week $WEEKS_SINCE since anchor $ANCHOR)"
echo "is_release_week=true" >> $GITHUB_OUTPUT
else
echo "Not a release week (week $WEEKS_SINCE since anchor $ANCHOR)"
echo "is_release_week=false" >> $GITHUB_OUTPUT
fi
- name: Summary
run: |
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
resolve-version:
needs: check-release-week
if: needs.check-release-week.outputs.is_release_week == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.resolve.outputs.current_version }}
@@ -163,8 +131,8 @@ jobs:
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
create-comfyui-pr:
needs: [check-release-week, resolve-version, trigger-release-if-needed]
if: always() && needs.resolve-version.result == 'success' && (needs.check-release-week.outputs.is_release_week == 'true' || github.event_name == 'workflow_dispatch')
needs: [resolve-version, trigger-release-if-needed]
if: always() && needs.resolve-version.result == 'success'
runs-on: ubuntu-latest
steps:
@@ -263,7 +231,7 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create/update branch (reuse same branch name each release cycle)
# Create/update branch (reuse same branch name each week)
BRANCH="automation/comfyui-frontend-bump"
git checkout -B "$BRANCH"
git add requirements.txt
@@ -275,7 +243,7 @@ jobs:
exit 0
fi
# Force push to fork (overwrites previous release cycle's branch)
# Force push to fork (overwrites previous week's branch)
# Note: This intentionally destroys branch history to maintain a single PR
# Any review comments or manual commits will need to be re-applied
if ! git push -f origin "$BRANCH"; then

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

@@ -9,9 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import '@/assets/css/style.css'
import { i18n } from '@/i18n'
import '@/lib/litegraph/public/css/litegraph.css'
import '@/assets/css/style.css'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
@@ -58,7 +58,6 @@ export const withTheme = (Story: StoryFn, context: StoryContext) => {
document.documentElement.classList.remove('dark-theme')
document.body.classList.remove('dark-theme')
}
document.body.classList.add('[&_*]:!font-inter')
return Story(context.args, context)
}

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

@@ -73,6 +73,24 @@ The project uses **Nx** for build orchestration and task management
- composables `useXyz.ts`
- Pinia stores `*Store.ts`
## Testing Guidelines
- Frameworks:
- Vitest (unit/component, happy-dom)
- Playwright (E2E)
- Test files:
- Unit/Component: `**/*.test.ts`
- E2E: `browser_tests/**/*.spec.ts`
- Litegraph Specific: `src/lib/litegraph/test/`
- Coverage: text/json/html reporters enabled
- aim to cover critical logic and new features
- Playwright:
- optional tags like `@mobile`, `@2x` are respected by config
- Tests to avoid
- Change detector tests
- e.g. a test that just asserts that the defaults are certain values
- Tests that are dependent on non-behavioral features like utility classes or styles
- Redundant tests
## Commit & Pull Request Guidelines
@@ -143,51 +161,14 @@ The project uses **Nx** for build orchestration and task management
14. Write code that is expressive and self-documenting to the furthest degree possible. This reduces the need for code comments which can get out of sync with the code itself. Try to avoid comments unless absolutely necessary
15. Do not add or retain redundant comments, clean as you go
16. Whenever a new piece of code is written, the author should ask themselves 'is there a simpler way to introduce the same functionality?'. If the answer is yes, the simpler course should be chosen
17. [Refactoring](https://refactoring.com/catalog/) should be used to make complex code simpler
17. Refactoring should be used to make complex code simpler
18. Try to minimize the surface area (exported values) of each module and composable
19. Don't use barrel files, e.g. `/some/package/index.ts` to re-export within `/src`
20. Keep functions short and functional
21. Minimize [nesting](https://wiki.c2.com/?ArrowAntiPattern), e.g. `if () { ... }` or `for () { ... }`
22. Avoid mutable state, prefer immutability and assignment at point of declaration
23. Favor pure functions (especially testable ones)
24. Do not use function expressions if it's possible to use function declarations instead
25. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
## Testing Guidelines
- Frameworks:
- Vitest (unit/component, happy-dom)
- Playwright (E2E)
- Test files:
- Unit/Component: `**/*.test.ts`
- E2E: `browser_tests/**/*.spec.ts`
- Litegraph Specific: `src/lib/litegraph/test/`
### General
1. Do not write change detector tests
e.g. a test that just asserts that the defaults are certain values
2. Do not write tests that are dependent on non-behavioral features like utility classes or styles
3. Be parsimonious in testing, do not write redundant tests
See <https://tidyfirst.substack.com/p/composable-tests>
4. [Dont Mock What You Dont Own](https://hynek.me/articles/what-to-mock-in-5-mins/)
### Vitest / Unit Tests
1. Do not write tests that just test the mocks
Ensure that the tests fail when the code itself would behave in a way that was not expected or desired
2. For mocking, leverage [Vitest's utilities](https://vitest.dev/guide/mocking.html) where possible
3. Keep your module mocks contained
Do not use global mutable state within the test file
Use `vi.hoisted()` if necessary to allow for per-test Arrange phase manipulation of deeper mock state
4. For Component testing, use [Vue Test Utils](https://test-utils.vuejs.org/) and especially follow the advice [about making components easy to test](https://test-utils.vuejs.org/guide/essentials/easy-to-test.html)
5. Aim for behavioral coverage of critical and new features
### Playwright / Browser / E2E Tests
1. Follow the Best Practices described [in the Playwright documentation](https://playwright.dev/docs/best-practices)
2. Do not use waitForTimeout, use Locator actions and [retrying assertions](https://playwright.dev/docs/test-assertions#auto-retrying-assertions)
3. Tags like `@mobile`, `@2x` are respected by config and should be used for relevant tests
24. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
## External Resources
@@ -201,7 +182,6 @@ The project uses **Nx** for build orchestration and task management
- Electron: <https://www.electronjs.org/docs/latest/>
- Wiki: <https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview>
- Nx: <https://nx.dev/docs/reference/nx-commands>
- [Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
## Project Philosophy
@@ -215,29 +195,6 @@ The project uses **Nx** for build orchestration and task management
- Thousands of users and extensions
- Prioritize clean interfaces that restrict extension access
### Code Review
In doing a code review, you should make sure that:
- The code is well-designed.
- The functionality is good for the users of the code.
- Any UI changes are sensible and look good.
- Any parallel programming is done safely.
- The code isnt more complex than it needs to be.
- The developer isnt implementing things they might need in the future but dont know they need now.
- Code has appropriate unit tests.
- Tests are well-designed.
- The developer used clear names for everything.
- Comments are clear and useful, and mostly explain why instead of what.
- Code is appropriately documented (generally in g3doc).
- The code conforms to our style guides.
#### [Complexity](https://google.github.io/eng-practices/review/reviewer/looking-for.html#complexity)
Is the CL more complex than it should be? Check this at every level of the CL—are individual lines too complex? Are functions too complex? Are classes too complex? “Too complex” usually means “cant be understood quickly by code readers.” It can also mean “developers are likely to introduce bugs when they try to call or modify this code.”
A particular type of complexity is over-engineering, where developers have made the code more generic than it needs to be, or added functionality that isnt presently needed by the system. Reviewers should be especially vigilant about over-engineering. Encourage developers to solve the problem they know needs to be solved now, not the problem that the developer speculates might need to be solved in the future. The future problem should be solved once it arrives and you can see its actual shape and requirements in the physical universe.
## Repository Navigation
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)

View File

@@ -12,7 +12,7 @@ For first-time setup, use the Claude command:
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
**Prerequisites:** Node.js >= 24, Git repository, available ports for dev server, storybook, etc.
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
## Development Workflow

View File

@@ -25,9 +25,6 @@
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu
# Partner Nodes
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu

View File

@@ -17,9 +17,17 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (v24) and pnpm
- Node.js (v18 or later to build; v24 for vite dev server) and pnpm
- Git for version control
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)
- A running ComfyUI backend instance
- **Tech Stack**:
- [Vue 3.5 Composition API](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
- [Pinia](https://pinia.vuejs.org/) for state management
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- litegraph.js (integrated in src/lib) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
### Initial Setup
@@ -47,18 +55,15 @@ To launch ComfyUI and have it connect to your development server:
python main.py --port 8188
```
If you are on Mac or a low-spec machine, you can run the server in CPU mode
### Git pre-commit hooks
```bash
python main.py --port 8188 --cpu
```
Run `pnpm prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
### Dev Server
- Start local ComfyUI backend at `localhost:8188`
- Run `pnpm dev` to start the dev server
- Run `pnpm dev:electron` to start the dev server with electron API mocked
- Run `pnpm dev:cloud` to start the dev server against the cloud backend (instead of local ComfyUI server)
#### Access dev server on touch devices
@@ -108,7 +113,7 @@ When you fix a bug that affects a version in feature freeze, we use an automated
1. Create your PR fixing the bug on `main` branch as usual
2. Before merging, add these labels to your PR:
- `needs-backport` - triggers the automated backport workflow
- `core/1.24` - targets the `core/1.24` release candidate branch
- `1.24` - targets the `core/1.24` release candidate branch
3. Merge your PR normally
4. The automated workflow will:
@@ -140,6 +145,67 @@ This project includes `.vscode/launch.json.default` and `.vscode/settings.json.d
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
### MCP Integrations
#### Playwright Browser Automation
The Playwright MCP server enables LLM coding assistants (like Copilot, Claude Code, etc.) to control and inspect web browsers programmatically. This allows assistants to navigate websites, take screenshots, and interact with web pages on your behalf.
For ComfyUI_frontend development, you can ask coding assistants to use Playwright screenshots to continuously verify that your code changes produce the expected visual results.
##### Setup for Claude Code
After installing dependencies with `pnpm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
```
> navigate to localhost:5173 and take a screenshot.
tell me the items in the menubar
● I'll navigate to localhost:5173 and take a
screenshot to see the menubar items.
● playwright:playwright_navigate (MCP)(url: …
"http://local
host:5173")
⎿ Navigated to http://localhost:5173
● playwright:playwright_screenshot (MCP)(name: "lo…
alhost_5173
_menubar")
⎿ Screenshot saved to: ../../../Downloads/localho
st_5173_menubar-2025-05-30T17-23-00-942Z.png
⎿ Screenshot also stored in memory with name:
'localhost_5173_menubar'
● Read(../../../Downloads/localhost_5173_menubar-2…
25-05-30T17-23-00-942Z.png)
⎿ Read image (94.3KB)
● Based on the screenshot, I can see the menubar at
the top of the ComfyUI interface contains the
following items:
Main Menubar Items:
- Workflow - Menu for workflow-related actions
- Edit - Menu for editing operations
- Help - Menu for help and documentation
Additional UI Elements:
- ComfyUI Logo - On the far left
- Workflow Tab - Shows "Unsaved Workflow" with a
dropdown and close button
- Layout Controls - On the far right (grid view
and hamburger menu icons)
The interface shows a typical ComfyUI workflow
graph with nodes like "Load Checkpoint", "CLIP
Text Encode (Prompt)", "KSampler", and "Empty
Latent Image" connected with colored cables.
```
## Testing
### Unit Tests
@@ -149,7 +215,7 @@ We've also included a list of recommended extensions in `.vscode/extensions.json
### Playwright Tests
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details. The snapshots are generated in the GH actions runner, not locally.
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details.
### Running All Tests
@@ -157,6 +223,7 @@ Before submitting a PR, ensure all tests pass:
```bash
pnpm test:unit
pnpm test:browser
pnpm typecheck
pnpm lint
pnpm format
@@ -165,28 +232,23 @@ pnpm format
## Code Style Guidelines
### TypeScript
- Use TypeScript for all new code
- Avoid `any` types - use proper type definitions
- Never use `@ts-expect-error` - fix the underlying type issue
### Vue 3 Patterns
- Use Composition API for all components
- Follow Vue 3.5+ patterns (props destructuring is reactive)
- Use `<script setup>` syntax
### Styling
- Use Tailwind CSS classes instead of custom CSS
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the [style.css](packages/design-system/src/css/style.css) like `bg-node-component-surface`
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
### Internationalization
- All user-facing strings must use vue-i18n
- Add translations to [src/locales/en/main.json](src/locales/en/main.json)
- Add translations to `src/locales/en/main.json`
- Use translation keys: `const { t } = useI18n(); t('key.path')`
- The corresponding values in other locales is generated automatically on releases, PR authors only need to edit [src/locales/en/main.json](src/locales/en/main.json)
## Icons
@@ -220,12 +282,34 @@ The original litegraph repository (https://github.com/Comfy-Org/litegraph.js) is
2. Run all tests and ensure they pass
3. Create a pull request with a clear title and description
4. Use conventional commit format for PR titles:
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation
- `refactor:` for code refactoring
- `test:` for test additions/changes
- `chore:` for maintenance tasks
- `[feat]` for new features
- `[fix]` for bug fixes
- `[docs]` for documentation
- `[refactor]` for code refactoring
- `[test]` for test additions/changes
- `[chore]` for maintenance tasks
### PR Description Template
```
## Description
Brief description of the changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests pass
- [ ] Component tests pass
- [ ] Browser tests pass (if applicable)
- [ ] Manual testing completed
## Screenshots (if applicable)
Add screenshots for UI changes
```
### Review Process
@@ -241,4 +325,4 @@ If you have questions about contributing:
- Ask in our [Discord](https://discord.com/invite/comfyorg)
- Open a new issue for clarification
Thank you for contributing to the ComfyUI Frontend!
Thank you for contributing to ComfyUI Frontend!

View File

@@ -33,11 +33,11 @@
The project follows a structured release process for each minor version, consisting of three distinct phases:
1. **Development Phase** - 2 weeks
1. **Development Phase** - 1 week
- Active development of new features
- Code changes merged to the development branch
2. **Feature Freeze** - 2 weeks
2. **Feature Freeze** - 1 week
- No new features accepted
- Only bug fixes are cherry-picked to the release branch
- Testing and stabilization of the codebase
@@ -56,16 +56,16 @@ To use the latest nightly release, add the following command line argument to yo
```
## Overlapping Release Cycles
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously. Each feature has approximately 4 weeks from merge to ComfyUI stable release (2 weeks on main, 2 weeks frozen on RC).
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously.
### Example Release Cycle
| Week | Date Range | Version 1.1 | Version 1.2 | Version 1.3 | Patch Releases |
|------|------------|-------------|-------------|-------------|----------------|
| 1-2 | Mar 1-14 | Development | - | - | - |
| 3-4 | Mar 15-28 | Feature Freeze | Development | - | 1.1.0 through 1.1.13 (daily) |
| 5-6 | Mar 29-Apr 11 | Released | Feature Freeze | Development | 1.1.14+ (daily)<br>1.2.0 through 1.2.13 (daily) |
| 7-8 | Apr 12-25 | - | Released | Feature Freeze | 1.2.14+ (daily)<br>1.3.0 through 1.3.13 (daily) |
| 1 | Mar 1-7 | Development | - | - | - |
| 2 | Mar 8-14 | Feature Freeze | Development | - | 1.1.0 through 1.1.6 (daily) |
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
## Release Summary

View File

@@ -87,8 +87,6 @@
}
},
"scripts": {
"lint": "nx run @comfyorg/desktop-ui:lint",
"typecheck": "nx run @comfyorg/desktop-ui:typecheck",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build -o dist/storybook"
},

View File

@@ -40,8 +40,7 @@
<script setup lang="ts">
import type { PassThrough } from '@primevue/core'
import Button from 'primevue/button'
import Step from 'primevue/step'
import type { StepPassThroughOptions } from 'primevue/step'
import Step, { type StepPassThroughOptions } from 'primevue/step'
import StepList from 'primevue/steplist'
defineProps<{
@@ -51,6 +50,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

@@ -155,14 +155,12 @@ export async function loadLocale(locale: string): Promise<void> {
}
// Only include English in the initial bundle
const enMessages = buildLocale(en, enNodes, enCommands, enSettings)
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings)
}
// Type for locale messages - inferred from the English locale structure
type LocaleMessages = typeof enMessages
const messages: Record<string, LocaleMessages> = {
en: enMessages
}
type LocaleMessages = typeof messages.en
export const i18n = createI18n({
// Must set `false`, as Vue I18n Legacy API is for Vue 2

View File

@@ -1,6 +1,5 @@
import { useTimeout } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import type { Ref } from 'vue'
import { type Ref, computed, ref, watch } from 'vue'
/**
* Vue boolean ref (writable computed) with one difference: when set to `true` it stays that way for at least {@link minDuration}.

View File

@@ -29,8 +29,7 @@ import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import Button from 'primevue/button'
import { useRoute } from 'vue-router'
import { getDialog } from '@/constants/desktopDialogs'
import type { DialogAction } from '@/constants/desktopDialogs'
import { type DialogAction, getDialog } from '@/constants/desktopDialogs'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'

View File

@@ -5,7 +5,7 @@
<img
class="sad-girl"
src="/assets/images/sad_girl.png"
:alt="$t('notSupported.illustrationAlt')"
alt="Sad girl illustration"
/>
<div class="no-drag sad-text flex items-center">

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
}

View File

@@ -1,412 +0,0 @@
{
"id": "2ba0b800-2f13-4f21-b8d6-c6cdb0152cae",
"revision": 0,
"last_node_id": 16,
"last_link_id": 9,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
60,
200
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
1
]
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
3,
5
]
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
8
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"cnr_id": "comfy-core",
"ver": "0.3.65",
"models": [
{
"name": "v1-5-pruned-emaonly-fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
"directory": "checkpoints"
}
]
},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
870,
170
],
"size": [
315,
474
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
685468484323813,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
975,
700
],
"size": [
210,
46
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": []
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": []
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
410,
410
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
"text, watermark"
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
520,
690
],
"size": [
315,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
411.21649169921875,
203.68695068359375
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, purple galaxy bottle,"
],
"color": "#232",
"bgcolor": "#353"
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
]
],
"groups": [
{
"id": 1,
"title": "Step 1 - Load model",
"bounding": [
50,
130,
335,
181.60000610351562
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 2,
"title": "Step 3 - Image size",
"bounding": [
510,
620,
335,
189.60000610351562
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "Step 2 - Prompt",
"bounding": [
400,
130,
445.27801513671875,
467.2060852050781
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.44218252181616574,
"offset": [
-666.5670907104311,
-2227.894644048147
]
},
"frontendVersion": "1.35.3",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true,
"workflowRendererVersion": "LG"
},
"version": 0.4
}

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: 79 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: 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

@@ -50,7 +50,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section shows the release
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).toBeVisible()
// Should show the release version
@@ -79,7 +79,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section shows no releases
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).toBeVisible()
// Should show "No recent releases" message
@@ -125,7 +125,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Should show no releases due to error
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(
whatsNewSection.locator('text=No recent releases')
).toBeVisible()
@@ -175,7 +175,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section is hidden
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).not.toBeVisible()
// Should not show any popups or toasts
@@ -260,7 +260,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section is visible
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).toBeVisible()
// Should show the release
@@ -308,7 +308,7 @@ test.describe('Release Notifications', () => {
await helpCenterButton.click()
// Verify "What's New?" section is visible
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).toBeVisible()
// Close help center
@@ -359,7 +359,7 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Section should be hidden regardless of empty releases
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).not.toBeVisible()
})
})

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

@@ -85,7 +85,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await comfyPage.page.getByText('Shape', { exact: true }).click()
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
timeout: 5000
})
@@ -136,18 +136,13 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame()
}
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })

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
})

View File

@@ -1,20 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Viewport', () => {
test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('viewport/default-viewport-saved-offscreen')
// Wait a few frames for rendering to stabilize
for (let i = 0; i < 5; i++) {
await comfyPage.nextFrame()
}
await expect(comfyPage.canvas).toHaveScreenshot(
'viewport-fits-when-saved-offscreen.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 129 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: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -828,55 +828,55 @@ test.describe('Vue Node Link Interaction', () => {
})
test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
test.fixme(
'Context menu opens and endpoint is pinned on Shift-drop',
async ({ comfyPage, comfyMouse }) => {
await comfyPage.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dropPos = { x: outputCenter.x + 90, y: outputCenter.y - 70 }
const dropPos = { x: outputCenter.x + 180, y: outputCenter.y - 140 }
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
// Context menu should be visible
const contextMenu = comfyPage.page.locator('.litecontextmenu')
await expect(contextMenu).toBeVisible()
// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()
// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
}
// Context menu should be visible
const contextMenu = comfyPage.page.locator('.litecontextmenu')
await expect(contextMenu).toBeVisible()
// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()
// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
})
)
test('Context menu -> Search pre-filters by link type and connects after selection', async ({
comfyPage,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 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: 60 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: 62 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: 58 KiB

After

Width:  |  Height:  |  Size: 58 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: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 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: 97 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 109 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

@@ -252,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

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