Compare commits

..

11 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
ec282d4eab Complete Storybook issue tracking system with documentation
Co-authored-by: snomiao <7323030+snomiao@users.noreply.github.com>
2025-08-20 10:29:21 +00:00
copilot-swe-agent[bot]
ae7f6b8143 Add Storybook issue template and tracking documentation
Co-authored-by: snomiao <7323030+snomiao@users.noreply.github.com>
2025-08-20 10:28:05 +00:00
copilot-swe-agent[bot]
90c09db919 Initial plan 2025-08-20 10:22:03 +00:00
Christian Byrne
8d0a523ffd [refactor] Remove obsolete Kontext Edit Button (#5108)
* feat: Remove obsolete Kontext Edit Button

Removes the 'Kontext Edit Button' and its associated code, as it has been made obsolete by the new 'Subgraphs + Partial Execution' feature.

Fixes #5093

* Update locales [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-08-19 18:36:00 -07:00
pythongosssss
97e5291b05 Update to latest version of workflow icon (#5103) 2025-08-19 15:14:06 -07:00
Christian Byrne
80097007c9 refactor readme to suggest deployed reports (#5112) 2025-08-19 15:03:02 -07:00
Arjan Singh
deba8df8ce [chore] ignore ./claude/settings.json (#5110) 2025-08-19 13:53:00 -07:00
Christian Byrne
694817297d [ci] Add caching support to format and knip commands (#5107)
- Enable caching for prettier and knip commands to improve CI performance
- Add no-cache variants for consistency with existing lint scripts
- Exclude generated type files from prettier formatting
- Add .prettiercache to .gitignore for proper cache management

Follows the same optimization pattern as ESLint caching from PR #4926.
2025-08-19 12:33:55 -07:00
Christian Byrne
28d74be363 add onRemove invoke to removeWidget method (#5102) 2025-08-19 10:39:20 -07:00
snomiao
2240645a7d [feat] Add Cloudflare Pages deployment for Playwright test reports (#5045)
* [feat] Add Cloudflare Pages deployment for Playwright test reports

- Deploy test reports to separate Cloudflare projects per browser
- Add real-time PR comments with progressive test status updates
- Use wrangler-action for unified Cloudflare tooling
- Support cross-browser testing with individual report links
- Document CI/CD integration in browser_tests/README.md

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

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Cloudflare project name for chromium-0.5x browser

* Extract project name transformation to variable for consistent URL formatting

* chore(ci): update branch filters for push and pull_request events in test-ui workflow to refine CI triggers

* [feat] Improve test-ui deployment with branch isolation and building page

- Use Cloudflare Pages --branch flag for proper branch isolation instead of modifying project names
- Add auto-refresh building page that shows test progress in real-time
- Deploy building page immediately when tests start for instant feedback
- Update URL generation to use branch-based Cloudflare Pages URLs format
- Maintain clean project names while isolating branches properly

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(test-ui.yaml): increase sleep duration from 5 to 10 seconds for cache propagation and restore cached setup steps for improved workflow efficiency

* [refactor] Remove building-page to reduce complexity

- Remove auto-refresh building page and related deployment steps
- Simplify PR comments to show basic test status without progress page
- Keep branch-based deployment for proper isolation while reducing complexity
- Maintain clean workflow focused on core functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(test-ui.yaml): add a separator in the workflow file for better readability and organization of the test status section

* [feat] Add Cloudflare Pages deployment for Playwright test reports

- Deploy test reports to separate Cloudflare projects per browser
- Add real-time PR comments with progressive test status updates
- Use wrangler-action for unified Cloudflare tooling
- Support cross-browser testing with individual report links
- Document CI/CD integration in browser_tests/README.md

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

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Cloudflare project name for chromium-0.5x browser

* Extract project name transformation to variable for consistent URL formatting

* chore(ci): update branch filters for push and pull_request events in test-ui workflow to refine CI triggers

* [feat] Improve test-ui deployment with branch isolation and building page

- Use Cloudflare Pages --branch flag for proper branch isolation instead of modifying project names
- Add auto-refresh building page that shows test progress in real-time
- Deploy building page immediately when tests start for instant feedback
- Update URL generation to use branch-based Cloudflare Pages URLs format
- Maintain clean project names while isolating branches properly

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(test-ui.yaml): increase sleep duration from 5 to 10 seconds for cache propagation and restore cached setup steps for improved workflow efficiency

* [refactor] Remove building-page to reduce complexity

- Remove auto-refresh building page and related deployment steps
- Simplify PR comments to show basic test status without progress page
- Keep branch-based deployment for proper isolation while reducing complexity
- Maintain clean workflow focused on core functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(test-ui.yaml): add a separator in the workflow file for better readability and organization of the test status section

* [fix] Address PR review feedback - improve workflow architecture and security

- [HIGH] Fix continue-on-error masking by adding final test result check that fails CI on test failures
- [MEDIUM] Move branch sanitization to setup job to reduce performance overhead
- [MEDIUM] Add compatibility-date to Cloudflare deployment for stability
- [LOW] Extract date format to environment variable to follow DRY principle
- [LOW] Quote shell variables properly to prevent word splitting
- [LOW] Update documentation to use dynamic branch-specific URLs

Addresses all security, performance, and code quality issues raised in automated PR review.
Maintains test report deployment while ensuring CI integrity.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(test-ui.yaml): replace loading emoji with an image for better visual consistency in test logs
style(test-ui.yaml): clean up whitespace in the workflow file for improved readability

* style(test-ui.yaml): format message to combine two lines into one for better readability

* chore(test-ui.yaml): add a blank line for better readability in the workflow file

* style(test-ui.yaml): update loading image alt text and format messages for better readability in GitHub Actions workflow

* [architecture] Separate test execution from deployment - clean CI design

BREAKING: Remove continue-on-error from test execution for proper CI integrity

**Clean Architecture Changes:**
- Remove `continue-on-error: true` from Playwright test execution
- Create separate `deploy-reports` job that always runs for debugging
- Test jobs now properly fail when tests fail (maintains CI integrity)
- Reports still deploy for debugging via dedicated deployment job
- Capture and pass actual exit codes between jobs via artifacts

**Benefits:**
-  CI fails when tests fail (no longer masked)
-  Reports still deploy for debugging regardless of test outcome
-  Clean separation of concerns (test vs deploy responsibilities)
-  Proper job dependencies and error handling
-  Individual browser test results preserved

**Job Flow:**
1. `setup` - Cache and prepare environment
2. `playwright-tests` - Run tests, fail if tests fail, upload artifacts
3. `deploy-reports` - Always deploy reports using artifacts (parallel)
4. `comment-summary` - Generate summary and fail workflow if needed

This addresses the high-priority architecture concern about continue-on-error
masking test failures while maintaining report deployment functionality.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* [refactor] Simplify deployment architecture - remove over-engineering

**Reverted to clean, simple approach based on feedback:**

1.  **Faster deployment** - Deploy immediately after each test (no waiting for matrix completion)
2.  **Remove unnecessary GITHUB_OUTPUT** - Don't save exit codes, use step.conclusion instead
3.  **Single job approach** - Use `if: always()` instead of separate deploy-reports job

**Key Changes:**
- Removed separate `deploy-reports` job (86 lines deleted!)
- Deploy in same job with `if: always()` - much faster
- Use `steps.playwright.conclusion` instead of captured exit codes
- Cleaner, simpler architecture with same functionality

**Benefits:**
- 🚀 **Much faster** - Reports deploy immediately per browser, not waiting for all tests
- 🧹 **Simpler** - One job handles test + deploy, easier to understand
-  **Still maintains CI integrity** - Tests fail properly when they should
- 📊 **Reports always deploy** - Available for debugging regardless of test outcome

The previous approach was over-engineered. This is much cleaner and faster.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(workflow): reorder condition in PR comment step for clarity and consistency

* chore(test-ui.yaml): update deployment command to remove compatibility date for better maintainability
docs(test-ui.yaml): add note to always() condition for clarity on artifact upload behavior

* [performance] Remove redundant branch sanitization - 75% processing reduction

**Issue**: Complex bash string operations running 4 times per build in matrix jobs
**Solution**: Remove duplicate branch sanitization, use pre-computed value from setup job

**Before**: Branch sanitization ran in both setup job AND each matrix job (5 total times)
**After**: Branch sanitization runs only once in setup job, reused via outputs

**Performance Impact**:
- 4 redundant tr/sed operations eliminated (matrix chromium, chromium-2x, chromium-0.5x, mobile-chrome)
- 75% reduction in branch name processing overhead
- Cleaner, more maintainable code

**Implementation**:
- Setup job: Computes `sanitized-branch` output once
- Matrix jobs: Use `${{ needs.setup.outputs.sanitized-branch }}` directly
- No duplicate string processing logic

Addresses PR review comment: [performance] medium Priority - Complex bash string operations in GitHub Actions matrix

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-19 10:22:45 -07:00
Benjamin Lu
cf9847a11e Remove PR checks workflows (#5099)
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-19 09:52:22 -07:00
34 changed files with 542 additions and 1106 deletions

View File

@@ -0,0 +1,96 @@
name: Storybook Improvement
description: 'Report Storybook-related improvements, new stories, or configuration issues'
title: '[Storybook]: '
labels: ['area:storybook', 'enhancement']
type: Storybook
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label: I have checked the existing Storybook documentation in `.storybook/`
required: true
- label: I have searched existing issues to make sure this isn't a duplicate
required: true
- label: I have checked if this relates to any existing Storybook PRs
- type: dropdown
id: category
attributes:
label: Category
description: What type of Storybook improvement is this?
options:
- Component Stories - Add new stories for existing components
- Story Enhancement - Improve existing stories
- Configuration - Storybook configuration improvements
- Visual Testing - Chromatic/visual regression testing
- Documentation - Storybook documentation improvements
- Build/Performance - Build optimizations or performance improvements
- Theme/Styling - Theme support or styling improvements
- Tools/Addons - Storybook addons or tool integration
- Other
validations:
required: true
- type: textarea
id: description
attributes:
label: What improvement are you suggesting?
description: Describe the Storybook improvement you'd like to see
placeholder: |
Example: "Add comprehensive stories for the NodeWidget component covering all widget types (text, number, combo, etc.) to improve component development and testing."
validations:
required: true
- type: textarea
id: context
attributes:
label: Use Case & Context
description: Why is this improvement needed? What problem does it solve?
placeholder: |
- Current state of the component/story
- What's missing or could be improved
- How this would benefit development workflow
validations:
required: true
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this improvement?
options:
- Low - Nice to have enhancement
- Medium - Would improve development workflow
- High - Important for component quality/testing
- Critical - Needed for proper Storybook functionality
validations:
required: true
- type: textarea
id: components
attributes:
label: Components Affected
description: Which components or areas would be affected by this improvement?
placeholder: |
- NodeWidget
- Settings components
- All button components
- etc.
- type: textarea
id: implementation
attributes:
label: Implementation Ideas (Optional)
description: Any ideas on how this could be implemented?
placeholder: |
- Specific stories to create
- Configuration changes needed
- Dependencies or tools required
- type: textarea
id: examples
attributes:
label: Examples or References
description: Any examples from other projects, screenshots, or links that help illustrate the improvement

View File

@@ -1,154 +0,0 @@
name: PR Checks
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
analyze:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check-changes.outputs.should_run }}
has_browser_tests: ${{ steps.check-coverage.outputs.has_browser_tests }}
has_screen_recording: ${{ steps.check-recording.outputs.has_recording }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure base branch is available
run: |
# Fetch the specific base commit to ensure it's available for git diff
git fetch origin ${{ github.event.pull_request.base.sha }}
- name: Check if significant changes exist
id: check-changes
run: |
# Get list of changed files
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})
# Filter for src/ files
SRC_FILES=$(echo "$CHANGED_FILES" | grep '^src/' || true)
if [ -z "$SRC_FILES" ]; then
echo "No src/ files changed"
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Count lines changed in src files
TOTAL_LINES=0
for file in $SRC_FILES; do
if [ -f "$file" ]; then
# Count added lines (non-empty)
ADDED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^+' | grep -v '^+++' | grep -v '^+$' | wc -l)
# Count removed lines (non-empty)
REMOVED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^-' | grep -v '^---' | grep -v '^-$' | wc -l)
TOTAL_LINES=$((TOTAL_LINES + ADDED + REMOVED))
fi
done
echo "Total lines changed in src/: $TOTAL_LINES"
if [ $TOTAL_LINES -gt 3 ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
fi
- name: Check browser test coverage
id: check-coverage
if: steps.check-changes.outputs.should_run == 'true'
run: |
# Check if browser tests were updated
BROWSER_TEST_CHANGES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} | grep '^browser_tests/.*\.ts$' || true)
if [ -n "$BROWSER_TEST_CHANGES" ]; then
echo "has_browser_tests=true" >> "$GITHUB_OUTPUT"
else
echo "has_browser_tests=false" >> "$GITHUB_OUTPUT"
fi
- name: Check for screen recording
id: check-recording
if: steps.check-changes.outputs.should_run == 'true'
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Check PR body for screen recording
# Check for GitHub user attachments or YouTube links
if echo "$PR_BODY" | grep -qiE 'github\.com/user-attachments/assets/[a-f0-9-]+|youtube\.com/watch|youtu\.be/'; then
echo "has_recording=true" >> "$GITHUB_OUTPUT"
else
echo "has_recording=false" >> "$GITHUB_OUTPUT"
fi
- name: Final check and create results
id: final-check
if: always()
run: |
# Initialize results
WARNINGS_JSON=""
# Only run checks if should_run is true
if [ "${{ steps.check-changes.outputs.should_run }}" == "true" ]; then
# Check browser test coverage
if [ "${{ steps.check-coverage.outputs.has_browser_tests }}" != "true" ]; then
if [ -n "$WARNINGS_JSON" ]; then
WARNINGS_JSON="${WARNINGS_JSON},"
fi
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: E2E Test Coverage Missing**\\n\\nIf this PR modifies behavior that can be covered by browser-based E2E tests, those tests are required. PRs lacking applicable test coverage may not be reviewed until added. Please add or update browser tests to ensure code quality and prevent regressions.\"}"
fi
# Check screen recording
if [ "${{ steps.check-recording.outputs.has_recording }}" != "true" ]; then
if [ -n "$WARNINGS_JSON" ]; then
WARNINGS_JSON="${WARNINGS_JSON},"
fi
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: Visual Documentation Missing**\\n\\nIf this PR changes user-facing behavior, visual proof (screen recording or screenshot) is required. PRs without applicable visual documentation may not be reviewed until provided.\\nYou can add it by:\\n\\n- GitHub: Drag & drop media directly into the PR description\\n\\n- YouTube: Include a link to a short demo\"}"
fi
fi
# Create results JSON
if [ -n "$WARNINGS_JSON" ]; then
# Create JSON with warnings
cat > pr-check-results.json << EOF
{
"fails": [],
"warnings": [$WARNINGS_JSON],
"messages": [],
"markdowns": []
}
EOF
echo "failed=false" >> "$GITHUB_OUTPUT"
else
# Create JSON with success
cat > pr-check-results.json << 'EOF'
{
"fails": [],
"warnings": [],
"messages": [],
"markdowns": []
}
EOF
echo "failed=false" >> "$GITHUB_OUTPUT"
fi
# Write PR metadata
echo "${{ github.event.pull_request.number }}" > pr-number.txt
echo "${{ github.event.pull_request.head.sha }}" > pr-sha.txt
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: pr-check-results-${{ github.run_id }}
path: |
pr-check-results.json
pr-number.txt
pr-sha.txt
retention-days: 1

View File

@@ -1,149 +0,0 @@
name: PR Comment
on:
workflow_run:
workflows: ["PR Checks"]
types: [completed]
permissions:
pull-requests: write
issues: write
statuses: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: pr-check-results-${{ github.event.workflow_run.id }}
path: /tmp/pr-artifacts
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Post results
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Helper function to safely read files
function safeReadFile(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return fs.readFileSync(filePath, 'utf8').trim();
} catch (e) {
console.error(`Error reading ${filePath}:`, e);
return null;
}
}
// Read artifact files
const artifactDir = '/tmp/pr-artifacts';
const prNumber = safeReadFile(path.join(artifactDir, 'pr-number.txt'));
const prSha = safeReadFile(path.join(artifactDir, 'pr-sha.txt'));
const resultsJson = safeReadFile(path.join(artifactDir, 'pr-check-results.json'));
// Validate PR number
if (!prNumber || isNaN(parseInt(prNumber))) {
throw new Error('Invalid or missing PR number');
}
// Parse and validate results
let results;
try {
results = JSON.parse(resultsJson || '{}');
} catch (e) {
console.error('Failed to parse check results:', e);
// Post error comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prNumber),
body: `⚠️ PR checks failed to complete properly. Error parsing results: ${e.message}`
});
return;
}
// Format check messages
const messages = [];
if (results.fails && results.fails.length > 0) {
messages.push('### ❌ Failures\n' + results.fails.map(f => f.message).join('\n\n'));
}
if (results.warnings && results.warnings.length > 0) {
messages.push('### ⚠️ Warnings\n' + results.warnings.map(w => w.message).join('\n\n'));
}
if (results.messages && results.messages.length > 0) {
messages.push('### 💬 Messages\n' + results.messages.map(m => m.message).join('\n\n'));
}
if (results.markdowns && results.markdowns.length > 0) {
messages.push(...results.markdowns.map(m => m.message));
}
// Find existing bot comment
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prNumber)
});
const botComment = comments.data.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('<!-- pr-checks-comment -->')
);
// Post comment if there are any messages
if (messages.length > 0) {
const body = messages.join('\n\n');
const commentBody = `<!-- pr-checks-comment -->\n${body}`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prNumber),
body: commentBody
});
}
} else {
// No messages - delete existing comment if present
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id
});
}
}
// Set commit status based on failures
if (prSha) {
const hasFailures = results.fails && results.fails.length > 0;
const hasWarnings = results.warnings && results.warnings.length > 0;
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: prSha,
state: hasFailures ? 'failure' : 'success',
context: 'pr-checks',
description: hasFailures
? `${results.fails.length} check(s) failed`
: hasWarnings
? `${results.warnings.length} warning(s)`
: 'All checks passed'
});
}

View File

@@ -4,13 +4,18 @@ on:
push:
branches: [main, master, core/*, desktop/*]
pull_request:
branches-ignore: [wip/*, draft/*, temp/*, vue-nodes-migration]
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
@@ -36,6 +41,29 @@ jobs:
with:
node-version: lts/*
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Tests Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- PLAYWRIGHT_TEST_STATUS -->
---
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
---
*This comment will be updated when tests complete*
- name: Build ComfyUI_frontend
run: |
npm ci
@@ -46,6 +74,14 @@ jobs:
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Generate sanitized branch name
id: branch-info
run: |
# Get branch name and sanitize it for Cloudflare branch names
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
@@ -57,6 +93,10 @@ jobs:
playwright-tests:
needs: setup
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
contents: read
strategy:
fail-fast: false
matrix:
@@ -78,6 +118,32 @@ jobs:
with:
python-version: '3.10'
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Set project name
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>${{ matrix.browser }}</bold>: Running tests...
- name: Install requirements
run: |
python -m pip install --upgrade pip
@@ -97,12 +163,165 @@ jobs:
working-directory: ComfyUI_frontend
- name: Run Playwright tests (${{ matrix.browser }})
run: npx playwright test --project=${{ matrix.browser }}
id: playwright
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
if: always() # note: use always() to allow results to be upload/report even tests failed.
with:
name: playwright-report-${{ matrix.browser }}
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
id: cloudflare-deploy
if: always()
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}
- name: Save deployment info for summary
if: always()
run: |
mkdir -p deployment-info
# Use step conclusion to determine test result
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
EXIT_CODE="0"
else
EXIT_CODE="1"
fi
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
- name: Upload deployment info
if: always()
uses: actions/upload-artifact@v4
with:
name: deployment-info-${{ matrix.browser }}
path: deployment-info/
retention-days: 1
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Complete
if: always() && github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
comment-summary:
needs: playwright-tests
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Download all deployment info
uses: actions/download-artifact@v4
with:
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body
id: comment-body
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
- name: Check test results and fail if needed
run: |
# Check if all tests passed and fail the job if not
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "false" ]; then
echo "❌ Tests failed in one or more browsers. Failing the CI job."
exit 1
else
echo "✅ All tests passed across all browsers!"
fi

4
.gitignore vendored
View File

@@ -13,8 +13,9 @@ bun.lockb
pnpm-lock.yaml
yarn.lock
# ESLint cache
# Cache files
.eslintcache
.prettiercache
node_modules
dist
@@ -22,6 +23,7 @@ dist-ssr
*.local
# Claude configuration
.claude/*.local.json
.claude/settings.json
# Editor directories and files
.vscode/*

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
src/types/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts

62
STORYBOOK_ISSUE_SYSTEM.md Normal file
View File

@@ -0,0 +1,62 @@
# Storybook Issue Tracking System
This directory contains resources for tracking and organizing all Storybook-related work in the ComfyUI Frontend repository.
## 📋 Components
### 1. Issue Template (`.github/ISSUE_TEMPLATE/storybook-improvement.yaml`)
A structured GitHub issue template specifically for Storybook-related improvements and requests.
**Features:**
- Categorizes improvements (Component Stories, Configuration, Visual Testing, etc.)
- Priority levels (Low → Critical)
- Component impact tracking
- Implementation ideas and examples
**Usage:** When creating new issues related to Storybook, use this template to ensure consistent formatting and complete information.
### 2. Tracking Documentation (`STORYBOOK_TRACKING_ISSUE.md`)
Comprehensive documentation listing all 27+ Storybook-related PRs, organized by category.
**Contains:**
- Current status overview
- PRs organized by category (Setup, Stories, Themes, Config, etc.)
- Upcoming priorities roadmap
- Contribution guidelines
- Resource links
## 🔧 How to Use
### For New Storybook Issues
1. Go to [GitHub Issues → New Issue](https://github.com/Comfy-Org/ComfyUI_frontend/issues/new/choose)
2. Select "Storybook Improvement" template
3. Fill out the structured form
4. Add `area:storybook` label if not automatically applied
### For Tracking Progress
1. Reference the tracking documentation in `STORYBOOK_TRACKING_ISSUE.md`
2. Create a GitHub issue using this content as the body
3. Use labels: `area:storybook`, `tracking`
4. Pin the issue for easy access
### For Contributors
1. Check existing tracking issue for current priorities
2. Follow guidelines in `.storybook/README.md` and `.storybook/CLAUDE.md`
3. Reference the tracking issue number in related PRs
4. Update tracking documentation when completing work
## 📚 Related Resources
- **Storybook Documentation**: `.storybook/README.md`
- **Developer Guidelines**: `.storybook/CLAUDE.md`
- **Component Examples**: `src/components/*/\*.stories.ts`
- **Visual Testing**: Chromatic integration in CI/CD workflows
## 🎯 Purpose
This system helps:
- **Organize** all Storybook-related work in one place
- **Track** progress across multiple PRs and initiatives
- **Prioritize** improvements based on impact and urgency
- **Facilitate** collaboration between contributors
- **Maintain** comprehensive documentation of Storybook evolution

121
STORYBOOK_TRACKING_ISSUE.md Normal file
View File

@@ -0,0 +1,121 @@
# 📚 Storybook Development Tracking
This issue serves as a central hub for tracking all Storybook-related PRs and improvements in the ComfyUI Frontend repository.
## 🎯 Overview
Storybook is a crucial part of our component development workflow, enabling:
- Component isolation and development
- Visual documentation and testing
- Automated visual regression testing with Chromatic
- Design system development and maintenance
## 📈 Current Status
**Storybook Setup**: ✅ Complete
**Component Coverage**: 🔄 In Progress
**Visual Testing**: ✅ Integrated with Chromatic
**Documentation**: ✅ Comprehensive guides available
## 📋 Storybook PRs by Category
### 🏗️ Initial Setup & Infrastructure
- **#4861** - [feat] Add Storybook setup and NodePreview story *(merged)*
- Complete Storybook v8 setup with Vue 3 + Vite
- Chromatic integration for visual testing
- Comprehensive documentation and guidelines
### 📖 Component Stories & Documentation
- **#4999** - [feat] 100+ more Stories for Common Components *(open)*
- 76 story variants across 11 components
- Covers STATIC → SIMPLE_PROPS → INTERACTIVE → COMPLEX tiers
- **#5034** - [feat] Add Storybook configuration and settings panel stories *(open)*
- Settings panel components with all input types
- Responsive design and accessibility features
- **#5098** - [feat] Add comprehensive Storybook stories for custom UI components *(open)*
- 12 custom UI components with interactive testing
- Button, input, and layout component stories
- **#5122** - [docs] Add Storybook documentation *(open)*
- Enhanced `.storybook/README.md` with comprehensive guidelines
### 🎨 Theme & Visual Improvements
- **#5088** - [feat] Add dark theme support for Storybook *(merged)*
- Dark theme toggle with persistence
- Smooth transitions and proper styling
### 🔧 Configuration & Build Optimizations
- **#5117** - [ci] Enhance CI/CD caching across all workflows *(open)*
- Improved caching for Storybook builds
- **#5118** - [ci] Add retry logic to wrangler page deploy step *(open)*
- Stability improvements for Storybook deployment
### 🚀 Features & Enhancements
- **#5119** - [feat] Add enhanced filter UI components *(open)*
- SearchBox integration and improved MultiSelect
- **#5096** - [fix] Resolve breadcrumb and workflow tabs layout conflict *(open)*
- Layout improvements affecting Storybook stories
- **#5113** - [fix] Reposition TaskItem info *(open)*
- Component fixes that impact Storybook examples
### 🔨 Technical Improvements & Fixes
- **#5106** - Fix/widget ordering consistency *(open)*
- Node widget improvements affecting stories
- **#5109** - Fix CopyToClipboard Issue *(open)*
- Component fixes relevant to Storybook examples
- **#5092** - Add support for high-resolution wheel events *(open)*
- Input handling improvements
- **#5115** - Fix: Shift+Click+Drag from outputs with Subgraph outputs *(open)*
- Node interaction improvements
- **#5114** - Remove duplicate semantic labeling from issue templates *(open)*
- Issue template improvements
- **#5102** - [fix] Invoke onRemove callback in LGraphNode.removeWidget method *(merged)*
- Widget system improvements
- **#5099** - Remove PR checks workflows *(merged)*
- CI/CD cleanup
- **#5103** - Update to latest version of workflow icon *(merged)*
- Icon updates affecting stories
- **#5107** - [ci] Add caching support to format and knip commands *(merged)*
- Build optimization improvements
- **#5108** - [refactor] Remove obsolete Kontext Edit Button *(merged)*
- Component cleanup
- **#5110** - [chore] Ignore ./claude/settings.json *(merged)*
- Development environment improvements
- **#5112** - [docs] Update browser tests README *(merged)*
- Testing documentation improvements
- **#4908** - Modal Component & Custom UI Components *(merged)*
- Foundation UI components used in stories
## 🎯 Upcoming Priorities
### High Priority
- [ ] Complete component story coverage for all major UI components
- [ ] Implement comprehensive visual regression testing
- [ ] Improve Storybook build performance and caching
### Medium Priority
- [ ] Add interactive component documentation
- [ ] Enhance theme switching and customization
- [ ] Improve mobile responsiveness of stories
### Low Priority
- [ ] Add more sophisticated mock data patterns
- [ ] Implement component testing automation
- [ ] Explore advanced Storybook addons
## 🔄 How to Contribute
1. **Creating New Stories**: Follow guidelines in `.storybook/README.md` and `.storybook/CLAUDE.md`
2. **Improving Existing Stories**: Use the Storybook Improvement issue template
3. **Documentation**: Update relevant documentation when adding features
4. **Testing**: Ensure all stories build and render correctly
## 📚 Resources
- **Storybook Documentation**: `.storybook/README.md`
- **Developer Guidelines**: `.storybook/CLAUDE.md`
- **Component Examples**: `src/components/*/\*.stories.ts`
- **Visual Testing**: Chromatic integration in CI/CD
---
*This issue is automatically maintained. Please reference this issue number when working on Storybook-related improvements.*

View File

@@ -392,16 +392,6 @@ Option 2 - Generate local baselines for comparison:
npx playwright test --update-snapshots
```
### Getting Test Artifacts from GitHub Actions
When tests fail in CI, you can download screenshots and traces:
1. Go to the failed workflow run in GitHub Actions
2. Scroll to "Artifacts" section at the bottom
3. Download `playwright-report` or `test-results`
4. Extract and open the HTML report locally
5. View actual vs expected screenshots and execution traces
### Creating New Screenshot Baselines
For PRs from `Comfy-Org/ComfyUI_frontend` branches:
@@ -412,6 +402,33 @@ For PRs from `Comfy-Org/ComfyUI_frontend` branches:
> **Note:** Fork PRs cannot auto-commit screenshots. A maintainer will need to commit the screenshots manually for you (don't worry, they'll do it).
## Viewing Test Reports
### Automated Test Deployment
The project automatically deploys Playwright test reports to Cloudflare Pages for every PR and push to main branches.
### Accessing Test Reports
- **From PR comments**: Click the "View Report" links for each browser
- **Direct URLs**: Reports are available at `https://[branch].comfyui-playwright-[browser].pages.dev` (branch-specific deployments)
- **From GitHub Actions**: Download artifacts from failed runs
### How It Works
1. **Test execution**: All browser tests run in parallel across multiple browsers
2. **Report generation**: HTML reports are generated for each browser configuration
3. **Cloudflare deployment**: Each browser's report deploys to its own Cloudflare Pages project with branch isolation:
- `comfyui-playwright-chromium` (with branch-specific URLs)
- `comfyui-playwright-mobile-chrome` (with branch-specific URLs)
- `comfyui-playwright-chromium-2x` (2x scale, with branch-specific URLs)
- `comfyui-playwright-chromium-0-5x` (0.5x scale, with branch-specific URLs)
4. **PR comments**: GitHub automatically updates PR comments with:
- ✅/❌ Test status for each browser
- Direct links to interactive test reports
- Real-time progress updates as tests complete
## Resources
- [Playwright UI Mode](https://playwright.dev/docs/test-ui-mode) - Interactive test debugging

View File

@@ -14,8 +14,10 @@
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:browser": "npx playwright test",
"test:unit": "vitest run tests-ui/tests",
"test:component": "vitest run src/components/",
@@ -25,7 +27,8 @@
"lint:fix": "eslint src --cache --fix",
"lint:no-cache": "eslint src",
"lint:fix:no-cache": "eslint src --fix",
"knip": "knip",
"knip": "knip --cache",
"knip:no-cache": "knip",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",

View File

@@ -1,7 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 5V3C14 2.44772 13.5523 2 13 2H11C10.4477 2 10 2.44772 10 3V5C10 5.55228 10.4477 6 11 6H13C13.5523 6 14 5.55228 14 5Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M6 5V3C6 2.44772 5.55228 2 5 2H3C2.44772 2 2 2.44772 2 3V5C2 5.55228 2.44772 6 3 6H5C5.55228 6 6 5.55228 6 5Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M14 13V11C14 10.4477 13.5523 10 13 10H11C10.4477 10 10 10.4477 10 11V13C10 13.5523 10.4477 14 11 14H13C13.5523 14 14 13.5523 14 13Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M10 4H6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M10 12H8C5.79086 12 4 10.2091 4 8V6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.99999 4H6.99999M8.99999 12H7.6231C5.02081 12 3.11138 9.55445 3.74252 7.02986L3.99999 6M13.6894 3.24254L13.1894 5.24254C13.0781 5.6877 12.6781 6 12.2192 6H10.2808C9.63019 6 9.15284 5.38861 9.31062 4.75746L9.81062 2.75746C9.92192 2.3123 10.3219 2 10.7808 2H12.7192C13.3698 2 13.8471 2.61139 13.6894 3.24254ZM6.68936 3.24254L6.18936 5.24254C6.07806 5.6877 5.67808 6 5.21921 6H3.28077C2.63019 6 2.15284 5.38861 2.31062 4.75746L2.81062 2.75746C2.92191 2.3123 3.3219 2 3.78077 2H5.71921C6.36978 2 6.84714 2.61139 6.68936 3.24254ZM13.6894 11.2425L13.1894 13.2425C13.0781 13.6877 12.6781 14 12.2192 14H10.2808C9.63019 14 9.15284 13.3886 9.31062 12.7575L9.81062 10.7575C9.92192 10.3123 10.3219 10 10.7808 10H12.7192C13.3698 10 13.8471 10.6114 13.6894 11.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 910 B

View File

@@ -12,7 +12,6 @@
<ColorPickerButton />
<BypassButton />
<PinButton />
<EditModelButton />
<Load3DViewerButton />
<MaskEditorButton />
<ConvertToSubgraphButton />
@@ -35,7 +34,6 @@ import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton.vue'
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'

View File

@@ -1,37 +0,0 @@
<template>
<Button
v-show="isImageOutputSelected"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_AddEditModelStep.label'),
showDelay: 1000
}"
severity="secondary"
text
icon="pi pi-pen-to-square"
@click="() => commandStore.execute('Comfy.Canvas.AddEditModelStep')"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil'
const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const isImageOutputOrEditModelNode = (node: unknown) =>
isLGraphNode(node) &&
(isImageNode(node) || node.type === 'workflow>FLUX.1 Kontext Image Edit')
const isImageOutputSelected = computed(
() =>
canvasStore.selectedItems.length === 1 &&
isImageOutputOrEditModelNode(canvasStore.selectedItems[0])
)
</script>

View File

@@ -16,7 +16,6 @@ import {
import { Point } from '@/lib/litegraph/src/litegraph'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
@@ -775,17 +774,6 @@ export function useCoreCommands(): ComfyCommand[] {
versionAdded: moveSelectedNodesVersionAdded,
function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])
},
{
id: 'Comfy.Canvas.AddEditModelStep',
icon: 'pi pi-pen-to-square',
label: 'Add Edit Model Step',
versionAdded: '1.23.3',
function: async () => {
const node = app.canvas.selectedItems.values().next().value
if (!(node instanceof LGraphNode)) return
await addFluxKontextGroupNode(node)
}
},
{
id: 'Comfy.Graph.ConvertToSubgraph',
icon: 'pi pi-sitemap',

View File

@@ -1925,6 +1925,7 @@ export class LGraphNode
}
}
widget.onRemove?.()
this.widgets.splice(widgetIndex, 1)
}

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "تصفح القوالب"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "إضافة خطوة تحرير النموذج"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "حذف العناصر المحددة"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "حول ComfyUI",
"Add Edit Model Step": "إضافة خطوة تعديل النموذج",
"Bottom Panel": "لوحة سفلية",
"Browse Templates": "تصفح القوالب",
"Bypass/Unbypass Selected Nodes": "تجاوز/إلغاء تجاوز العقد المحددة",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "Browse Templates"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "Add Edit Model Step"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Delete Selected Items"
},

View File

@@ -958,7 +958,6 @@
"Restart": "Restart",
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
"Browse Templates": "Browse Templates",
"Add Edit Model Step": "Add Edit Model Step",
"Delete Selected Items": "Delete Selected Items",
"Zoom to fit": "Zoom to fit",
"Move Selected Nodes Down": "Move Selected Nodes Down",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "Explorar plantillas"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "Agregar paso de edición de modelo"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Eliminar elementos seleccionados"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "Acerca de ComfyUI",
"Add Edit Model Step": "Agregar paso de edición de modelo",
"Bottom Panel": "Panel inferior",
"Browse Templates": "Explorar plantillas",
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "Parcourir les modèles"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "Ajouter/Modifier une étape de modèle"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Supprimer les éléments sélectionnés"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "À propos de ComfyUI",
"Add Edit Model Step": "Ajouter une étape dédition de modèle",
"Bottom Panel": "Panneau inférieur",
"Browse Templates": "Parcourir les modèles",
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "テンプレートを参照"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "編集モデルステップを追加"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "選択したアイテムを削除"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "ComfyUIについて",
"Add Edit Model Step": "モデル編集ステップを追加",
"Bottom Panel": "下部パネル",
"Browse Templates": "テンプレートを参照",
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "템플릿 탐색"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "모델 편집 단계 추가"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "선택한 항목 삭제"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "ComfyUI에 대하여",
"Add Edit Model Step": "모델 편집 단계 추가",
"Bottom Panel": "하단 패널",
"Browse Templates": "템플릿 탐색",
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "Просмотр шаблонов"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "Добавить или изменить шаг модели"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Удалить выбранные элементы"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "О ComfyUI",
"Add Edit Model Step": "Добавить или изменить шаг модели",
"Bottom Panel": "Нижняя панель",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "瀏覽範本"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "新增編輯模型步驟"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "刪除選取項目"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "關於 ComfyUI",
"Add Edit Model Step": "新增編輯模型步驟",
"Bottom Panel": "底部面板",
"Browse Templates": "瀏覽範本",
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": {
"label": "浏览模板"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "添加编辑模型步骤"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "删除选定的项目"
},

View File

@@ -762,7 +762,6 @@
},
"menuLabels": {
"About ComfyUI": "关于ComfyUI",
"Add Edit Model Step": "添加编辑模型步骤",
"Bottom Panel": "底部面板",
"Browse Templates": "浏览模板",
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",

View File

@@ -1,693 +0,0 @@
import _ from 'es-toolkit/compat'
import {
type INodeOutputSlot,
type LGraph,
type LGraphNode,
LLink,
LiteGraph,
type Point
} from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { parseFilePath } from '@/utils/formatUtil'
import { app } from './app'
const fluxKontextGroupNode = {
nodes: [
{
id: -1,
type: 'Reroute',
pos: [2354.87890625, -127.23468780517578],
size: [75, 26],
flags: {},
order: 20,
mode: 0,
inputs: [{ name: '', type: '*', link: null }],
outputs: [{ name: '', type: '*', links: null }],
properties: { showOutputText: false, horizontal: false },
index: 0
},
{
id: -1,
type: 'ReferenceLatent',
pos: [2730, -220],
size: [197.712890625, 46],
flags: {},
order: 22,
mode: 0,
inputs: [
{
localized_name: 'conditioning',
name: 'conditioning',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'latent',
name: 'latent',
shape: 7,
type: 'LATENT',
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
links: []
}
],
properties: {
'Node name for S&R': 'ReferenceLatent',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
index: 1
},
{
id: -1,
type: 'VAEDecode',
pos: [3270, -110],
size: [210, 46],
flags: {},
order: 25,
mode: 0,
inputs: [
{
localized_name: 'samples',
name: 'samples',
type: 'LATENT',
link: null
},
{
localized_name: 'vae',
name: 'vae',
type: 'VAE',
link: null
}
],
outputs: [
{
localized_name: 'IMAGE',
name: 'IMAGE',
type: 'IMAGE',
slot_index: 0,
links: []
}
],
properties: {
'Node name for S&R': 'VAEDecode',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
index: 2
},
{
id: -1,
type: 'KSampler',
pos: [2930, -110],
size: [315, 262],
flags: {},
order: 24,
mode: 0,
inputs: [
{
localized_name: 'model',
name: 'model',
type: 'MODEL',
link: null
},
{
localized_name: 'positive',
name: 'positive',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'negative',
name: 'negative',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'latent_image',
name: 'latent_image',
type: 'LATENT',
link: null
},
{
localized_name: 'seed',
name: 'seed',
type: 'INT',
widget: { name: 'seed' },
link: null
},
{
localized_name: 'steps',
name: 'steps',
type: 'INT',
widget: { name: 'steps' },
link: null
},
{
localized_name: 'cfg',
name: 'cfg',
type: 'FLOAT',
widget: { name: 'cfg' },
link: null
},
{
localized_name: 'sampler_name',
name: 'sampler_name',
type: 'COMBO',
widget: { name: 'sampler_name' },
link: null
},
{
localized_name: 'scheduler',
name: 'scheduler',
type: 'COMBO',
widget: { name: 'scheduler' },
link: null
},
{
localized_name: 'denoise',
name: 'denoise',
type: 'FLOAT',
widget: { name: 'denoise' },
link: null
}
],
outputs: [
{
localized_name: 'LATENT',
name: 'LATENT',
type: 'LATENT',
slot_index: 0,
links: []
}
],
properties: {
'Node name for S&R': 'KSampler',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [972054013131369, 'fixed', 20, 1, 'euler', 'simple', 1],
index: 3
},
{
id: -1,
type: 'FluxGuidance',
pos: [2940, -220],
size: [211.60000610351562, 58],
flags: {},
order: 23,
mode: 0,
inputs: [
{
localized_name: 'conditioning',
name: 'conditioning',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'guidance',
name: 'guidance',
type: 'FLOAT',
widget: { name: 'guidance' },
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
slot_index: 0,
links: []
}
],
properties: {
'Node name for S&R': 'FluxGuidance',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [2.5],
index: 4
},
{
id: -1,
type: 'SaveImage',
pos: [3490, -110],
size: [985.3012084960938, 1060.3828125],
flags: {},
order: 26,
mode: 0,
inputs: [
{
localized_name: 'images',
name: 'images',
type: 'IMAGE',
link: null
},
{
localized_name: 'filename_prefix',
name: 'filename_prefix',
type: 'STRING',
widget: { name: 'filename_prefix' },
link: null
}
],
outputs: [],
properties: { cnr_id: 'comfy-core', ver: '0.3.38' },
widgets_values: ['ComfyUI'],
index: 5
},
{
id: -1,
type: 'CLIPTextEncode',
pos: [2500, -110],
size: [422.84503173828125, 164.31304931640625],
flags: {},
order: 12,
mode: 0,
inputs: [
{
localized_name: 'clip',
name: 'clip',
type: 'CLIP',
link: null
},
{
localized_name: 'text',
name: 'text',
type: 'STRING',
widget: { name: 'text' },
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
slot_index: 0,
links: []
}
],
title: 'CLIP Text Encode (Positive Prompt)',
properties: {
'Node name for S&R': 'CLIPTextEncode',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: ['there is a bright light'],
color: '#232',
bgcolor: '#353',
index: 6
},
{
id: -1,
type: 'CLIPTextEncode',
pos: [2504.1435546875, 97.9598617553711],
size: [422.84503173828125, 164.31304931640625],
flags: { collapsed: true },
order: 13,
mode: 0,
inputs: [
{
localized_name: 'clip',
name: 'clip',
type: 'CLIP',
link: null
},
{
localized_name: 'text',
name: 'text',
type: 'STRING',
widget: { name: 'text' },
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
slot_index: 0,
links: []
}
],
title: 'CLIP Text Encode (Negative Prompt)',
properties: {
'Node name for S&R': 'CLIPTextEncode',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [''],
color: '#322',
bgcolor: '#533',
index: 7
},
{
id: -1,
type: 'UNETLoader',
pos: [2630, -370],
size: [270, 82],
flags: {},
order: 6,
mode: 0,
inputs: [
{
localized_name: 'unet_name',
name: 'unet_name',
type: 'COMBO',
widget: { name: 'unet_name' },
link: null
},
{
localized_name: 'weight_dtype',
name: 'weight_dtype',
type: 'COMBO',
widget: { name: 'weight_dtype' },
link: null
}
],
outputs: [
{
localized_name: 'MODEL',
name: 'MODEL',
type: 'MODEL',
links: []
}
],
properties: {
'Node name for S&R': 'UNETLoader',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: ['flux1-kontext-dev.safetensors', 'default'],
color: '#223',
bgcolor: '#335',
index: 8
},
{
id: -1,
type: 'DualCLIPLoader',
pos: [2100, -290],
size: [337.76861572265625, 130],
flags: {},
order: 8,
mode: 0,
inputs: [
{
localized_name: 'clip_name1',
name: 'clip_name1',
type: 'COMBO',
widget: { name: 'clip_name1' },
link: null
},
{
localized_name: 'clip_name2',
name: 'clip_name2',
type: 'COMBO',
widget: { name: 'clip_name2' },
link: null
},
{
localized_name: 'type',
name: 'type',
type: 'COMBO',
widget: { name: 'type' },
link: null
},
{
localized_name: 'device',
name: 'device',
shape: 7,
type: 'COMBO',
widget: { name: 'device' },
link: null
}
],
outputs: [
{
localized_name: 'CLIP',
name: 'CLIP',
type: 'CLIP',
links: []
}
],
properties: {
'Node name for S&R': 'DualCLIPLoader',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [
'clip_l.safetensors',
't5xxl_fp8_e4m3fn_scaled.safetensors',
'flux',
'default'
],
color: '#223',
bgcolor: '#335',
index: 9
},
{
id: -1,
type: 'VAELoader',
pos: [2960, -370],
size: [270, 58],
flags: {},
order: 7,
mode: 0,
inputs: [
{
localized_name: 'vae_name',
name: 'vae_name',
type: 'COMBO',
widget: { name: 'vae_name' },
link: null
}
],
outputs: [
{
localized_name: 'VAE',
name: 'VAE',
type: 'VAE',
links: []
}
],
properties: {
'Node name for S&R': 'VAELoader',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: ['ae.safetensors'],
color: '#223',
bgcolor: '#335',
index: 10
}
],
links: [
[6, 0, 1, 0, 72, 'CONDITIONING'],
[0, 0, 1, 1, 66, '*'],
[3, 0, 2, 0, 69, 'LATENT'],
[10, 0, 2, 1, 76, 'VAE'],
[8, 0, 3, 0, 74, 'MODEL'],
[4, 0, 3, 1, 70, 'CONDITIONING'],
[7, 0, 3, 2, 73, 'CONDITIONING'],
[0, 0, 3, 3, 66, '*'],
[1, 0, 4, 0, 67, 'CONDITIONING'],
[2, 0, 5, 0, 68, 'IMAGE'],
[9, 0, 6, 0, 75, 'CLIP'],
[9, 0, 7, 0, 75, 'CLIP']
],
external: [],
config: {
'0': {},
'1': {},
'2': { output: { '0': { visible: true } } },
'3': {
output: { '0': { visible: true } },
input: {
denoise: { visible: false },
cfg: { visible: false }
}
},
'4': {},
'5': {},
'6': {},
'7': { input: { text: { visible: false } } },
'8': { input: { weight_dtype: { visible: false } } },
'9': { input: { type: { visible: false }, device: { visible: false } } },
'10': {}
}
}
export async function ensureGraphHasFluxKontextGroupNode(
graph: LGraph & { extra: { groupNodes?: Record<string, any> } }
) {
graph.extra ??= {}
graph.extra.groupNodes ??= {}
if (graph.extra.groupNodes['FLUX.1 Kontext Image Edit']) return
graph.extra.groupNodes['FLUX.1 Kontext Image Edit'] =
structuredClone(fluxKontextGroupNode)
// Lazy import to avoid circular dependency issues
const { GroupNodeConfig } = await import('@/extensions/core/groupNode')
await GroupNodeConfig.registerFromWorkflow(
{
'FLUX.1 Kontext Image Edit':
graph.extra.groupNodes['FLUX.1 Kontext Image Edit']
},
[]
)
}
export async function addFluxKontextGroupNode(fromNode: LGraphNode) {
const { canvas } = app
const { graph } = canvas
if (!graph) throw new TypeError('Graph is not initialized')
await ensureGraphHasFluxKontextGroupNode(graph)
const node = LiteGraph.createNode('workflow>FLUX.1 Kontext Image Edit')
if (!node) throw new TypeError('Failed to create node')
const pos = getPosToRightOfNode(fromNode)
graph.add(node)
node.pos = pos
app.canvas.processSelect(node, undefined)
connectPreviousLatent(fromNode, node)
const symb = Object.getOwnPropertySymbols(node)[0]
// @ts-expect-error It's there -- promise.
node[symb].populateWidgets()
setWidgetValues(node)
}
function setWidgetValues(node: LGraphNode) {
const seedInput = node.widgets?.find((x) => x.name === 'seed')
if (!seedInput) throw new TypeError('Seed input not found')
seedInput.value = Math.floor(Math.random() * 1_125_899_906_842_624)
const firstClip = node.widgets?.find((x) => x.name === 'clip_name1')
setPreferredValue('t5xxl_fp8_e4m3fn_scaled.safetensors', 't5xxl', firstClip)
const secondClip = node.widgets?.find((x) => x.name === 'clip_name2')
setPreferredValue('clip_l.safetensors', 'clip_l', secondClip)
const unet = node.widgets?.find((x) => x.name === 'unet_name')
setPreferredValue('flux1-dev-kontext_fp8_scaled.safetensors', 'kontext', unet)
const vae = node.widgets?.find((x) => x.name === 'vae_name')
setPreferredValue('ae.safetensors', 'ae.s', vae)
}
function setPreferredValue(
preferred: string,
match: string,
widget: IBaseWidget | undefined
): void {
if (!widget) throw new TypeError('Widget not found')
const { values } = widget.options
if (!Array.isArray(values)) return
// Match against filename portion only
const mapped = values.map((x) => parseFilePath(x).filename)
const value =
mapped.find((x) => x === preferred) ??
mapped.find((x) => x.includes?.(match))
widget.value = value ?? preferred
}
function getPosToRightOfNode(fromNode: LGraphNode) {
const nodes = app.canvas.graph?.nodes
if (!nodes) throw new TypeError('Could not get graph nodes')
const pos = [
fromNode.pos[0] + fromNode.size[0] + 100,
fromNode.pos[1]
] satisfies Point
while (nodes.find((x) => isPointTooClose(x.pos, pos))) {
pos[0] += 20
pos[1] += 20
}
return pos
}
function connectPreviousLatent(fromNode: LGraphNode, toEditNode: LGraphNode) {
const { canvas } = app
const { graph } = canvas
if (!graph) throw new TypeError('Graph is not initialized')
const l = findNearestOutputOfType([fromNode], 'LATENT')
if (!l) {
const imageOutput = findNearestOutputOfType([fromNode], 'IMAGE')
if (!imageOutput) throw new TypeError('No image output found')
const vaeEncode = LiteGraph.createNode('VAEEncode')
if (!vaeEncode) throw new TypeError('Failed to create node')
const { node: imageNode, index: imageIndex } = imageOutput
graph.add(vaeEncode)
vaeEncode.pos = getPosToRightOfNode(fromNode)
vaeEncode.pos[1] -= 200
vaeEncode.connect(0, toEditNode, 0)
imageNode.connect(imageIndex, vaeEncode, 0)
return
}
const { node, index } = l
node.connect(index, toEditNode, 0)
}
function getInputNodes(node: LGraphNode): LGraphNode[] {
return node.inputs
.map((x) => LLink.resolve(x.link, app.graph)?.outputNode)
.filter((x) => !!x)
}
function getOutputOfType(
node: LGraphNode,
type: string
): {
output: INodeOutputSlot
index: number
} {
const index = node.outputs.findIndex((x) => x.type === type)
const output = node.outputs[index]
return { output, index }
}
function findNearestOutputOfType(
nodes: Iterable<LGraphNode>,
type: string = 'LATENT',
depth: number = 0
): { node: LGraphNode; index: number } | undefined {
for (const node of nodes) {
const { output, index } = getOutputOfType(node, type)
if (output) return { node, index }
}
if (depth < 3) {
const closestNodes = new Set([...nodes].flatMap((x) => getInputNodes(x)))
const res = findNearestOutputOfType(closestNodes, type, depth + 1)
if (res) return res
}
}
function isPointTooClose(a: Point, b: Point, precision: number = 5) {
return Math.abs(a[0] - b[0]) < precision && Math.abs(a[1] - b[1]) < precision
}