Compare commits

..

4 Commits

585 changed files with 31398 additions and 93601 deletions

View File

@@ -25,6 +25,3 @@ e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
# [refactor] Improve updates/notifications domain organization (#5590)
27ab355f9c73415dc39f4d3f512b02308f847801
# Migrate Tailwind styles to design-system package
9f19d8fb4bd22518879343b49c05634dca777df0

View File

@@ -36,9 +36,9 @@ body:
3. Click Queue Prompt
4. See error
value: |
1.
2.
3.
1.
2.
3.
validations:
required: true

View File

@@ -1,119 +0,0 @@
name: Post Release Summary Comment
description: Post or update a PR comment summarizing release links with diff, derived versions, and optional extras.
author: ComfyUI Frontend Team
inputs:
issue-number:
description: Optional PR number override (defaults to the current pull request)
default: ''
version_file:
description: Path to the JSON file containing the current version (relative to repo root)
required: true
outputs:
prev_version:
description: Previous version derived from the parent commit
value: ${{ steps.build.outputs.prev_version }}
runs:
using: composite
steps:
- name: Build comment body
id: build
shell: bash
run: |
set -euo pipefail
VERSION_FILE="${{ inputs.version_file }}"
REPO="${{ github.repository }}"
if [[ -z "$VERSION_FILE" ]]; then
echo "::error::version_file input is required" >&2
exit 1
fi
PREV_JSON=$(git show HEAD^1:"$VERSION_FILE" 2>/dev/null || true)
if [[ -z "$PREV_JSON" ]]; then
echo "::error::Unable to read $VERSION_FILE from parent commit" >&2
exit 1
fi
PREV_VERSION=$(printf '%s' "$PREV_JSON" | node -pe "const data = JSON.parse(require('fs').readFileSync(0, 'utf8')); if (!data.version) { process.exit(1); } data.version")
if [[ -z "$PREV_VERSION" ]]; then
echo "::error::Unable to determine previous version from $VERSION_FILE" >&2
exit 1
fi
NEW_VERSION=$(node -pe "const fs=require('fs');const data=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));if(!data.version){process.exit(1);}data.version" "$VERSION_FILE")
if [[ -z "$NEW_VERSION" ]]; then
echo "::error::Unable to determine current version from $VERSION_FILE" >&2
exit 1
fi
MARKER='release-summary'
MESSAGE='Publish jobs finished successfully:'
LINKS_VALUE=''
case "$VERSION_FILE" in
package.json)
LINKS_VALUE=$(printf '%s\n%s' \
'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/' \
'npm types|https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/{{version}}')
;;
apps/desktop-ui/package.json)
MARKER='desktop-release-summary'
LINKS_VALUE='npm desktop UI|https://www.npmjs.com/package/@comfyorg/desktop-ui/v/{{version}}'
;;
esac
DIFF_PREFIX='v'
DIFF_LABEL='Diff'
DIFF_URL="https://github.com/${REPO}/compare/${DIFF_PREFIX}${PREV_VERSION}...${DIFF_PREFIX}${NEW_VERSION}"
COMMENT_FILE=$(mktemp)
{
echo "<!--$MARKER:$DIFF_PREFIX$NEW_VERSION-->"
echo "$MESSAGE"
echo ""
echo "- $DIFF_LABEL: [\`$DIFF_PREFIX$PREV_VERSION...$DIFF_PREFIX$NEW_VERSION\`]($DIFF_URL)"
while IFS= read -r RAW_LINE; do
LINE=$(echo "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$LINE" ]] && continue
if [[ "$LINE" != *"|"* ]]; then
echo "::warning::Skipping malformed link entry: $LINE" >&2
continue
fi
LABEL=${LINE%%|*}
URL_TEMPLATE=${LINE#*|}
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
echo "- $LABEL: [\`$NEW_VERSION\`]($URL)"
done <<< "$LINKS_VALUE"
echo ""
} > "$COMMENT_FILE"
{
echo "body<<COMMENT_BODY_END_MARKER"
cat "$COMMENT_FILE"
echo "COMMENT_BODY_END_MARKER"
} >> "$GITHUB_OUTPUT"
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
- name: Find existing comment
id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }}
body: ${{ steps.build.outputs.body }}
edit-mode: replace

View File

@@ -105,4 +105,4 @@ jobs:
labels: Manager
delete-branch: true
add-paths: |
src/types/generatedManagerTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -6,8 +6,6 @@ on:
branches:
- main
pull_request:
paths:
- '**/*.json'
jobs:
json-lint:

View File

@@ -51,7 +51,7 @@ jobs:
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes

View File

@@ -6,7 +6,7 @@ on:
paths:
- 'tools/devtools/**'
push:
branches: [main]
branches: [ main ]
paths:
- 'tools/devtools/**'

View File

@@ -13,7 +13,7 @@ jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
@@ -43,14 +43,14 @@ jobs:
repo: context.repo.repo,
state: 'open',
});
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
if (!pr) {
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
return null;
}
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
return pr.number;
@@ -74,7 +74,7 @@ jobs:
run-id: ${{ github.event.workflow_run.id }}
pattern: playwright-report-*
path: reports
- name: Handle Test Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
@@ -85,9 +85,9 @@ jobs:
# Rename merged report if exists
[ -d "reports/playwright-report-chromium-merged" ] && \
mv reports/playwright-report-chromium-merged reports/playwright-report-chromium
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"
"completed"

View File

@@ -29,7 +29,7 @@ jobs:
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
# Save the entire workspace as cache for later test jobs to restore
- name: Generate cache key

View File

@@ -13,7 +13,7 @@ jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
@@ -43,14 +43,14 @@ jobs:
repo: context.repo.repo,
state: 'open',
});
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
if (!pr) {
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
return null;
}
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
return pr.number;
@@ -74,7 +74,7 @@ jobs:
run-id: ${{ github.event.workflow_run.id }}
name: storybook-static
path: storybook-static
- name: Handle Storybook Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
@@ -88,4 +88,4 @@ jobs:
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"
"completed"

View File

@@ -2,7 +2,7 @@ name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
on:
workflow_dispatch: # Allow manual triggering
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [main]
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -89,7 +89,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0 # Required for Chromatic baseline
fetch-depth: 0 # Required for Chromatic baseline
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -111,9 +111,9 @@ jobs:
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
onlyChanged: true # Only capture changed stories
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
onlyChanged: true # Only capture changed stories
- name: Set job status
id: job-status
@@ -138,17 +138,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: storybook-static
path: storybook-static
- name: Make deployment script executable
run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
- name: Deploy Storybook and comment on PR
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -176,25 +176,25 @@ jobs:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
// Find the existing Storybook comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.pull_request.number }}
});
const storybookComment = comments.find(comment =>
const storybookComment = comments.find(comment =>
comment.body.includes('<!-- STORYBOOK_BUILD_STATUS -->')
);
if (storybookComment && buildUrl && storybookUrl) {
// Append Chromatic info to existing comment
const updatedBody = storybookComment.body.replace(
/---\n(.*)$/s,
`---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,

View File

@@ -1,33 +0,0 @@
name: "CI: YAML Validation"
description: "Validates YAML syntax and style using yamllint with relaxed rules"
on:
push:
branches:
- main
paths:
- '**/*.yml'
- '**/*.yaml'
pull_request:
paths:
- '**/*.yml'
- '**/*.yaml'
jobs:
yaml-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install yamllint
run: |
python -m pip install --upgrade pip
python -m pip install yamllint
- name: Validate YAML syntax and style
run: ./scripts/cicd/check-yaml.sh

View File

@@ -2,11 +2,11 @@ name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
on:
# Manual dispatch for urgent translation updates
# Manual dispatch for urgent translation updates
workflow_dispatch:
# Only trigger on PRs to main/master - additional branch filtering in job condition
pull_request:
branches: [main]
branches: [ main ]
types: [opened, synchronize, reopened]
jobs:
@@ -15,45 +15,45 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup ComfyUI Server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Setup playwright environment
- name: Setup ComfyUI Frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup ComfyUI Server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
# Update locales, collect new strings and update translations using OpenAI, then commit changes
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales"
git push origin HEAD:${{ github.head_ref }}
# Update locales, collect new strings and update translations using OpenAI, then commit changes
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales"
git push origin HEAD:${{ github.head_ref }}

View File

@@ -21,116 +21,116 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: 'true'
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: 'true'
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install custom node Python requirements
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install custom node Python requirements
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
# Start ComfyUI Server
- name: Start ComfyUI Server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
wait-for-it --service
# Start ComfyUI Server
- name: Start ComfyUI Server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
wait-for-it --service
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Capture base i18n
run: pnpm exec tsx scripts/diff-i18n capture
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Diff base vs updated i18n
run: pnpm exec tsx scripts/diff-i18n diff
- name: Update i18n in custom node repository
run: |
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
install -d "$LOCALE_DIR"
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
# Git ops for pushing changes and creating PR
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
echo "Fork failed - repository might already be forked"
# Exit 0 to prevent the workflow from failing
exit 0
}
- name: Capture base i18n
run: pnpm exec tsx scripts/diff-i18n capture
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Diff base vs updated i18n
run: pnpm exec tsx scripts/diff-i18n diff
- name: Update i18n in custom node repository
run: |
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
install -d "$LOCALE_DIR"
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
# Enable workflows on the forked repository
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
-F can_approve_pull_request_reviews=true \
-F default_workflow_permissions="write" \
-F enabled=true
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
# Git ops for pushing changes and creating PR
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
echo "Fork failed - repository might already be forked"
# Exit 0 to prevent the workflow from failing
exit 0
}
- name: Commit changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
# Enable workflows on the forked repository
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
-F can_approve_pull_request_reviews=true \
-F default_workflow_permissions="write" \
-F enabled=true
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
# Create and switch to new branch
git checkout -b update-locales
- name: Commit changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
# Stage and commit changes
git add -A
git commit -m "Update locales"
# Create and switch to new branch
git checkout -b update-locales
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
# github public key to confirm it's github server
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
# Stage and commit changes
git add -A
git commit -m "Update locales"
- name: Push changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Force push to create the branch
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
# github public key to confirm it's github server
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
- name: Push changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Force push to create the branch
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}

View File

@@ -13,42 +13,42 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"
title: "Update locales for node definitions"
body: |
Automated PR to update locales for node definitions
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"
title: "Update locales for node definitions"
body: |
Automated PR to update locales for node definitions
This PR was created automatically by the frontend update workflow.
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies
This PR was created automatically by the frontend update workflow.
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies

View File

@@ -19,8 +19,8 @@ on:
jobs:
backport:
if: >
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
@@ -38,19 +38,19 @@ jobs:
echo "::error::Invalid PR number format. Must be a positive integer."
exit 1
fi
# Validate PR exists and is merged
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
exit 1
fi
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
if [ "$MERGED" != "true" ]; then
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
exit 1
fi
# Validate PR has needs-backport label
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
@@ -164,7 +164,6 @@ jobs:
PENDING=()
SKIPPED=()
REUSED=()
for target in $REQUESTED_TARGETS; do
SAFE_TARGET=$(echo "$target" | tr '/' '-')
@@ -177,22 +176,10 @@ jobs:
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
OPEN_PR=$(
gh pr list \
--state open \
--head "${BACKPORT_BRANCH}" \
--json number \
--jq 'if length > 0 then .[0].number else "" end'
)
if [ -n "$OPEN_PR" ]; then
SKIPPED+=("${target} (PR #${OPEN_PR})")
continue
fi
REUSED+=("$BACKPORT_BRANCH")
SKIPPED+=("$target")
else
PENDING+=("$target")
fi
PENDING+=("$target")
done
SKIPPED_JOINED="${SKIPPED[*]:-}"
@@ -200,20 +187,16 @@ jobs:
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
echo "reused-branches=${REUSED[*]:-}" >> $GITHUB_OUTPUT
if [ -z "$PENDING_JOINED" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
if [ -n "$SKIPPED_JOINED" ]; then
echo "::warning::Backport branches exist: ${SKIPPED_JOINED}"
echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}"
fi
else
echo "skip=false" >> $GITHUB_OUTPUT
if [ -n "$SKIPPED_JOINED" ]; then
echo "::notice::Skipping backport targets: ${SKIPPED_JOINED}"
fi
if [ "${#REUSED[@]}" -gt 0 ]; then
echo "::notice::Reusing backport branches: ${REUSED[*]}"
echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}"
fi
fi
@@ -225,12 +208,7 @@ jobs:
run: |
FAILED=""
SUCCESS=""
CREATED_BRANCHES_FILE="$(
mktemp "$RUNNER_TEMP/backport-branches-XXXXXX"
)"
echo "CREATED_BRANCHES_FILE=$CREATED_BRANCHES_FILE" >> "$GITHUB_ENV"
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
@@ -245,12 +223,6 @@ jobs:
TARGET_BRANCH="${target}"
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
REMOTE_BACKPORT_EXISTS=false
if git ls-remote --exit-code origin "${BACKPORT_BRANCH}" >/dev/null 2>&1; then
REMOTE_BACKPORT_EXISTS=true
echo "::notice::Updating existing branch ${BACKPORT_BRANCH}"
fi
echo "::group::Backporting to ${TARGET_BRANCH}"
@@ -275,12 +247,7 @@ jobs:
# Try cherry-pick
if git cherry-pick "${MERGE_COMMIT}"; then
if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then
git push --force-with-lease origin "${BACKPORT_BRANCH}"
else
git push origin "${BACKPORT_BRANCH}"
fi
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
git push origin "${BACKPORT_BRANCH}"
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
# Return to main (keep the branch, we need it for PR)
@@ -304,13 +271,6 @@ jobs:
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
if [ -s "$CREATED_BRANCHES_FILE" ]; then
CREATED_LIST=$(paste -sd' ' "$CREATED_BRANCHES_FILE")
echo "created-branches=${CREATED_LIST}" >> $GITHUB_OUTPUT
else
echo "created-branches=" >> $GITHUB_OUTPUT
fi
if [ -n "${FAILED}" ]; then
exit 1
fi
@@ -330,7 +290,7 @@ jobs:
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r target branch <<< "${backport}"
@@ -388,25 +348,6 @@ jobs:
fi
done
- name: Cleanup stranded backport branches
if: steps.filter-targets.outputs.skip != 'true' && failure()
run: |
FILE="${CREATED_BRANCHES_FILE:-}"
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
echo "No backport branches recorded for cleanup"
exit 0
fi
while IFS= read -r branch; do
[ -z "$branch" ] && continue
printf 'Deleting branch %s\n' "${branch}"
if ! git push origin --delete "$branch"; then
echo "::warning::Failed to delete ${branch}"
fi
done < "$FILE"
- name: Remove needs-backport label
if: steps.filter-targets.outputs.skip != 'true' && success()
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"

View File

@@ -17,9 +17,40 @@ concurrency:
cancel-in-progress: true
jobs:
claude-review:
wait-for-ci:
runs-on: ubuntu-latest
if: github.event.label.name == 'claude-review'
outputs:
should-proceed: ${{ steps.check-status.outputs.proceed }}
steps:
- name: Wait for other CI checks
uses: lewagon/wait-on-check-action@e106e5c43e8ca1edea6383a39a01c5ca495fd812
with:
ref: ${{ github.event.pull_request.head.sha }}
check-regexp: '^(lint-and-format|test|playwright-tests)'
allowed-conclusions: success,skipped,failure,cancelled,neutral,action_required,timed_out,stale
wait-interval: 30
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if we should proceed
id: check-status
run: |
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
echo "Some CI checks failed - skipping Claude review"
echo "proceed=false" >> $GITHUB_OUTPUT
else
echo "All CI checks passed - proceeding with Claude review"
echo "proceed=true" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
claude-review:
needs: wait-for-ci
if: needs.wait-for-ci.outputs.should-proceed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository

View File

@@ -127,26 +127,26 @@ jobs:
echo "=========================================="
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
echo "=========================================="
# 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"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "✓ Found changed files:"
echo "$changed_files"
file_count=$(echo "$changed_files" | wc -l)
echo "Count: $file_count"
echo "has-changes=true" >> $GITHUB_OUTPUT
echo ""
# Create staging directory
mkdir -p /tmp/changed_snapshots_shard
# Copy only changed files, preserving directory structure
# Strip 'browser_tests/' prefix to avoid double nesting
echo "Copying changed files to staging directory..."
@@ -159,7 +159,7 @@ jobs:
cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix"
echo " → $file_without_prefix"
done <<< "$changed_files"
echo ""
echo "Staged files for upload:"
find /tmp/changed_snapshots_shard -type f
@@ -233,18 +233,18 @@ jobs:
shard_name=$(basename "$shard_dir")
file_count=$(find "$shard_dir" -type f | wc -l)
if [ "$file_count" -eq 0 ]; then
echo " $shard_name: no files"
continue
fi
echo "Processing $shard_name ($file_count file(s))..."
# Copy files directly, preserving directory structure
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
merged_count=$((merged_count + 1))
echo " ✓ Merged"
echo ""
@@ -272,25 +272,25 @@ jobs:
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if git diff --quiet browser_tests/; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "=========================================="
echo "COMMITTING CHANGES"
echo "=========================================="
echo "has-changes=true" >> $GITHUB_OUTPUT
git add browser_tests/
git commit -m "[automated] Update test expectations"
echo "Pushing to ${{ needs.setup.outputs.branch }}..."
git push origin ${{ needs.setup.outputs.branch }}
echo "✓ Commit and push successful"
- name: Add Done Reaction
@@ -306,4 +306,4 @@ jobs:
if: always() && github.event_name == 'pull_request'
run: gh pr edit ${{ needs.setup.outputs.pr-number }} --remove-label "New Browser Test Expectations"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,10 +1,9 @@
---
name: Publish Desktop UI on PR Merge
on:
pull_request:
types: ['closed']
branches: [main, core/*]
types: [ closed ]
branches: [ main, core/* ]
paths:
- 'apps/desktop-ui/package.json'
@@ -58,26 +57,3 @@ jobs:
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
comment_desktop_publish:
name: Comment Desktop Publish Summary
needs:
- resolve
- publish
if: success()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2
- name: Post desktop release summary comment
uses: ./.github/actions/comment-release-links
with:
issue-number: ${{ github.event.pull_request.number }}
version_file: apps/desktop-ui/package.json

View File

@@ -153,78 +153,8 @@ jobs:
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Ensure release labels
if: steps.check_version.outputs.is_minor_bump == 'true'
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BRANCH_BASE="${{ steps.check_version.outputs.branch_base }}"
if [[ -z "$BRANCH_BASE" ]]; then
echo "::error::Branch base not set; unable to manage labels"
exit 1
fi
declare -A COLORS=(
[core]="4361ee"
[cloud]="4f6ef5"
)
for PREFIX in core cloud; do
LABEL="${PREFIX}/${BRANCH_BASE}"
COLOR="${COLORS[$PREFIX]}"
DESCRIPTION="Backport PRs for ${PREFIX} ${BRANCH_BASE}"
if gh label view "$LABEL" >/dev/null 2>&1; then
gh label edit "$LABEL" \
--color "$COLOR" \
--description "$DESCRIPTION"
echo "🔄 Updated label $LABEL"
else
gh label create "$LABEL" \
--color "$COLOR" \
--description "$DESCRIPTION"
echo "✨ Created label $LABEL"
fi
done
MIN_LABELS_TO_KEEP=3
MAX_LABELS_TO_FETCH=200
for PREFIX in core cloud; do
mapfile -t LABELS < <(
gh label list \
--json name \
--limit "$MAX_LABELS_TO_FETCH" \
--jq '.[].name' |
grep -E "^${PREFIX}/[0-9]+\.[0-9]+$" |
sort -t/ -k2,2V
)
TOTAL=${#LABELS[@]}
if (( TOTAL <= MIN_LABELS_TO_KEEP )); then
echo " Nothing to prune for $PREFIX labels"
continue
fi
REMOVE_COUNT=$((TOTAL - MIN_LABELS_TO_KEEP))
if (( REMOVE_COUNT > 1 )); then
REMOVE_COUNT=1
fi
for ((i=0; i<REMOVE_COUNT; i++)); do
OLD_LABEL="${LABELS[$i]}"
gh label delete "$OLD_LABEL" --yes
echo "🗑️ Removed old label $OLD_LABEL"
done
done
- name: Post summary
if: always() && steps.check_version.outputs.is_minor_bump == 'true'
if: steps.check_version.outputs.is_minor_bump == 'true'
run: |
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
RESULTS="${{ steps.create_branches.outputs.results }}"

View File

@@ -1,10 +1,9 @@
---
name: Release Draft Create
on:
pull_request:
types: ['closed']
branches: [main, core/*]
types: [ closed ]
branches: [ main, core/* ]
paths:
- 'package.json'
@@ -31,9 +30,7 @@ jobs:
- name: Get current version
id: current_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Check if prerelease
id: check_prerelease
run: |
@@ -74,8 +71,7 @@ jobs:
name: dist-files
- name: Create release
id: create_release
uses: >-
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -83,14 +79,9 @@ jobs:
dist.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: >-
${{ github.event.pull_request.base.ref == 'main' &&
needs.build.outputs.is_prerelease == 'false' }}
draft: >-
${{ github.event.pull_request.base.ref != 'main' ||
needs.build.outputs.is_prerelease == 'true' }}
prerelease: >-
${{ needs.build.outputs.is_prerelease == 'true' }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' && needs.build.outputs.is_prerelease == 'false' }}
draft: ${{ github.event.pull_request.base.ref != 'main' || needs.build.outputs.is_prerelease == 'true' }}
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true
publish_pypi:
@@ -119,8 +110,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: >-
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
@@ -132,28 +122,3 @@ jobs:
version: ${{ needs.build.outputs.version }}
ref: ${{ github.event.pull_request.merge_commit_sha }}
secrets: inherit
comment_release_summary:
name: Comment Release Summary
needs:
- draft_release
- publish_pypi
- publish_types
if: success()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2
- name: Post release summary comment
uses: ./.github/actions/comment-release-links
with:
issue-number: ${{ github.event.pull_request.number }}
version_file: package.json

View File

@@ -59,7 +59,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Bump version
id: bump-version

View File

@@ -92,3 +92,4 @@ jobs:
base: ${{ github.event.inputs.branch }}
labels: |
Release

1
.gitignore vendored
View File

@@ -18,7 +18,6 @@ yarn.lock
.stylelintcache
node_modules
.pnpm-store
dist
dist-ssr
*.local

View File

@@ -1,43 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
".i18nrc.cjs",
"components.d.ts",
"lint-staged.config.js",
"vitest.setup.ts",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*",
"packages/registry-types/src/comfyRegistryTypes.ts",
"src/extensions/core/*",
"src/scripts/*",
"src/types/generatedManagerTypes.ts",
"src/types/vue-shim.d.ts"
],
"rules": {
"no-async-promise-executor": "off",
"no-control-regex": "off",
"no-eval": "off",
"no-self-assign": "allow",
"no-unused-expressions": "off",
"no-unused-private-class-members": "off",
"no-useless-rename": "off",
"typescript/no-this-alias": "off",
"typescript/no-unnecessary-parameter-property-assignment": "off",
"typescript/no-unsafe-declaration-merging": "off",
"typescript/no-unused-vars": "off",
"unicorn/no-empty-file": "off",
"unicorn/no-new-array": "off",
"unicorn/no-single-promise-in-promise-methods": "off",
"unicorn/no-useless-fallback-in-spread": "off",
"unicorn/no-useless-spread": "off",
"typescript/await-thenable": "off",
"typescript/no-base-to-string": "off",
"typescript/no-duplicate-type-constituents": "off",
"typescript/no-for-in-array": "off",
"typescript/no-meaningless-void-operator": "off",
"typescript/no-redundant-type-constituents": "off",
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error"
}
}

View File

@@ -74,15 +74,8 @@ const config: StorybookConfig = {
'@': process.cwd() + '/src'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -1,10 +0,0 @@
extends: default
ignore: |
node_modules/
dist/
rules:
line-length: disable
document-start: disable
truthy: disable

View File

@@ -17,7 +17,6 @@ This bootstraps the monorepo with dependencies, builds, tests, and dev server ve
- `pnpm typecheck`: Type checking
- `pnpm build`: Build for production (via nx)
- `pnpm lint`: Linting (via nx)
- `pnpm oxlint`: Fast Rust-based linting with Oxc
- `pnpm format`: Prettier formatting
- `pnpm test:unit`: Run all unit tests
- `pnpm test:browser`: Run E2E tests via Playwright

View File

@@ -243,7 +243,7 @@ pnpm format
### 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` theme, e.g. `bg-node-component-surface`
- Follow the existing dark theme pattern: `dark-theme:` prefix (not `dark:`)
### Internationalization
- All user-facing strings must use vue-i18n

View File

@@ -75,15 +75,8 @@ const config: StorybookConfig = {
'@frontend-locales': process.cwd() + '/../../src/locales'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -1,206 +0,0 @@
<template>
<Select
:id="dropdownId"
v-model="selectedLocale"
:options="localeOptions"
option-label="label"
option-value="value"
:disabled="isSwitching"
:pt="dropdownPt"
:size="props.size"
class="language-selector"
@change="onLocaleChange"
>
<template #value="{ value }">
<span :class="valueClass">
<i class="pi pi-language" :class="iconClass" />
<span>{{ displayLabel(value as SupportedLocale) }}</span>
</span>
</template>
<template #option="{ option }">
<span :class="optionClass">
<i class="pi pi-language" :class="iconClass" />
<span class="leading-none">{{ option.label }}</span>
</span>
</template>
</Select>
</template>
<script setup lang="ts">
import Select from 'primevue/select'
import type { SelectChangeEvent } from 'primevue/select'
import { computed, ref, watch } from 'vue'
import { i18n, loadLocale, st } from '@/i18n'
type VariantKey = 'dark' | 'light'
type SizeKey = 'small' | 'large'
const props = withDefaults(
defineProps<{
variant?: VariantKey
size?: SizeKey
}>(),
{
variant: 'dark',
size: 'small'
}
)
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
const LOCALES = [
['en', 'English'],
['zh', '中文'],
['zh-TW', '繁體中文'],
['ru', 'Русский'],
['ja', '日本語'],
['ko', '한국어'],
['fr', 'Français'],
['es', 'Español'],
['ar', 'عربي'],
['tr', 'Türkçe']
] as const satisfies ReadonlyArray<[string, string]>
type SupportedLocale = (typeof LOCALES)[number][0]
const SIZE_PRESETS = {
large: {
wrapper: 'px-3 py-1 min-w-[7rem]',
gap: 'gap-2',
valueText: 'text-xs',
optionText: 'text-sm',
icon: 'text-sm'
},
small: {
wrapper: 'px-2 py-0.5 min-w-[5rem]',
gap: 'gap-1',
valueText: 'text-[0.65rem]',
optionText: 'text-xs',
icon: 'text-xs'
}
} as const satisfies Record<SizeKey, Record<string, string>>
const VARIANT_PRESETS = {
light: {
root: 'bg-white/80 border border-neutral-200 text-neutral-700 rounded-full shadow-sm backdrop-blur hover:border-neutral-400 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-white',
trigger: 'text-neutral-500 hover:text-neutral-700',
item: 'text-neutral-700 bg-transparent hover:bg-neutral-100 focus-visible:outline-none',
valueText: 'text-neutral-600',
optionText: 'text-neutral-600',
icon: 'text-neutral-500'
},
dark: {
root: 'bg-neutral-900/70 border border-neutral-700 text-neutral-200 rounded-full shadow-sm backdrop-blur hover:border-neutral-500 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900',
trigger: 'text-neutral-400 hover:text-neutral-200',
item: 'text-neutral-200 bg-transparent hover:bg-neutral-800/80 focus-visible:outline-none',
valueText: 'text-neutral-100',
optionText: 'text-neutral-100',
icon: 'text-neutral-300'
}
} as const satisfies Record<VariantKey, Record<string, string>>
const selectedLocale = ref<string>(i18n.global.locale.value)
const isSwitching = ref(false)
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
const variantPreset = computed(
() => VARIANT_PRESETS[props.variant as VariantKey]
)
const dropdownPt = computed(() => ({
root: {
class: `${variantPreset.value.root} ${sizePreset.value.wrapper}`
},
trigger: {
class: variantPreset.value.trigger
},
item: {
class: `${variantPreset.value.item} ${sizePreset.value.optionText}`
}
}))
const valueClass = computed(() =>
[
'flex items-center font-medium uppercase tracking-wide leading-tight',
sizePreset.value.gap,
sizePreset.value.valueText,
variantPreset.value.valueText
].join(' ')
)
const optionClass = computed(() =>
[
'flex items-center leading-tight',
sizePreset.value.gap,
variantPreset.value.optionText,
sizePreset.value.optionText
].join(' ')
)
const iconClass = computed(() =>
[sizePreset.value.icon, variantPreset.value.icon].join(' ')
)
const localeOptions = computed(() =>
LOCALES.map(([value, fallback]) => ({
value,
label: st(`settings.Comfy_Locale.options.${value}`, fallback)
}))
)
const labelLookup = computed(() =>
localeOptions.value.reduce<Record<string, string>>((acc, option) => {
acc[option.value] = option.label
return acc
}, {})
)
function displayLabel(locale?: SupportedLocale) {
if (!locale) {
return st('settings.Comfy_Locale.name', 'Language')
}
return labelLookup.value[locale] ?? locale
}
watch(
() => i18n.global.locale.value,
(newLocale) => {
if (newLocale !== selectedLocale.value) {
selectedLocale.value = newLocale
}
}
)
async function onLocaleChange(event: SelectChangeEvent) {
const nextLocale = event.value as SupportedLocale | undefined
if (!nextLocale || nextLocale === i18n.global.locale.value) {
return
}
isSwitching.value = true
try {
await loadLocale(nextLocale)
i18n.global.locale.value = nextLocale
} catch (error) {
console.error(`Failed to change locale to "${nextLocale}"`, error)
selectedLocale.value = i18n.global.locale.value
} finally {
isSwitching.value = false
}
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.p-dropdown-panel .p-dropdown-item) {
@apply transition-colors;
}
:deep(.p-dropdown) {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<BaseViewTemplate dark hide-language-selector>
<BaseViewTemplate dark>
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
@@ -71,8 +71,8 @@ const updateConsent = async () => {
} catch (error) {
toast.add({
severity: 'error',
summary: t('install.settings.errorUpdatingConsent'),
detail: t('install.settings.errorUpdatingConsentDetail'),
summary: t('install.errorUpdatingConsent'),
detail: t('install.errorUpdatingConsentDetail'),
life: 3000
})
} finally {

View File

@@ -1,7 +1,7 @@
<template>
<BaseViewTemplate dark>
<div class="flex items-center justify-center min-h-screen">
<div class="grid gap-8">
<div class="grid grid-rows-2 gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img

View File

@@ -1,15 +1,12 @@
<template>
<div
class="font-sans w-screen h-screen flex flex-col relative"
class="font-sans w-screen h-screen flex flex-col"
:class="[
dark
? 'text-neutral-300 bg-neutral-900 dark-theme'
: 'text-neutral-900 bg-neutral-300'
]"
>
<div v-if="showLanguageSelector" class="absolute top-6 right-6 z-10">
<LanguageSelector :variant="variant" />
</div>
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow()"
@@ -23,20 +20,14 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
import LanguageSelector from '@/components/common/LanguageSelector.vue'
import { nextTick, onMounted, ref } from 'vue'
import { electronAPI, isElectron, isNativeWindow } from '../../utils/envUtil'
const { dark = false, hideLanguageSelector = false } = defineProps<{
const { dark = false } = defineProps<{
dark?: boolean
hideLanguageSelector?: boolean
}>()
const variant = computed(() => (dark ? 'dark' : 'light'))
const showLanguageSelector = computed(() => !hideLanguageSelector)
const darkTheme = {
color: 'rgba(0, 0, 0, 0)',
symbolColor: '#d4d4d4'

View File

@@ -3,8 +3,9 @@
"compilerOptions": {
"noEmit": true,
"allowImportingTsExtensions": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/*": ["src/*"],
"@frontend-locales/*": ["../../src/locales/*"]
}
},

View File

@@ -1,20 +1,17 @@
import type { FullConfig } from '@playwright/test'
import { config as loadEnv } from 'dotenv'
import dotenv from 'dotenv'
import { backupPath } from './utils/backupUtils'
import { syncDevtools } from './utils/devtoolsSync'
loadEnv()
dotenv.config()
export default function globalSetup(_: FullConfig) {
export default function globalSetup(config: FullConfig) {
if (!process.env.CI) {
if (process.env.TEST_COMFYUI_DIR) {
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
renameAndReplaceWithScaffolding: true
})
syncDevtools(process.env.TEST_COMFYUI_DIR)
} else {
console.warn(
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'

View File

@@ -0,0 +1,144 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
interface ChatHistoryEntry {
prompt: string
response: string
response_id: string
}
async function renderChatHistory(page: Page, history: ChatHistoryEntry[]) {
const nodeId = await page.evaluate(() => window['app'].graph.nodes[0]?.id)
// Simulate API sending display_component message
await page.evaluate(
({ nodeId, history }) => {
const event = new CustomEvent('display_component', {
detail: {
node_id: nodeId,
component: 'ChatHistoryWidget',
props: {
history: JSON.stringify(history)
}
}
})
window['app'].api.dispatchEvent(event)
return true
},
{ nodeId, history }
)
return nodeId
}
test.describe('Chat History Widget', () => {
let nodeId: string
test.beforeEach(async ({ comfyPage }) => {
nodeId = await renderChatHistory(comfyPage.page, [
{ prompt: 'Hello', response: 'World', response_id: '123' }
])
// Wait for chat history to be rendered
await comfyPage.page.waitForSelector('.pi-pencil')
})
test('displays chat history when receiving display_component message', async ({
comfyPage
}) => {
// Verify the chat history is displayed correctly
await expect(comfyPage.page.getByText('Hello')).toBeVisible()
await expect(comfyPage.page.getByText('World')).toBeVisible()
})
test('handles message editing interaction', async ({ comfyPage }) => {
// Get first node's ID
nodeId = await comfyPage.page.evaluate(() => {
const node = window['app'].graph.nodes[0]
// Make sure the node has a prompt widget (for editing functionality)
if (!node.widgets) {
node.widgets = []
}
// Add a prompt widget if it doesn't exist
if (!node.widgets.find((w) => w.name === 'prompt')) {
node.widgets.push({
name: 'prompt',
type: 'text',
value: 'Original prompt'
})
}
return node.id
})
await renderChatHistory(comfyPage.page, [
{
prompt: 'Message 1',
response: 'Response 1',
response_id: '123'
},
{
prompt: 'Message 2',
response: 'Response 2',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
const originalTextAreaInput = await comfyPage.page
.getByPlaceholder('text')
.nth(1)
.inputValue()
// Click edit button on first message
await comfyPage.page.getByLabel('Edit').first().click()
await comfyPage.nextFrame()
// Verify cancel button appears
await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible()
// Click cancel edit
await comfyPage.page.getByLabel('Cancel').click()
// Verify prompt input is restored
await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue(
originalTextAreaInput
)
})
test('handles real-time updates to chat history', async ({ comfyPage }) => {
// Send initial history
await renderChatHistory(comfyPage.page, [
{
prompt: 'Initial message',
response: 'Initial response',
response_id: '123'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
// Update history with additional messages
await renderChatHistory(comfyPage.page, [
{
prompt: 'Follow-up',
response: 'New response',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
// Move mouse over the canvas to force update
await comfyPage.page.mouse.move(100, 100)
await comfyPage.nextFrame()
// Verify new messages appear
await expect(comfyPage.page.getByText('Follow-up')).toBeVisible()
await expect(comfyPage.page.getByText('New response')).toBeVisible()
})
})

View File

@@ -88,10 +88,6 @@ test.describe('Missing models warning', () => {
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
// Check that the copy URL button is also visible for Desktop environment
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
await expect(copyUrlButton).toBeVisible()
})
test('Should display a warning when missing models are found in node properties', async ({
@@ -105,10 +101,6 @@ test.describe('Missing models warning', () => {
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
// Check that the copy URL button is also visible for Desktop environment
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
await expect(copyUrlButton).toBeVisible()
})
test('Should not display a warning when no missing models are found', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

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

After

Width:  |  Height:  |  Size: 64 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: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -1,52 +0,0 @@
import fs from 'fs-extra'
import path from 'path'
import { fileURLToPath } from 'url'
export function syncDevtools(targetComfyDir: string): boolean {
if (!targetComfyDir) {
console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
return false
}
// Validate and sanitize the target directory path
const resolvedTargetDir = path.resolve(targetComfyDir)
// Basic path validation to prevent directory traversal
if (resolvedTargetDir.includes('..') || !path.isAbsolute(resolvedTargetDir)) {
console.error('syncDevtools failed: Invalid target directory path')
return false
}
const moduleDir =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url))
const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')
if (!fs.pathExistsSync(devtoolsSrc)) {
console.warn(
`syncDevtools skipped: source directory not found at ${devtoolsSrc}`
)
return false
}
const devtoolsDest = path.resolve(
resolvedTargetDir,
'custom_nodes',
'ComfyUI_devtools'
)
console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)
try {
fs.removeSync(devtoolsDest)
fs.ensureDirSync(devtoolsDest)
fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
console.warn('syncDevtools: copy complete')
return true
} catch (error) {
console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
return false
}
}

View File

@@ -3,7 +3,6 @@ import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import { importX } from 'eslint-plugin-import-x'
import oxlint from 'eslint-plugin-oxlint'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
@@ -34,18 +33,7 @@ const settings = {
],
noWarnOnMultipleProjects: true
})
],
'vue-i18n': {
localeDir: [
{
pattern: './src/locales/**/*.json',
localeKey: 'path',
localePattern:
/^\.?\/?src\/locales\/(?<locale>[A-Za-z0-9-]+)\/.+\.json$/
}
],
messageSyntaxVersion: '^9.0.0'
}
]
} as const
const commonParserOptions = {
@@ -106,23 +94,19 @@ export default defineConfig([
// @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'],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
// @ts-expect-error Bad types in the plugin
importX.flatConfigs.recommended,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
// @ts-expect-error Bad types in the plugin
importX.flatConfigs.typescript,
{
plugins: {
'unused-imports': unusedImports,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility in i18n plugin
// @ts-expect-error Bad types in the plugin
'@intlify/vue-i18n': pluginI18n
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
@@ -140,8 +124,8 @@ export default defineConfig([
'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)?:/'],
// Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'],
'vue/multi-word-component-names': 'off', // TODO: fix
'vue/no-template-shadow': 'off', // TODO: fix
'vue/match-component-import-name': 'error',
@@ -275,7 +259,5 @@ export default defineConfig([
'@typescript-eslint/no-floating-promises': 'off',
'no-console': 'off'
}
},
// Turn off ESLint rules that are already handled by oxlint
...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json')
}
])

14
global.d.ts vendored
View File

@@ -8,21 +8,7 @@ declare const __USE_PROD_CONFIG__: boolean
interface Window {
__CONFIG__: {
mixpanel_token?: string
require_whitelist?: boolean
subscription_required?: boolean
max_upload_size?: number
comfy_api_base_url?: string
comfy_platform_base_url?: string
firebase_config?: {
apiKey: string
authDomain: string
databaseURL?: string
projectId: string
storageBucket: string
messagingSenderId: string
appId: string
measurementId?: string
}
server_health_alert?: {
message: string
tooltip?: string

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.32.5",
"version": "1.32.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -16,7 +16,6 @@
"size:collect": "node scripts/size-collect.js",
"size:report": "node scripts/size-report.js",
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"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": "nx serve",
@@ -28,14 +27,13 @@
"json-schema": "tsx scripts/generate-json-schema.ts",
"knip:no-cache": "knip",
"knip": "knip --cache",
"lint:fix:no-cache": "oxlint src --type-aware --fix && eslint src --fix",
"lint:fix": "oxlint src --type-aware --fix && eslint src --cache --fix",
"lint:no-cache": "oxlint src --type-aware && eslint src",
"lint:fix:no-cache": "eslint src --fix",
"lint:fix": "eslint src --cache --fix",
"lint:no-cache": "eslint src",
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint": "oxlint src --type-aware && eslint src --cache",
"lint": "eslint src --cache",
"locale": "lobe-i18n locale",
"oxlint": "oxlint src --type-aware",
"preinstall": "pnpm dlx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
@@ -43,7 +41,6 @@
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
"test:browser": "pnpm exec nx e2e",
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 pnpm test:browser",
"test:unit": "nx run test",
"typecheck": "vue-tsc --noEmit",
"zipdist": "node scripts/zipdist.js",
@@ -80,7 +77,6 @@
"eslint-config-prettier": "catalog:",
"eslint-import-resolver-typescript": "catalog:",
"eslint-plugin-import-x": "catalog:",
"eslint-plugin-oxlint": "catalog:",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
@@ -96,8 +92,6 @@
"markdown-table": "catalog:",
"mixpanel-browser": "catalog:",
"nx": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"picocolors": "catalog:",
"postcss-html": "catalog:",
"prettier": "catalog:",
@@ -131,7 +125,6 @@
"@comfyorg/comfyui-electron-types": "0.4.73-0",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/shared-frontend-utils": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@iconify/json": "catalog:",
"@primeuix/forms": "catalog:",

View File

@@ -73,7 +73,6 @@
--color-jade-400: #47e469;
--color-jade-600: #00cd72;
--color-graphite-400: #9C9EAB;
--color-gold-400: #fcbf64;
--color-gold-500: #fdab34;
@@ -160,6 +159,7 @@
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-smoke-300);
/* Code styling colors for help menu*/
--code-text-color: rgb(0 122 255 / 1);
@@ -227,7 +227,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-200);
--secondary-background-hover: var(--color-smoke-400);
--secondary-background-selected: var(--color-smoke-600);
--base-background: var(--color-white);
--primary-background: var(--color-azure-400);
@@ -241,36 +241,6 @@
--border-subtle: var(--color-smoke-400);
--muted-background: var(--color-smoke-700);
--accent-background: var(--color-smoke-800);
/* Component/Node tokens from design system light */
--component-node-background: var(--color-white);
--component-node-border: var(--color-border-default);
--component-node-foreground: var(--base-foreground);
--component-node-foreground-secondary: var(--color-muted-foreground);
--component-node-widget-background: var(--secondary-background);
--component-node-widget-background-hovered: var(--secondary-background-hover);
--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);
/* Default UI element color palette variables */
--palette-contrast-mix-color: #fff;
--palette-interface-panel-surface: var(--comfy-menu-bg);
--palette-interface-stroke: color-mix(in srgb, var(--interface-panel-surface) 75.5%, var(--contrast-mix-color));
--palette-interface-panel-box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.4);
--palette-interface-panel-drop-shadow: 1px 1px 4px rgb(0 0 0 / 0.4);
--palette-interface-panel-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 92.5%, var(--contrast-mix-color));
--palette-interface-panel-selected-surface: color-mix(in srgb, var(--interface-panel-surface) 87.5%, var(--contrast-mix-color));
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
--modal-card-background: var(--secondary-background);
--modal-card-button-surface: var(--color-smoke-300);
--modal-card-placeholder-background: var(--color-smoke-600);
--modal-card-tag-background: var(--color-smoke-200);
--modal-card-tag-foreground: var(--base-foreground);
--modal-panel-background: var(--color-white);
}
.dark-theme {
@@ -292,6 +262,8 @@
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-charcoal-300);
--dialog-surface: var(--color-neutral-700);
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
@@ -317,7 +289,7 @@
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-600);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-charcoal-600);
--node-component-surface: var(--color-charcoal-800);
--node-component-tooltip: var(--color-white);
--node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800);
@@ -355,25 +327,6 @@
--border-subtle: var(--color-charcoal-300);
--muted-background: var(--color-charcoal-100);
--accent-background: var(--color-charcoal-100);
/* Component/Node tokens from design dark system */
--component-node-background: var(--color-charcoal-600);
--component-node-border: var(--color-charcoal-100);
--component-node-foreground: var(--base-foreground);
--component-node-foreground-secondary: var(--color-muted-foreground);
--component-node-widget-background: var(--secondary-background-hover);
--component-node-widget-background-hovered: var(--secondary-background-selected);
--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);
--modal-card-background: var(--secondary-background);
--modal-card-button-surface: var(--color-charcoal-300);
--modal-card-placeholder-background: var(--secondary-background);
--modal-card-tag-background: var(--color-charcoal-200);
--modal-card-tag-foreground: var(--base-foreground);
--modal-panel-background: var(--color-charcoal-600);
}
@theme inline {
@@ -384,14 +337,7 @@
--color-button-surface: var(--button-surface);
--color-button-surface-contrast: var(--button-surface-contrast);
--color-subscription-button-gradient: var(--subscription-button-gradient);
--color-modal-card-background: var(--modal-card-background);
--color-modal-card-button-surface: var(--modal-card-button-surface);
--color-modal-card-placeholder-background: var(--modal-card-placeholder-background);
--color-modal-card-tag-background: var(--modal-card-tag-background);
--color-modal-card-tag-foreground: var(--modal-card-tag-foreground);
--color-modal-panel-background: var(--modal-panel-background);
--color-dialog-surface: var(--dialog-surface);
--color-interface-menu-component-surface-hovered: var(
--interface-menu-component-surface-hovered
@@ -403,14 +349,6 @@
--interface-menu-keybind-surface-default
);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-panel-hover-surface: var(--interface-panel-hover-surface);
--color-interface-panel-selected-surface: var(
--interface-panel-selected-surface
);
--color-interface-button-hover-surface: var(
--interface-button-hover-surface
);
--color-comfy-menu-bg: var(--comfy-menu-bg);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
@@ -456,17 +394,6 @@
--color-text-primary: var(--text-primary);
--color-input-surface: var(--input-surface);
/* Component/Node design tokens */
--color-component-node-background: var(--component-node-background);
--color-component-node-border: var(--component-node-border);
--color-component-node-foreground: var(--component-node-foreground);
--color-component-node-foreground-secondary: var(--component-node-foreground-secondary);
--color-component-node-widget-background: var(--component-node-widget-background);
--color-component-node-widget-background-hovered: var(--component-node-widget-background-hovered);
--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);
/* Semantic tokens */
--color-base-foreground: var(--base-foreground);
--color-muted-foreground: var(--muted-foreground);
@@ -570,40 +497,36 @@ body {
.comfy-markdown {
/* We assign the textarea and the Tiptap editor to the same CSS grid area to stack them on top of one another. */
display: grid;
& > textarea,
.tiptap {
grid-area: 1 / 1 / 2 / 2;
}
}
& > textarea {
opacity: 0;
pointer-events: none;
}
.comfy-markdown > textarea {
grid-area: 1 / 1 / 2 / 2;
}
&.editing {
& > textarea {
opacity: 1;
pointer-events: all;
}
.comfy-markdown .tiptap {
grid-area: 1 / 1 / 2 / 2;
background-color: var(--comfy-input-bg);
color: var(--input-text);
overflow: hidden;
overflow-y: auto;
resize: none;
border: none;
box-sizing: border-box;
font-size: var(--comfy-textarea-font-size);
height: 100%;
padding: 0.5em;
}
.tiptap {
opacity: 0;
pointer-events: none;
}
}
.comfy-markdown.editing .tiptap {
display: none;
}
.tiptap {
overflow-y: auto;
font-size: var(--comfy-textarea-font-size);
.comfy-markdown .tiptap :first-child {
margin-top: 0;
}
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 0;
}
}
.comfy-markdown .tiptap :last-child {
margin-bottom: 0;
}
.comfy-markdown .tiptap blockquote {
@@ -1365,466 +1288,3 @@ audio.comfy-audio.empty-audio-widget {
border-radius: 0;
}
/* END LOD specific styles */
/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {
position: absolute;
backgroundColor: transparent;
z-index: 8889;
pointer-events: none;
border-radius: 50%;
overflow: visible;
outline: 1px dashed black;
box-shadow: 0 0 0 1px white;
}
#maskEditor_brushPreviewGradient {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
display: none;
}
.maskEditor_sidePanelTitle {
text-align: center;
font-size: 15px;
font-family: sans-serif;
color: var(--descrip-text);
margin-top: 10px;
}
.maskEditor_sidePanelBrushShapeCircle {
width: 35px;
height: 35px;
border-radius: 50%;
border: 1px solid var(--border-color);
pointer-events: auto;
transition: background 0.1s;
margin-left: 7.5px;
}
.maskEditor_sidePanelBrushRange {
width: 180px;
appearance: none;
background: transparent;
cursor: pointer;
}
.maskEditor_sidePanelBrushRange::-webkit-slider-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
cursor: grab;
margin-top: -8px;
background: var(--p-surface-700);
border: 1px solid var(--border-color);
}
.maskEditor_sidePanelBrushRange::-moz-range-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
cursor: grab;
background: var(--p-surface-800);
border: 1px solid var(--border-color);
}
.maskEditor_sidePanelBrushRange::-webkit-slider-runnable-track {
background: var(--p-surface-700);
height: 3px;
}
.maskEditor_sidePanelBrushRange::-moz-range-track {
background: var(--p-surface-700);
height: 3px;
}
.maskEditor_sidePanelBrushShapeSquare {
width: 35px;
height: 35px;
margin: 5px;
border: 1px solid var(--border-color);
pointer-events: auto;
transition: background 0.1s;
}
.maskEditor_brushShape_dark {
background: transparent;
}
.maskEditor_brushShape_dark:hover {
background: var(--p-surface-900);
}
.maskEditor_brushShape_light {
background: transparent;
}
.maskEditor_brushShape_light:hover {
background: var(--comfy-menu-bg);
}
.maskEditor_sidePanelLayer {
display: flex;
width: 100%;
height: 50px;
}
.maskEditor_sidePanelLayerVisibilityContainer {
width: 50px;
height: 50px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
}
.maskEditor_sidePanelVisibilityToggle {
width: 12px;
height: 12px;
border-radius: 50%;
pointer-events: auto;
}
.maskEditor_sidePanelLayerIconContainer {
width: 60px;
height: 50px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
fill: var(--input-text);
}
.maskEditor_sidePanelLayerIconContainer svg {
width: 30px;
height: 30px;
}
.maskEditor_sidePanelBigButton {
width: 85px;
height: 30px;
background: rgb(0 0 0 / 0.2);
border: 1px solid var(--border-color);
color: var(--input-text);
font-family: sans-serif;
font-size: 15px;
pointer-events: auto;
transition: background-color 0.1s;
}
.maskEditor_sidePanelBigButton:hover {
background-color: var(--p-overlaybadge-outline-color);
border: none;
}
.maskEditor_toolPanelContainer {
width: 4rem;
height: 4rem;
display: flex;
justify-content: center;
align-items: center;
position: relative;
transition: background-color 0.2s;
}
.maskEditor_toolPanelContainerSelected svg {
fill: var(--p-button-text-primary-color) !important;
}
.maskEditor_toolPanelContainerSelected .maskEditor_toolPanelIndicator {
display: block;
}
.maskEditor_toolPanelContainer svg {
width: 75%;
aspect-ratio: 1/1;
fill: var(--p-button-text-secondary-color);
}
.maskEditor_toolPanelContainerDark:hover {
background-color: var(--p-surface-800);
}
.maskEditor_toolPanelContainerLight:hover {
background-color: var(--p-surface-300);
}
.maskEditor_toolPanelIndicator {
display: none;
height: 100%;
width: 4px;
position: absolute;
left: 0;
background: var(--p-button-text-primary-color);
}
.maskEditor_sidePanelSeparator {
width: 100%;
height: 2px;
background: var(--border-color);
margin-top: 1.5em;
margin-bottom: 5px;
}
#maskEditorCanvasContainer {
position: absolute;
width: 1000px;
height: 667px;
left: 359px;
top: 280px;
}
.maskEditor_topPanelIconButton_dark {
width: 50px;
height: 30px;
pointer-events: auto;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.1s;
background: var(--p-surface-800);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
}
.maskEditor_topPanelIconButton_dark:hover {
background-color: var(--p-surface-900);
}
.maskEditor_topPanelIconButton_dark svg {
width: 25px;
height: 25px;
pointer-events: none;
fill: var(--input-text);
}
.maskEditor_topPanelIconButton_light {
width: 50px;
height: 30px;
pointer-events: auto;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.1s;
background: var(--comfy-menu-bg);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
}
.maskEditor_topPanelIconButton_light:hover {
background-color: var(--p-surface-300);
}
.maskEditor_topPanelIconButton_light svg {
width: 25px;
height: 25px;
pointer-events: none;
fill: var(--input-text);
}
.maskEditor_topPanelButton_dark {
height: 30px;
background: var(--p-surface-800);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
color: var(--input-text);
font-family: sans-serif;
pointer-events: auto;
transition: 0.1s;
width: 60px;
}
.maskEditor_topPanelButton_dark:hover {
background-color: var(--p-surface-900);
}
.maskEditor_topPanelButton_light {
height: 30px;
background: var(--comfy-menu-bg);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
color: var(--input-text);
font-family: sans-serif;
pointer-events: auto;
transition: 0.1s;
width: 60px;
}
.maskEditor_topPanelButton_light:hover {
background-color: var(--p-surface-300);
}
.maskEditor_sidePanel_paintBucket_Container {
width: 180px;
display: flex;
flex-direction: column;
position: relative;
}
.maskEditor_sidePanel_colorSelect_Container {
display: flex;
width: 180px;
align-items: center;
gap: 5px;
height: 30px;
}
.maskEditor_sidePanel_colorSelect_tolerance_container {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 10px;
}
.maskEditor_sidePanelContainerColumn {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 12px;
}
.maskEditor_sidePanelContainerRow {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
min-height: 24px;
position: relative;
}
.maskEditor_accent_bg_dark {
background: var(--p-surface-800);
}
.maskEditor_accent_bg_very_dark {
background: var(--p-surface-900);
}
.maskEditor_accent_bg_light {
background: var(--p-surface-300);
}
.maskEditor_accent_bg_very_light {
background: var(--comfy-menu-bg);
}
.maskEditor_sidePanelToggleContainer {
cursor: pointer;
display: inline-block;
position: absolute;
right: 0;
}
.maskEditor_sidePanelToggleSwitch {
display: inline-block;
border-radius: 16px;
width: 40px;
height: 24px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
background: var(--p-surface-300);
}
.dark-theme .maskEditor_sidePanelToggleSwitch {
background: var(--p-surface-700);
}
.maskEditor_sidePanelToggleSwitch::before, .maskEditor_sidePanelToggleSwitch::after {
content: "";
}
.maskEditor_sidePanelToggleSwitch::before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
width: 16px;
height: 16px;
position: absolute;
top: 4px;
left: 4px;
transition: ease 0.2s;
}
.maskEditor_sidePanelToggleContainer:hover .maskEditor_sidePanelToggleSwitch::before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
}
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch {
background: var(--p-button-text-primary-color);
}
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
background: var(--comfy-menu-bg);
left: 20px;
}
.dark-theme .maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
background: var(--p-surface-900);
}
.maskEditor_sidePanelToggleCheckbox {
position: absolute;
visibility: hidden;
}
.maskEditor_sidePanelDropdown {
border: 1px solid var(--p-form-field-border-color);
background: var(--comfy-menu-bg);
height: 24px;
padding-left: 5px;
padding-right: 5px;
border-radius: 6px;
transition: background 0.1s;
}
.maskEditor_sidePanelDropdown option {
background: var(--comfy-menu-bg);
}
.maskEditor_sidePanelDropdown:focus {
outline: 1px solid var(--p-surface-300);
}
.maskEditor_sidePanelDropdown option:hover {
background: white;
}
.maskEditor_sidePanelDropdown option:active {
background: var(--p-surface-300);
}
.dark-theme .maskEditor_sidePanelDropdown {
background: var(--p-surface-900);
}
.dark-theme .maskEditor_sidePanelDropdown option {
background: var(--p-surface-900);
}
.dark-theme .maskEditor_sidePanelDropdown:focus {
outline: 1px solid var(--p-button-text-primary-color);
}
.dark-theme .maskEditor_sidePanelDropdown option:active {
background: var(--p-highlight-background);
}
.maskEditor_layerRow {
height: 50px;
width: 100%;
border-radius: 10px;
}
.maskEditor_sidePanelLayerPreviewContainer {
width: 40px;
height: 30px;
}
.maskEditor_sidePanelLayerPreviewContainer > svg{
width: 100%;
height: 100%;
object-fit: contain;
fill: var(--p-surface-100);
}
.maskEditor_sidePanelImageLayerImage {
width: 100%;
height: 100%;
object-fit: contain;
}
.maskEditor_sidePanelSubTitle {
text-align: left;
font-size: 12px;
font-family: sans-serif;
color: var(--descrip-text);
}
.maskEditor_containerDropdown {
position: absolute;
right: 0;
}
.maskEditor_sidePanelLayerCheckbox {
margin-left: 15px;
}
/* ===================== End of Mask Editor Styles ===================== */

View File

@@ -1,4 +0,0 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.13727 11.2201L6.27454 14.4398" stroke="var(--pin-color, var(--color-smoke-800, #8a8a8a))" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
<path d="M6.28085 6.68385C6.21652 6.92342 6.08664 7.1403 5.9058 7.31009C5.72497 7.47989 5.50035 7.59587 5.25721 7.645L3.95569 7.91742C3.71254 7.96655 3.48793 8.08253 3.30709 8.25233C3.12626 8.42212 2.99637 8.639 2.93204 8.87857L2.80091 9.36797C2.75515 9.53876 2.7791 9.72073 2.86751 9.87385C2.95591 10.027 3.10153 10.1387 3.27231 10.1845L10.9997 12.255C11.1705 12.3008 11.3525 12.2768 11.5056 12.1884C11.6587 12.1 11.7705 11.9544 11.8162 11.7836L11.9474 11.2942C12.0114 11.0546 12.0074 10.8018 11.9357 10.5643C11.864 10.3269 11.7274 10.1141 11.5414 9.95001L10.5505 9.06333C10.3645 8.89921 10.2279 8.68646 10.1562 8.44899C10.0845 8.21153 10.0805 7.95877 10.1446 7.71913L10.7933 5.29787C10.8391 5.12709 10.9508 4.98148 11.1039 4.89307C11.2571 4.80466 11.439 4.78071 11.6098 4.82647C11.9514 4.91799 12.3153 4.87008 12.6216 4.69327C12.9278 4.51646 13.1513 4.22523 13.2428 3.88366C13.3343 3.54209 13.2864 3.17815 13.1096 2.8719C12.9328 2.56566 12.6416 2.34219 12.3 2.25067L7.1484 0.870299C6.80683 0.778775 6.44289 0.826689 6.13665 1.0035C5.8304 1.18031 5.60694 1.47154 5.51541 1.81311C5.42389 2.15468 5.4718 2.51862 5.64861 2.82487C5.82542 3.13111 6.11665 3.35458 6.45822 3.4461C6.62901 3.49186 6.77462 3.6036 6.86302 3.75672C6.95143 3.90984 6.97539 4.09181 6.92962 4.2626L6.28085 6.68385Z" fill="var(--handle-color, var(--color-gold-600, #fd9903))" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -4,13 +4,6 @@ import { addDynamicIconSelectors } from '@iconify/tailwind'
import { iconCollection } from './src/iconCollection'
export default {
theme: {
extend: {
boxShadow: {
interface: 'var(--interface-panel-box-shadow)'
}
}
},
plugins: [
addDynamicIconSelectors({
iconSets: {

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,27 @@
import { defineConfig, devices } from '@playwright/test'
import type { PlaywrightTestConfig } from '@playwright/test'
const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
? {
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 1, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
}
}
: {
retries: process.env.CI ? 3 : 0,
use: {
trace: 'on-first-retry'
}
}
export default defineConfig({
testDir: './browser_tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
reporter: 'html',
...maybeLocalOptions,
// /* // Toggle for [LOCAL] testing.
retries: process.env.CI ? 3 : 0,
use: {
trace: 'on-first-retry'
},
/*/ // [LOCAL]
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
},
//*/
globalSetup: './browser_tests/globalSetup.ts',
globalTeardown: './browser_tests/globalTeardown.ts',

188
pnpm-lock.yaml generated
View File

@@ -147,9 +147,6 @@ catalogs:
eslint-plugin-import-x:
specifier: ^4.16.1
version: 4.16.1
eslint-plugin-oxlint:
specifier: 1.25.0
version: 1.25.0
eslint-plugin-prettier:
specifier: ^5.5.4
version: 5.5.4
@@ -195,12 +192,6 @@ catalogs:
nx:
specifier: 21.4.1
version: 21.4.1
oxlint:
specifier: ^1.25.0
version: 1.28.0
oxlint-tsgolint:
specifier: ^0.4.0
version: 0.4.0
picocolors:
specifier: ^1.1.1
version: 1.1.1
@@ -326,9 +317,6 @@ importers:
'@comfyorg/registry-types':
specifier: workspace:*
version: link:packages/registry-types
'@comfyorg/shared-frontend-utils':
specifier: workspace:*
version: link:packages/shared-frontend-utils
'@comfyorg/tailwind-utils':
specifier: workspace:*
version: link:packages/tailwind-utils
@@ -573,9 +561,6 @@ importers:
eslint-plugin-import-x:
specifier: 'catalog:'
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
eslint-plugin-oxlint:
specifier: 'catalog:'
version: 1.25.0
eslint-plugin-prettier:
specifier: 'catalog:'
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
@@ -621,12 +606,6 @@ importers:
nx:
specifier: 'catalog:'
version: 21.4.1
oxlint:
specifier: 'catalog:'
version: 1.28.0(oxlint-tsgolint@0.4.0)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.4.0
picocolors:
specifier: 'catalog:'
version: 1.1.1
@@ -2556,76 +2535,6 @@ packages:
cpu: [x64]
os: [win32]
'@oxlint-tsgolint/darwin-arm64@0.4.0':
resolution: {integrity: sha512-2jNvhxs6JJy93Z4SQ/VErODBzZtFKxQ+sybcKYcw5/K41tXOiBbJSwBMZ2PPvivCVkVcyOkJvfs5UXWW7o79uw==}
cpu: [arm64]
os: [darwin]
'@oxlint-tsgolint/darwin-x64@0.4.0':
resolution: {integrity: sha512-6A+YBecdZhk2NJ8Dh3kRkR6htNekDmAopFkdyrtNvsHJs5qNNuwUv5RZlVMYiaQTh/Y/tZ0YWE4+cVdqPIEyxQ==}
cpu: [x64]
os: [darwin]
'@oxlint-tsgolint/linux-arm64@0.4.0':
resolution: {integrity: sha512-JaX8JfQnY3UwX7l6BXIjhEaJAVeKVASELLFCdoo5+DOHgPuiiSKcxCVgTl92WPAuS0TYFXOgqOg31WXkvdi8bQ==}
cpu: [arm64]
os: [linux]
'@oxlint-tsgolint/linux-x64@0.4.0':
resolution: {integrity: sha512-iu106lxV1O64O4vK2eRoIuY2iHuil/hyDNKLRNVaTg1un+yoxN6/C5uxrJix/EJ+1O27P9c+sXmMplcmbXujtg==}
cpu: [x64]
os: [linux]
'@oxlint-tsgolint/win32-arm64@0.4.0':
resolution: {integrity: sha512-KTp9EzkTCGAh4/sL3l5a9otX63TvTs5riBcrcqu0jYS3P762rZSezzMMDc0Ld51x+I37125p9+bue2vmlH/KbQ==}
cpu: [arm64]
os: [win32]
'@oxlint-tsgolint/win32-x64@0.4.0':
resolution: {integrity: sha512-ioyBLHx0HA+hn5of8mhnA8W8DWQyJEHc7SBvwku0EW9bWt7zvBtWRJfx1YilvM+KVBdLVX731qeofdJT1fbJiQ==}
cpu: [x64]
os: [win32]
'@oxlint/darwin-arm64@1.28.0':
resolution: {integrity: sha512-H7J41/iKbgm7tTpdSnA/AtjEAhxyzNzCMKWtKU5wDuP2v39jrc3fasQEJruk6hj1YXPbJY4N+1nK/jE27GMGDQ==}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@1.28.0':
resolution: {integrity: sha512-bGsSDEwpyYzNc6FIwhTmbhSK7piREUjMlmWBt7eoR3ract0+RfhZYYG4se1Ngs+4WOFC0B3gbv23fyF+cnbGGQ==}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@1.28.0':
resolution: {integrity: sha512-eNH/evMpV3xAA4jIS8dMLcGkM/LK0WEHM0RO9bxrHPAwfS72jhyPJtd0R7nZhvhG6U1bhn5jhoXbk1dn27XIAQ==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@1.28.0':
resolution: {integrity: sha512-ickvpcekNeRLND3llndiZOtJBb6LDZqNnZICIDkovURkOIWPGJGmAxsHUOI6yW6iny9gLmIEIGl/c1b5nFk6Ag==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@1.28.0':
resolution: {integrity: sha512-DkgAh4LQ8NR3DwTT7/LGMhaMau0RtZkih91Ez5Usk7H7SOxo1GDi84beE7it2Q+22cAzgY4hbw3c6svonQTjxg==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@1.28.0':
resolution: {integrity: sha512-VBnMi3AJ2w5p/kgeyrjcGOKNY8RzZWWvlGHjCJwzqPgob4MXu6T+5Yrdi7EVJyIlouL8E3LYPYjmzB9NBi9gZw==}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@1.28.0':
resolution: {integrity: sha512-tomhIks+4dKs8axB+s4GXHy+ZWXhUgptf1XnG5cZg8CzRfX4JFX9k8l2fPUgFwytWnyyvZaaXLRPWGzoZ6yoHQ==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@1.28.0':
resolution: {integrity: sha512-4+VO5P/UJ2nq9sj6kQToJxFy5cKs7dGIN2DiUSQ7cqyUi7EKYNQKe+98HFcDOjtm33jQOQnc4kw8Igya5KPozg==}
cpu: [x64]
os: [win32]
'@phenomnomnominal/tsquery@5.0.1':
resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==}
peerDependencies:
@@ -4829,9 +4738,6 @@ packages:
'@typescript-eslint/parser':
optional: true
eslint-plugin-oxlint@1.25.0:
resolution: {integrity: sha512-grS4KdR9FAxoQC+wMkepeQHL4osMhoYfUI11Pot6Gitqr4wWi+JZrX0Shr8Bs9fjdWhEjtaZIV6cr4mbfytmyw==}
eslint-plugin-prettier@5.5.4:
resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -5792,9 +5698,6 @@ packages:
jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsondiffpatch@0.6.0:
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -6489,20 +6392,6 @@ packages:
oxc-resolver@11.6.1:
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
oxlint-tsgolint@0.4.0:
resolution: {integrity: sha512-RpvLxPvSt0Xzr3frTiw5rP6HUW0djZ2uNdzHc8Pv556sbTnFWXmLdK8FRqqy7vVXZTkoVSdY3PsvOsVAqGjc+Q==}
hasBin: true
oxlint@1.28.0:
resolution: {integrity: sha512-gE97d0BcIlTTSJrim395B49mIbQ9VO8ZVoHdWai7Svl+lEeUAyCLTN4d7piw1kcB8VfgTp1JFVlAvMPD9GewMA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.4.0'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -7815,8 +7704,8 @@ packages:
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
vue-component-type-helpers@3.1.3:
resolution: {integrity: sha512-V1dOD8XYfstOKCnXbWyEJIrhTBMwSyNjv271L1Jlx9ExpNlCSuqOs3OdWrGJ0V544zXufKbcYabi/o+gK8lyfQ==}
vue-component-type-helpers@3.1.2:
resolution: {integrity: sha512-ch3/SKBtxdZq18vsEntiGCdSszCRNfhX5QaTxjSacCAXLlNQRXfXo+ANjoQEYJMsJOJy1/vHF6Tkc4s85MS+zw==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -10209,48 +10098,6 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.6.1':
optional: true
'@oxlint-tsgolint/darwin-arm64@0.4.0':
optional: true
'@oxlint-tsgolint/darwin-x64@0.4.0':
optional: true
'@oxlint-tsgolint/linux-arm64@0.4.0':
optional: true
'@oxlint-tsgolint/linux-x64@0.4.0':
optional: true
'@oxlint-tsgolint/win32-arm64@0.4.0':
optional: true
'@oxlint-tsgolint/win32-x64@0.4.0':
optional: true
'@oxlint/darwin-arm64@1.28.0':
optional: true
'@oxlint/darwin-x64@1.28.0':
optional: true
'@oxlint/linux-arm64-gnu@1.28.0':
optional: true
'@oxlint/linux-arm64-musl@1.28.0':
optional: true
'@oxlint/linux-x64-gnu@1.28.0':
optional: true
'@oxlint/linux-x64-musl@1.28.0':
optional: true
'@oxlint/win32-arm64@1.28.0':
optional: true
'@oxlint/win32-x64@1.28.0':
optional: true
'@phenomnomnominal/tsquery@5.0.1(typescript@5.9.2)':
dependencies:
esquery: 1.6.0
@@ -10617,7 +10464,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.3
vue-component-type-helpers: 3.1.2
'@swc/helpers@0.5.17':
dependencies:
@@ -12675,10 +12522,6 @@ snapshots:
- supports-color
optional: true
eslint-plugin-oxlint@1.25.0:
dependencies:
jsonc-parser: 3.3.1
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
dependencies:
eslint: 9.35.0(jiti@2.4.2)
@@ -13744,8 +13587,6 @@ snapshots:
jsonc-parser@3.2.0: {}
jsonc-parser@3.3.1: {}
jsondiffpatch@0.6.0:
dependencies:
'@types/diff-match-patch': 1.0.36
@@ -14713,27 +14554,6 @@ snapshots:
'@oxc-resolver/binding-win32-ia32-msvc': 11.6.1
'@oxc-resolver/binding-win32-x64-msvc': 11.6.1
oxlint-tsgolint@0.4.0:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 0.4.0
'@oxlint-tsgolint/darwin-x64': 0.4.0
'@oxlint-tsgolint/linux-arm64': 0.4.0
'@oxlint-tsgolint/linux-x64': 0.4.0
'@oxlint-tsgolint/win32-arm64': 0.4.0
'@oxlint-tsgolint/win32-x64': 0.4.0
oxlint@1.28.0(oxlint-tsgolint@0.4.0):
optionalDependencies:
'@oxlint/darwin-arm64': 1.28.0
'@oxlint/darwin-x64': 1.28.0
'@oxlint/linux-arm64-gnu': 1.28.0
'@oxlint/linux-arm64-musl': 1.28.0
'@oxlint/linux-x64-gnu': 1.28.0
'@oxlint/linux-x64-musl': 1.28.0
'@oxlint/win32-arm64': 1.28.0
'@oxlint/win32-x64': 1.28.0
oxlint-tsgolint: 0.4.0
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -16343,7 +16163,7 @@ snapshots:
vue-component-type-helpers@3.1.1: {}
vue-component-type-helpers@3.1.3: {}
vue-component-type-helpers@3.1.2: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:

View File

@@ -50,7 +50,6 @@ catalog:
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.6
eslint-plugin-unused-imports: ^4.2.0
@@ -66,8 +65,6 @@ catalog:
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 21.4.1
oxlint: ^1.25.0
oxlint-tsgolint: ^0.4.0
picocolors: ^1.1.1
pinia: ^2.1.7
postcss-html: ^1.8.0

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
mapfile -t yaml_files < <(git ls-files '*.yml' '*.yaml')
if [[ ${#yaml_files[@]} -eq 0 ]]; then
echo "No YAML files found to lint"
exit 0
fi
yamllint --config-file .yamllint "${yaml_files[@]}"

View File

@@ -16,10 +16,6 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { t } from '@/i18n'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
@@ -27,8 +23,6 @@ import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const workflowStore = useWorkflowStore()
const dialogService = useDialogService()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
@@ -54,26 +48,6 @@ onMounted(() => {
document.addEventListener('contextmenu', showContextMenu)
}
// Handle Vite preload errors (e.g., when assets are deleted after deployment)
window.addEventListener('vite:preloadError', async (_event) => {
// Auto-reload if app is not ready or there are no unsaved changes
if (!app.vueAppReady || !workflowStore.activeWorkflow?.isModified) {
window.location.reload()
} else {
// Show confirmation dialog if there are unsaved changes
await dialogService
.confirm({
title: t('g.vitePreloadErrorTitle'),
message: t('g.vitePreloadErrorMessage')
})
.then((confirmed) => {
if (confirmed) {
window.location.reload()
}
})
}
})
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()

View File

@@ -22,7 +22,7 @@
},
"litegraph_base": {
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
"CLEAR_BACKGROUND_COLOR": "#141414",
"CLEAR_BACKGROUND_COLOR": "#222",
"NODE_TITLE_COLOR": "#999",
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
@@ -52,7 +52,7 @@
"comfy_base": {
"fg-color": "#fff",
"bg-color": "#202020",
"comfy-menu-bg": "#171718",
"comfy-menu-bg": "#353535",
"comfy-menu-secondary-bg": "#303030",
"comfy-input-bg": "#222",
"input-text": "#ddd",

View File

@@ -68,12 +68,7 @@
"content-fg": "#222",
"content-hover-bg": "#adadad",
"content-hover-fg": "#222",
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem",
"interface-panel-box-shadow": "1px 1px 8px 0 rgba(0, 0, 0, 0.2)",
"interface-panel-drop-shadow": "1px 1px 4px rgba(0, 0, 0, 0.4)",
"interface-panel-hover-surface": "var(--color-gray-200)",
"interface-panel-selected-surface": "color-mix(in srgb, var(--interface-panel-surface) 78%, var(--contrast-mix-color))",
"contrast-mix-color": "#000"
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem"
}
}
}

View File

@@ -30,7 +30,7 @@
## Styling
- Use Tailwind CSS only (no custom CSS)
- Use the correct tokens from style.css in the design system package
- Dark theme: use "dark-theme:" prefix
- For common operations, try to use existing VueUse composables that automatically handle effect scope
- Example: Use `useElementHover` instead of manually managing mouseover/mouseout event listeners
- Example: Use `useIntersectionObserver` for visibility detection instead of custom scroll handlers

View File

@@ -1,13 +1,12 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-1">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg px-2 shadow-md"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
@@ -25,7 +24,6 @@ import { onMounted, ref } from 'vue'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
@@ -50,5 +48,6 @@ onMounted(() => {
<style scoped>
.actionbar-container {
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
}
</style>

View File

@@ -48,7 +48,6 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton'
@@ -133,15 +132,6 @@ watch(visible, async (newVisible) => {
}
})
/**
* Track run button handle drag start using mousedown on the drag handle.
*/
useEventListener(dragHandleRef, 'mousedown', () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'actionbar_run_handle_drag_start'
})
})
const lastDragState = ref({
x: x.value,
y: y.value,
@@ -268,9 +258,7 @@ const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z1000',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'p-0 static mr-2 border-none bg-transparent'
: 'fixed shadow-interface'
isDocked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed'
)
)
</script>

View File

@@ -100,7 +100,7 @@ import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {
@@ -118,9 +118,6 @@ const queueModeMenuItemLookup = computed(() => {
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_on_change_selected'
})
queueMode.value = 'change'
}
}
@@ -131,9 +128,6 @@ const queueModeMenuItemLookup = computed(() => {
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_instant_selected'
})
queueMode.value = 'instant'
}
}
@@ -164,18 +158,9 @@ const queuePrompt = async (e: Event) => {
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
if (batchCount.value > 1) {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_run_multiple_batches_submitted'
})
}
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
await commandStore.execute(commandId, {
metadata: {
subscribe_to_run: false,
trigger_source: 'button'
}
})
await commandStore.execute(commandId)
}
</script>

View File

@@ -7,7 +7,7 @@
class="flex flex-col"
>
<h3
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-text-secondary uppercase"
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-surface-600 uppercase dark-theme:text-surface-400"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
@@ -16,7 +16,7 @@
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200"
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200 hover:bg-surface-100 dark-theme:hover:bg-surface-700"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
@@ -32,7 +32,7 @@
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge min-w-6 rounded bg-muted-background px-2 py-1 text-center font-mono text-xs"
class="key-badge min-w-6 rounded border bg-surface-200 px-2 py-1 text-center font-mono text-xs dark-theme:bg-surface-600"
>
{{ formatKey(key) }}
</span>
@@ -46,11 +46,11 @@
</template>
<script setup lang="ts">
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ComfyCommandImpl } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { t } = useI18n()
@@ -100,3 +100,21 @@ const formatKey = (key: string): string => {
return keyMap[key] || key
}
</script>
<style scoped>
.subcategory-title {
color: var(--p-text-muted-color);
}
.key-badge {
background-color: var(--p-surface-200);
border: 1px solid var(--p-surface-300);
min-width: 1.5rem;
text-align: center;
}
.dark-theme .key-badge {
background-color: var(--p-surface-600);
border-color: var(--p-surface-500);
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="subgraph-breadcrumb w-auto drop-shadow-[var(--interface-panel-drop-shadow)]"
class="subgraph-breadcrumb w-auto drop-shadow-md"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
@@ -40,7 +40,6 @@ import { computed, onUpdated, ref, watch } from 'vue'
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -74,9 +73,6 @@ const items = computed(() => {
const items = navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
label: subgraph.name,
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'breadcrumb_subgraph_item_selected'
})
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')
@@ -101,9 +97,6 @@ const home = computed(() => ({
key: 'root',
isBlueprint: isBlueprint.value,
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'breadcrumb_subgraph_root_selected'
})
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')
@@ -208,8 +201,8 @@ onUpdated(() => {
:deep(.p-breadcrumb-separator),
:deep(.p-breadcrumb-item) {
@apply h-12;
border-top: 1px solid var(--interface-stroke);
border-bottom: 1px solid var(--interface-stroke);
border-top: 1px solid var(--p-panel-border-color);
border-bottom: 1px solid var(--p-panel-border-color);
background-color: var(--comfy-menu-bg);
}
@@ -221,7 +214,7 @@ onUpdated(() => {
@apply rounded-l-lg;
/* Then collapse the root workflow */
flex-shrink: 5000;
border-left: 1px solid var(--interface-stroke);
border-left: 1px solid var(--p-panel-border-color);
.p-breadcrumb-item-link {
padding-left: var(--p-breadcrumb-item-padding);
@@ -232,7 +225,7 @@ onUpdated(() => {
@apply rounded-r-lg;
/* Then collapse the active item */
flex-shrink: 1;
border-right: 1px solid var(--interface-stroke);
border-right: 1px solid var(--p-panel-border-color);
}
:deep(.p-breadcrumb-item-link:hover),

View File

@@ -47,7 +47,6 @@
</template>
<script setup lang="ts">
import { appendJsonExt } from '@comfyorg/shared-frontend-utils/formatUtil'
import InputText from 'primevue/inputtext'
import type { MenuState } from 'primevue/menu'
import Menu from 'primevue/menu'
@@ -64,6 +63,7 @@ import {
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { appendJsonExt } from '@/utils/formatUtil'
interface Props {
item: MenuItem

View File

@@ -10,7 +10,8 @@ import { cn } from '@/utils/tailwindUtil'
const iconGroupClasses = cn(
'flex justify-center items-center shrink-0',
'outline-hidden border-none p-0 rounded-lg',
'bg-secondary-background shadow-sm',
'bg-white dark-theme:bg-zinc-700',
'text-neutral-950 dark-theme:text-white',
'transition-all duration-200',
'cursor-pointer'
)

View File

@@ -32,7 +32,7 @@ defineOptions({
interface IconTextButtonProps extends BaseButtonProps {
iconPosition?: 'left' | 'right'
label: string
onClick?: () => void
onClick: () => void
}
const {

View File

@@ -7,24 +7,13 @@
<Popover
ref="popover"
append-to="body"
auto-z-index
dismissable
close-on-escape
unstyled
:append-to="'body'"
:auto-z-index="true"
:base-z-index="1000"
:pt="{
root: {
class: cn('absolute z-50')
},
content: {
class: cn(
'mt-1 rounded-lg',
'bg-secondary-background text-base-foreground',
'shadow-lg'
)
}
}"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="pt"
@show="$emit('menuOpened')"
@hide="$emit('menuClosed')"
>
@@ -37,7 +26,7 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
@@ -68,4 +57,19 @@ const toggle = (event: Event) => {
const hide = () => {
popover.value?.hide()
}
const pt = computed(() => ({
root: {
class: cn('absolute z-50')
},
content: {
class: cn(
'mt-1 rounded-lg',
'bg-white dark-theme:bg-zinc-800',
'text-neutral dark-theme:text-white',
'shadow-lg',
'border border-zinc-200 dark-theme:border-zinc-700'
)
}
}))
</script>

View File

@@ -47,8 +47,8 @@ const containerClasses = computed(() => {
// Variant styles
const variantClasses = {
default: cn(
hasBackground && 'bg-modal-card-background',
hasBorder && 'border border-border-default',
hasBackground && 'bg-white dark-theme:bg-zinc-800',
hasBorder && 'border border-zinc-200 dark-theme:border-zinc-700',
hasShadow && 'shadow-sm',
hasCursor && 'cursor-pointer'
),
@@ -57,9 +57,9 @@ const containerClasses = computed(() => {
'p-2 transition-colors duration-200'
),
outline: cn(
hasBorder && 'border-2 border-border-subtle',
hasBorder && 'border-2 border-zinc-300 dark-theme:border-zinc-600',
hasCursor && 'cursor-pointer',
'hover:border-border-subtle/50 transition-colors'
'hover:border-zinc-400 dark-theme:hover:border-zinc-500 transition-colors'
)
}

View File

@@ -1,5 +1,7 @@
<template>
<div class="line-clamp-2 h-7 text-xs text-muted-foreground">
<div class="line-clamp-2 h-7 text-xs text-zinc-500 dark-theme:text-zinc-400">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -19,7 +19,7 @@ const baseClasses =
const variantStyles = {
dark: 'bg-zinc-500/40 text-white/90',
light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground')
light: 'backdrop-blur-[2px] bg-white/50 text-zinc-900 dark-theme:text-white'
}
const chipClasses = computed(() => {

View File

@@ -12,9 +12,8 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import type { DeviceStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
device: DeviceStats

View File

@@ -12,7 +12,7 @@
</div>
</div>
<div class="file-action flex flex-row items-center gap-2">
<div class="file-action">
<Button
v-if="status === null || status === 'error'"
class="file-action-button"
@@ -23,13 +23,6 @@
icon="pi pi-download"
@click="triggerDownload"
/>
<Button
v-if="(status === null || status === 'error') && !!props.url"
:label="$t('g.copyURL')"
size="small"
outlined
@click="copyURL"
/>
</div>
</div>
<div
@@ -82,15 +75,14 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import Button from 'primevue/button'
import ProgressBar from 'primevue/progressbar'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string
@@ -108,7 +100,6 @@ const status = ref<string | null>(null)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const { copyToClipboard } = useCopyToClipboard()
const electronDownloadStore = useElectronDownloadStore()
// @ts-expect-error fixme ts strict error
const [savePath, filename] = props.label.split('/')
@@ -135,8 +126,4 @@ const triggerDownload = async () => {
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
const copyURL = async () => {
await copyToClipboard(props.url)
}
</script>

View File

@@ -43,13 +43,13 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import Button from 'primevue/button'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string

View File

@@ -3,7 +3,7 @@
<div class="flex items-center gap-2">
<div
class="preview-box flex h-16 w-16 items-center justify-center rounded border p-2"
:class="{ 'bg-base-background': !modelValue }"
:class="{ 'bg-smoke-100 dark-theme:bg-smoke-800': !modelValue }"
>
<img
v-if="modelValue"

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