diff --git a/.claude/commands/create-frontend-release.md b/.claude/commands/create-frontend-release.md
new file mode 100644
index 000000000..2a9dd7445
--- /dev/null
+++ b/.claude/commands/create-frontend-release.md
@@ -0,0 +1,654 @@
+# Create Frontend Release
+
+This command guides you through creating a comprehensive frontend release with semantic versioning analysis, automated change detection, security scanning, and multi-stage human verification.
+
+
+Create a frontend release with version type: $ARGUMENTS
+
+Expected format: Version increment type and optional description
+Examples:
+- `patch` - Bug fixes only
+- `minor` - New features, backward compatible
+- `major` - Breaking changes
+- `prerelease` - Alpha/beta/rc releases
+- `patch "Critical security fixes"` - With custom description
+- `minor --skip-changelog` - Skip automated changelog generation
+- `minor --dry-run` - Simulate release without executing
+
+If no arguments provided, the command will analyze recent changes and recommend the appropriate version type.
+
+
+## Prerequisites
+
+Before starting, ensure:
+- You have push access to the repository
+- GitHub CLI (`gh`) is authenticated
+- You're on a clean main branch working tree
+- All intended changes are merged to main
+- You understand the scope of changes being released
+
+## Critical Checks Before Starting
+
+### 1. Check Current Version Status
+```bash
+# Get current version and check if it's a pre-release
+CURRENT_VERSION=$(node -p "require('./package.json').version")
+if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
+ echo "⚠️ Current version $CURRENT_VERSION is a pre-release"
+ echo "Consider promoting to stable (e.g., 1.24.0-1 → 1.24.0) first"
+fi
+```
+
+### 2. Find Last Stable Release
+```bash
+# Get last stable release tag (no pre-release suffix)
+LAST_STABLE=$(git tag -l "v*" | grep -v "\-" | sort -V | tail -1)
+echo "Last stable release: $LAST_STABLE"
+```
+
+## Configuration Options
+
+**Environment Variables:**
+- `RELEASE_SKIP_SECURITY_SCAN=true` - Skip security audit
+- `RELEASE_AUTO_APPROVE=true` - Skip some confirmation prompts
+- `RELEASE_DRY_RUN=true` - Simulate release without executing
+
+## Release Process
+
+### Step 1: Environment Safety Check
+
+1. Verify clean working directory:
+ ```bash
+ git status --porcelain
+ ```
+2. Confirm on main branch:
+ ```bash
+ git branch --show-current
+ ```
+3. Pull latest changes:
+ ```bash
+ git pull origin main
+ ```
+4. Check GitHub CLI authentication:
+ ```bash
+ gh auth status
+ ```
+5. Verify npm/PyPI publishing access (dry run)
+6. **CONFIRMATION REQUIRED**: Environment ready for release?
+
+### Step 2: Analyze Recent Changes
+
+1. Get current version from package.json
+2. **IMPORTANT**: Determine correct base for comparison:
+ ```bash
+ # If current version is pre-release, use last stable release
+ if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
+ BASE_TAG=$LAST_STABLE
+ else
+ BASE_TAG=$(git describe --tags --abbrev=0)
+ fi
+ ```
+3. Find commits since base release (CRITICAL: use --first-parent):
+ ```bash
+ git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent
+ ```
+4. Count total commits:
+ ```bash
+ COMMIT_COUNT=$(git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent | wc -l)
+ echo "Found $COMMIT_COUNT commits since $BASE_TAG"
+ ```
+5. Analyze commits for:
+ - Breaking changes (BREAKING CHANGE, !, feat())
+ - New features (feat:, feature:)
+ - Bug fixes (fix:, bugfix:)
+ - Documentation changes (docs:)
+ - Dependency updates
+6. **VERIFY PR TARGET BRANCHES**:
+ ```bash
+ # Get merged PRs and verify they were merged to main
+ gh pr list --state merged --limit 50 --json number,title,baseRefName,mergedAt | \
+ jq -r '.[] | select(.baseRefName == "main") | "\(.number): \(.title)"'
+ ```
+7. **HUMAN ANALYSIS**: Review change summary and verify scope
+
+### Step 3: Semantic Version Determination
+
+Based on analysis, determine version type:
+
+**Pre-release Handling:**
+- If current version is pre-release (e.g., 1.24.0-1):
+ - Consider promoting to stable (1.24.0) instead of new version
+ - Or create new minor/major if significant changes added
+
+**Automatic Detection:**
+- **MAJOR**: Breaking changes detected (`BREAKING CHANGE`, `!` in commits)
+- **MINOR**: New features without breaking changes (`feat:` commits)
+- **PATCH**: Only bug fixes, docs, or dependency updates
+
+**Version Workflow Limitations:**
+- ⚠️ Cannot use "stable" as version_type - not in allowed values
+- Allowed values: patch, minor, major, prepatch, preminor, premajor, prerelease
+- For pre-release → stable promotion, must manually update version
+
+**Manual Override Options:**
+- If arguments provided, validate against detected changes
+- **CONFIRMATION REQUIRED**: Version type correct for these changes?
+- **WARNING**: If manual override conflicts with detected breaking changes
+
+**Version Preview:**
+- Current: `${CURRENT_VERSION}`
+- Proposed: Show exact version number
+- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
+
+### Step 4: Security and Dependency Audit
+
+1. Run security audit:
+ ```bash
+ npm audit --audit-level moderate
+ ```
+2. Check for known vulnerabilities in dependencies
+3. Scan for hardcoded secrets or credentials:
+ ```bash
+ git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
+ ```
+4. Verify no sensitive data in recent commits
+5. **SECURITY REVIEW**: Address any critical findings before proceeding?
+
+### Step 5: Pre-Release Testing
+
+1. Run complete test suite:
+ ```bash
+ npm run test:unit
+ npm run test:component
+ npm run test:browser
+ ```
+2. Run type checking:
+ ```bash
+ npm run typecheck
+ ```
+3. Run linting (may have issues with missing packages):
+ ```bash
+ npm run lint || echo "Lint issues - verify if critical"
+ ```
+4. Test build process:
+ ```bash
+ npm run build
+ npm run build:types
+ ```
+5. **QUALITY GATE**: All tests and builds passing?
+
+### Step 6: Breaking Change Analysis
+
+For minor/major releases:
+1. Analyze API changes in:
+ - Public TypeScript interfaces
+ - Extension APIs
+ - Component props
+ - CLAUDE.md guidelines
+2. Check for:
+ - Removed public functions/classes
+ - Changed function signatures
+ - Deprecated feature removals
+ - Configuration changes
+3. Generate breaking change summary
+4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified?
+
+### Step 7: Generate and Save Changelog
+
+1. Extract commit messages since base release:
+ ```bash
+ git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent > commits.txt
+ ```
+2. **CRITICAL**: Verify PR inclusion by checking merge location:
+ ```bash
+ # For each significant PR mentioned, verify it's on main
+ for PR in ${SIGNIFICANT_PRS}; do
+ COMMIT=$(gh pr view $PR --json mergeCommit -q .mergeCommit.oid)
+ git branch -r --contains $COMMIT | grep -q "origin/main" || \
+ echo "WARNING: PR #$PR not on main branch!"
+ done
+ ```
+3. Group by type:
+ - 🚀 **Features** (feat:)
+ - 🐛 **Bug Fixes** (fix:)
+ - 💥 **Breaking Changes** (BREAKING CHANGE)
+ - 📚 **Documentation** (docs:)
+ - 🔧 **Maintenance** (chore:, refactor:)
+ - ⬆️ **Dependencies** (deps:, dependency updates)
+4. Include PR numbers and links
+5. Add issue references (Fixes #123)
+6. **Save changelog locally:**
+ ```bash
+ # Save to dated file for history
+ echo "$CHANGELOG" > release-notes-${NEW_VERSION}-$(date +%Y%m%d).md
+
+ # Save to current for easy access
+ echo "$CHANGELOG" > CURRENT_RELEASE_NOTES.md
+ ```
+7. **CHANGELOG REVIEW**: Verify all PRs listed are actually on main branch
+
+### Step 8: Create Enhanced Release Notes
+
+1. Create comprehensive user-facing release notes including:
+ - **What's New**: Major features and improvements
+ - **Bug Fixes**: User-visible fixes
+ - **Breaking Changes**: Migration guide if applicable
+ - **Dependencies**: Major dependency updates
+ - **Performance**: Notable performance improvements
+ - **Contributors**: Thank contributors for their work
+2. Reference related documentation updates
+3. Include screenshots for UI changes (if available)
+4. **Save release notes:**
+ ```bash
+ # Enhanced release notes for GitHub
+ echo "$RELEASE_NOTES" > github-release-notes-${NEW_VERSION}.md
+ ```
+5. **CONTENT REVIEW**: Release notes clear and helpful for users?
+
+### Step 9: Create Version Bump PR
+
+**For standard version bumps (patch/minor/major):**
+```bash
+# Trigger the workflow
+gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE}
+
+# Workflow runs quickly - usually creates PR within 30 seconds
+echo "Workflow triggered. Waiting for PR creation..."
+```
+
+**For pre-release → stable promotion:**
+1. Must manually create branch and update version:
+ ```bash
+ git checkout -b version-bump-${NEW_VERSION}
+ # Edit package.json to remove pre-release suffix
+ git add package.json
+ git commit -m "${NEW_VERSION}"
+ git push origin version-bump-${NEW_VERSION}
+ ```
+
+2. Wait for PR creation (if using workflow) or create manually:
+ ```bash
+ # For workflow-created PRs - wait and find it
+ sleep 30
+ # Look for PR from comfy-pr-bot (not github-actions)
+ PR_NUMBER=$(gh pr list --author comfy-pr-bot --limit 1 --json number --jq '.[0].number')
+
+ # Verify we got the PR
+ if [ -z "$PR_NUMBER" ]; then
+ echo "PR not found yet. Checking recent PRs..."
+ gh pr list --limit 5 --json number,title,author
+ fi
+
+ # For manual PRs
+ gh pr create --title "${NEW_VERSION}" \
+ --body-file enhanced-pr-description.md \
+ --label "Release"
+ ```
+3. **Create enhanced PR description:**
+ ```bash
+ cat > enhanced-pr-description.md << EOF
+ # Release v${NEW_VERSION}
+
+ ## Version Change
+ \`${CURRENT_VERSION}\` → \`${NEW_VERSION}\` (${VERSION_TYPE})
+
+ ## Changelog
+ ${CHANGELOG}
+
+ ## Breaking Changes
+ ${BREAKING_CHANGES}
+
+ ## Testing Performed
+ - ✅ Full test suite (unit, component, browser)
+ - ✅ TypeScript compilation
+ - ✅ Linting checks
+ - ✅ Build verification
+ - ✅ Security audit
+
+ ## Distribution Channels
+ - GitHub Release (with dist.zip)
+ - PyPI Package (comfyui-frontend-package)
+ - npm Package (@comfyorg/comfyui-frontend-types)
+
+ ## Post-Release Tasks
+ - [ ] Verify all distribution channels
+ - [ ] Update external documentation
+ - [ ] Monitor for issues
+ EOF
+ ```
+4. Update PR with enhanced description:
+ ```bash
+ gh pr edit ${PR_NUMBER} --body-file enhanced-pr-description.md
+ ```
+5. Add changelog as comment for easy reference:
+ ```bash
+ gh pr comment ${PR_NUMBER} --body-file CURRENT_RELEASE_NOTES.md
+ ```
+6. **PR REVIEW**: Version bump PR created and enhanced correctly?
+
+### Step 11: Critical Release PR Verification
+
+1. **CRITICAL**: Verify PR has "Release" label:
+ ```bash
+ gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \
+ echo "ERROR: Release label missing! Add it immediately!"
+ ```
+2. Check for update-locales commits:
+ ```bash
+ # WARNING: update-locales may add [skip ci] which blocks release workflow!
+ gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \
+ echo "WARNING: [skip ci] detected - release workflow may not trigger!"
+ ```
+3. Verify version number in package.json
+4. Review all changed files
+5. Ensure no unintended changes included
+6. Wait for required PR checks:
+ ```bash
+ gh pr checks ${PR_NUMBER} --watch
+ ```
+7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
+
+### Step 12: Pre-Merge Validation
+
+1. **Review Requirements**: Release PRs require approval
+2. Monitor CI checks - watch for update-locales
+3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger!
+4. Check no new commits to main since PR creation
+5. **DEPLOYMENT READINESS**: Ready to merge?
+
+### Step 13: Execute Release
+
+1. **FINAL CONFIRMATION**: Merge PR to trigger release?
+2. Merge the Release PR:
+ ```bash
+ gh pr merge ${PR_NUMBER} --merge
+ ```
+3. **IMMEDIATELY CHECK**: Did release workflow trigger?
+ ```bash
+ sleep 10
+ gh run list --workflow=release.yaml --limit=1
+ ```
+4. If workflow didn't trigger due to [skip ci]:
+ ```bash
+ echo "ERROR: Release workflow didn't trigger!"
+ echo "Options:"
+ echo "1. Create patch release (e.g., 1.24.1) to trigger workflow"
+ echo "2. Investigate manual release options"
+ ```
+5. If workflow triggered, monitor execution:
+ ```bash
+ WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
+ gh run watch ${WORKFLOW_RUN_ID}
+ ```
+
+### Step 14: Enhance GitHub Release
+
+1. Wait for automatic release creation:
+ ```bash
+ # Wait for release to be created
+ while ! gh release view v${NEW_VERSION} >/dev/null 2>&1; do
+ echo "Waiting for release creation..."
+ sleep 10
+ done
+ ```
+
+2. **Enhance the GitHub release:**
+ ```bash
+ # Update release with our enhanced notes
+ gh release edit v${NEW_VERSION} \
+ --title "🚀 ComfyUI Frontend v${NEW_VERSION}" \
+ --notes-file github-release-notes-${NEW_VERSION}.md \
+ --latest
+
+ # Add any additional assets if needed
+ # gh release upload v${NEW_VERSION} additional-assets.zip
+ ```
+
+3. **Verify release details:**
+ ```bash
+ gh release view v${NEW_VERSION}
+ ```
+
+### Step 15: Verify Multi-Channel Distribution
+
+1. **GitHub Release:**
+ ```bash
+ gh release view v${NEW_VERSION} --json assets,body,createdAt,tagName
+ ```
+ - ✅ Check release notes
+ - ✅ Verify dist.zip attachment
+ - ✅ Confirm release marked as latest (for main branch)
+
+2. **PyPI Package:**
+ ```bash
+ # Check PyPI availability (may take a few minutes)
+ for i in {1..10}; do
+ if curl -s https://pypi.org/pypi/comfyui-frontend-package/json | jq -r '.releases | keys[]' | grep -q ${NEW_VERSION}; then
+ echo "✅ PyPI package available"
+ break
+ fi
+ echo "⏳ Waiting for PyPI package... (attempt $i/10)"
+ sleep 30
+ done
+ ```
+
+3. **npm Package:**
+ ```bash
+ # Check npm availability
+ for i in {1..10}; do
+ if npm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then
+ echo "✅ npm package available"
+ break
+ fi
+ echo "⏳ Waiting for npm package... (attempt $i/10)"
+ sleep 30
+ done
+ ```
+
+4. **DISTRIBUTION VERIFICATION**: All channels published successfully?
+
+### Step 16: Post-Release Monitoring Setup
+
+1. **Monitor immediate release health:**
+ ```bash
+ # Check for immediate issues
+ gh issue list --label "bug" --state open --limit 5 --json title,number,createdAt
+
+ # Monitor download metrics (if accessible)
+ gh release view v${NEW_VERSION} --json assets --jq '.assets[].downloadCount'
+ ```
+
+2. **Update documentation tracking:**
+ ```bash
+ cat > post-release-checklist.md << EOF
+ # Post-Release Checklist for v${NEW_VERSION}
+
+ ## Immediate Tasks (Next 24 hours)
+ - [ ] Monitor error rates and user feedback
+ - [ ] Watch for critical issues
+ - [ ] Verify documentation is up to date
+ - [ ] Check community channels for questions
+
+ ## Short-term Tasks (Next week)
+ - [ ] Update external integration guides
+ - [ ] Monitor adoption metrics
+ - [ ] Gather user feedback
+ - [ ] Plan next release cycle
+
+ ## Long-term Tasks
+ - [ ] Analyze release process improvements
+ - [ ] Update release templates based on learnings
+ - [ ] Document any new patterns discovered
+
+ ## Key Metrics to Track
+ - Download counts: GitHub, PyPI, npm
+ - Issue reports related to v${NEW_VERSION}
+ - Community feedback and adoption
+ - Performance impact measurements
+ EOF
+ ```
+
+3. **Create release summary:**
+ ```bash
+ cat > release-summary-${NEW_VERSION}.md << EOF
+ # Release Summary: ComfyUI Frontend v${NEW_VERSION}
+
+ **Released:** $(date)
+ **Type:** ${VERSION_TYPE}
+ **Duration:** ~${RELEASE_DURATION} minutes
+ **Release Commit:** ${RELEASE_COMMIT}
+
+ ## Metrics
+ - **Commits Included:** ${COMMITS_COUNT}
+ - **Contributors:** ${CONTRIBUTORS_COUNT}
+ - **Files Changed:** ${FILES_CHANGED}
+ - **Lines Added/Removed:** +${LINES_ADDED}/-${LINES_REMOVED}
+
+ ## Distribution Status
+ - ✅ GitHub Release: Published
+ - ✅ PyPI Package: Available
+ - ✅ npm Types: Available
+
+ ## Next Steps
+ - Monitor for 24-48 hours
+ - Address any critical issues immediately
+ - Plan next release cycle
+
+ ## Files Generated
+ - \`release-notes-${NEW_VERSION}-$(date +%Y%m%d).md\` - Detailed changelog
+ - \`github-release-notes-${NEW_VERSION}.md\` - GitHub release notes
+ - \`post-release-checklist.md\` - Follow-up tasks
+ EOF
+ ```
+
+4. **RELEASE COMPLETION**: All post-release setup completed?
+
+## Advanced Safety Features
+
+### Rollback Procedures
+
+**Pre-Merge Rollback:**
+```bash
+# Close version bump PR and reset
+gh pr close ${PR_NUMBER}
+git reset --hard origin/main
+git clean -fd
+```
+
+**Post-Merge Rollback:**
+```bash
+# Create immediate patch release with reverts
+git revert ${RELEASE_COMMIT}
+# Follow this command again with patch version
+```
+
+**Emergency Procedures:**
+```bash
+# Document incident
+cat > release-incident-${NEW_VERSION}.md << EOF
+# Release Incident Report
+
+**Version:** ${NEW_VERSION}
+**Issue:** [Describe the problem]
+**Impact:** [Severity and scope]
+**Resolution:** [Steps taken]
+**Prevention:** [Future improvements]
+EOF
+
+# Contact package registries for critical issues
+echo "For critical security issues, consider:"
+echo "- PyPI: Contact support for package yanking"
+echo "- npm: Use 'npm unpublish' within 72 hours"
+echo "- GitHub: Update release with warning notes"
+```
+
+### Quality Gates Summary
+
+The command implements multiple quality gates:
+
+1. **🔒 Security Gate**: Vulnerability scanning, secret detection
+2. **🧪 Quality Gate**: Full test suite, linting, type checking
+3. **📋 Content Gate**: Changelog accuracy, release notes quality
+4. **🔄 Process Gate**: Release timing verification
+5. **✅ Verification Gate**: Multi-channel publishing confirmation
+6. **📊 Monitoring Gate**: Post-release health tracking
+
+## Common Scenarios
+
+### Scenario 1: Regular Feature Release
+```bash
+/project:create-frontend-release minor
+```
+- Analyzes features since last release
+- Generates changelog automatically
+- Creates comprehensive release notes
+
+### Scenario 2: Critical Security Patch
+```bash
+/project:create-frontend-release patch "Security fixes for CVE-2024-XXXX"
+```
+- Expedited security scanning
+- Enhanced monitoring setup
+
+### Scenario 3: Major Version with Breaking Changes
+```bash
+/project:create-frontend-release major
+```
+- Comprehensive breaking change analysis
+- Migration guide generation
+
+### Scenario 4: Pre-release Testing
+```bash
+/project:create-frontend-release prerelease
+```
+- Creates alpha/beta/rc versions
+- Draft release status
+
+## Common Issues and Solutions
+
+### Issue: Pre-release Version Confusion
+**Problem**: Not sure whether to promote pre-release or create new version
+**Solution**:
+- If no new commits since pre-release: promote to stable
+- If new commits exist: consider new minor version
+
+### Issue: Wrong Commit Count
+**Problem**: Changelog includes commits from other branches
+**Solution**: Always use `--first-parent` flag with git log
+
+### Issue: Release Workflow Doesn't Trigger
+**Problem**: update-locales adds [skip ci] to PR
+**Solution**:
+1. Create patch release to trigger workflow
+2. Alternative: Revert version and re-run version bump workflow
+3. Fix update-locales to skip [skip ci] for Release PRs
+
+**Update**: Sometimes update-locales doesn't add [skip ci] - always verify!
+
+### Issue: Version Workflow Limitations
+**Problem**: Cannot use "stable" as version_type
+**Solution**: Manually create PR for pre-release → stable promotion
+
+### Issue: Missing PRs in Changelog
+**Problem**: PR was merged to different branch
+**Solution**: Verify PR merge target with:
+```bash
+gh pr view ${PR_NUMBER} --json baseRefName
+```
+
+### Issue: Release Failed Due to [skip ci]
+**Problem**: Release workflow didn't trigger after merge
+**Recovery Strategy**:
+1. Revert version in a new PR (e.g., 1.24.0 → 1.24.0-1)
+2. Merge the revert PR
+3. Run version bump workflow again
+4. This creates a fresh PR without [skip ci]
+Benefits: Cleaner than creating extra version numbers
+
+## Key Learnings & Notes
+
+1. **PR Author**: Version bump PRs are created by `comfy-pr-bot`, not `github-actions`
+2. **Workflow Speed**: Version bump workflow typically completes in ~20-30 seconds
+3. **Update-locales Behavior**: Inconsistent - sometimes adds [skip ci], sometimes doesn't
+4. **Recovery Options**: Reverting version is cleaner than creating extra versions
+
diff --git a/.claude/commands/create-hotfix-release.md b/.claude/commands/create-hotfix-release.md
new file mode 100644
index 000000000..b1e521a29
--- /dev/null
+++ b/.claude/commands/create-hotfix-release.md
@@ -0,0 +1,222 @@
+# Create Hotfix Release
+
+This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step.
+
+
+Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS
+
+Expected format: Comma-separated list of commits or PR numbers
+Examples:
+- `abc123,def456,ghi789` (commits)
+- `#1234,#5678` (PRs)
+- `abc123,#1234,def456` (mixed)
+
+If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs.
+
+
+## Prerequisites
+
+Before starting, ensure:
+- You have push access to the repository
+- GitHub CLI (`gh`) is authenticated
+- You're on a clean working tree
+- You understand the commits/PRs you're cherry-picking
+
+## Hotfix Release Process
+
+### Step 1: Identify Target Core Branch
+
+1. Fetch the current ComfyUI requirements.txt from master branch:
+ ```bash
+ curl -s https://raw.githubusercontent.com/comfyanonymous/ComfyUI/master/requirements.txt | grep "comfyui-frontend-package"
+ ```
+2. Extract the `comfyui-frontend-package` version (e.g., `comfyui-frontend-package==1.23.4`)
+3. Parse version to get major.minor (e.g., `1.23.4` → `1.23`)
+4. Determine core branch: `core/.` (e.g., `core/1.23`)
+5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
+6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
+
+### Step 2: Parse and Validate Arguments
+
+1. Parse the comma-separated list of commits/PRs
+2. For each item:
+ - If starts with `#`: Treat as PR number
+ - Otherwise: Treat as commit hash
+3. For PR numbers:
+ - Fetch PR details using `gh pr view `
+ - Extract the merge commit if PR is merged
+ - If PR has multiple commits, list them all
+ - **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
+4. Validate all commit hashes exist in the repository
+
+### Step 3: Analyze Target Changes
+
+1. For each commit/PR to cherry-pick:
+ - Display commit hash, author, date
+ - Show PR title and number (if applicable)
+ - Display commit message
+ - Show files changed and diff statistics
+ - Check if already in core branch: `git branch --contains `
+2. Identify potential conflicts by checking changed files
+3. **CONFIRMATION REQUIRED**: Proceed with these commits?
+
+### Step 4: Create Hotfix Branch
+
+1. Checkout the core branch (e.g., `core/1.23`)
+2. Pull latest changes: `git pull origin core/X.Y`
+3. Display current version from package.json
+4. Create hotfix branch: `hotfix/-`
+ - Example: `hotfix/1.23.4-20241120`
+5. **CONFIRMATION REQUIRED**: Created branch correctly?
+
+### Step 5: Cherry-pick Changes
+
+For each commit:
+1. Attempt cherry-pick: `git cherry-pick `
+2. If conflicts occur:
+ - Display conflict details
+ - Show conflicting sections
+ - Provide resolution guidance
+ - **CONFIRMATION REQUIRED**: Conflicts resolved correctly?
+3. After successful cherry-pick:
+ - Show the changes: `git show HEAD`
+ - Run validation: `npm run typecheck && npm run lint`
+4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
+
+### Step 6: Create PR to Core Branch
+
+1. Push the hotfix branch: `git push origin hotfix/-`
+2. Create PR using gh CLI:
+ ```bash
+ gh pr create --base core/X.Y --head hotfix/- \
+ --title "[Hotfix] Cherry-pick fixes to core/X.Y" \
+ --body "Cherry-picked commits: ..."
+ ```
+3. Add appropriate labels (but NOT "Release" yet)
+4. PR body should include:
+ - List of cherry-picked commits/PRs
+ - Original issue references
+ - Testing instructions
+ - Impact assessment
+5. **CONFIRMATION REQUIRED**: PR created correctly?
+
+### Step 7: Wait for Tests
+
+1. Monitor PR checks: `gh pr checks`
+2. Display test results as they complete
+3. If any tests fail:
+ - Show failure details
+ - Analyze if related to cherry-picks
+ - **DECISION REQUIRED**: Fix and continue, or abort?
+4. Wait for all required checks to pass
+5. **CONFIRMATION REQUIRED**: All tests passing?
+
+### Step 8: Merge Hotfix PR
+
+1. Verify all checks have passed
+2. Check for required approvals
+3. Merge the PR: `gh pr merge --merge`
+4. Delete the hotfix branch
+5. **CONFIRMATION REQUIRED**: PR merged successfully?
+
+### Step 9: Create Version Bump
+
+1. Checkout the core branch: `git checkout core/X.Y`
+2. Pull latest changes: `git pull origin core/X.Y`
+3. Read current version from package.json
+4. Determine patch version increment:
+ - Current: `1.23.4` → New: `1.23.5`
+5. Create release branch named with new version: `release/1.23.5`
+6. Update version in package.json to `1.23.5`
+7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
+8. **CONFIRMATION REQUIRED**: Version bump correct?
+
+### Step 10: Create Release PR
+
+1. Push release branch: `git push origin release/1.23.5`
+2. Create PR with Release label:
+ ```bash
+ gh pr create --base core/X.Y --head release/1.23.5 \
+ --title "[Release] v1.23.5" \
+ --body "..." \
+ --label "Release"
+ ```
+3. **CRITICAL**: Verify "Release" label is added
+4. PR description should include:
+ - Version: `1.23.4` → `1.23.5`
+ - Included fixes (link to previous PR)
+ - Release notes for users
+5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
+
+### Step 11: Monitor Release Process
+
+1. Wait for PR checks to pass
+2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
+3. Merge the PR: `gh pr merge --merge`
+4. Monitor release workflow:
+ ```bash
+ gh run list --workflow=release.yaml --limit=1
+ gh run watch
+ ```
+5. Track progress:
+ - GitHub release draft/publication
+ - PyPI upload
+ - npm types publication
+
+### Step 12: Post-Release Verification
+
+1. Verify GitHub release:
+ ```bash
+ gh release view v1.23.5
+ ```
+2. Check PyPI package:
+ ```bash
+ pip index versions comfyui-frontend-package | grep 1.23.5
+ ```
+3. Verify npm package:
+ ```bash
+ npm view @comfyorg/comfyui-frontend-types@1.23.5
+ ```
+4. Generate release summary with:
+ - Version released
+ - Commits included
+ - Issues fixed
+ - Distribution status
+5. **CONFIRMATION REQUIRED**: Release completed successfully?
+
+## Safety Checks
+
+Throughout the process:
+- Always verify core branch matches ComfyUI's requirements.txt
+- For PRs: Ensure using correct commits (merge vs individual)
+- Check version numbers follow semantic versioning
+- **Critical**: "Release" label must be on version bump PR
+- Validate cherry-picks don't break core branch stability
+- Keep audit trail of all operations
+
+## Rollback Procedures
+
+If something goes wrong:
+- Before push: `git reset --hard origin/core/X.Y`
+- After PR creation: Close PR and start over
+- After failed release: Create new patch version with fixes
+- Document any issues for future reference
+
+## Important Notes
+
+- Core branch version will be behind main - this is expected
+- The "Release" label triggers the PyPI/npm publication
+- PR numbers must include the `#` prefix
+- Mixed commits/PRs are supported but review carefully
+- Always wait for full test suite before proceeding
+
+## Expected Timeline
+
+- Step 1-3: ~10 minutes (analysis)
+- Steps 4-6: ~15-30 minutes (cherry-picking)
+- Step 7: ~10-20 minutes (tests)
+- Steps 8-10: ~10 minutes (version bump)
+- Step 11-12: ~15-20 minutes (release)
+- Total: ~60-90 minutes
+
+This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released.
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
index a19f49ff7..a8866207a 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,5 +1,9 @@
if [[ "$OS" == "Windows_NT" ]]; then
npx.cmd lint-staged
+ # Check for unused i18n keys in staged files
+ npx.cmd tsx scripts/check-unused-i18n-keys.ts
else
npx lint-staged
+ # Check for unused i18n keys in staged files
+ npx tsx scripts/check-unused-i18n-keys.ts
fi
diff --git a/.i18nrc.cjs b/.i18nrc.cjs
index 7b27108fe..01a04cd97 100644
--- a/.i18nrc.cjs
+++ b/.i18nrc.cjs
@@ -9,9 +9,10 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
- outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
+ outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
+ Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
`
});
diff --git a/README.md b/README.md
index 0004bdecf..d490ae628 100644
--- a/README.md
+++ b/README.md
@@ -529,7 +529,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- - Node.js (v16 or later) and npm
+ - Node.js (v16 or later; v20/v22 strongly recommended) and npm
- Git for version control
- A running ComfyUI backend instance
diff --git a/browser_tests/README.md b/browser_tests/README.md
index 88bd865f8..1aeaa6e54 100644
--- a/browser_tests/README.md
+++ b/browser_tests/README.md
@@ -14,7 +14,7 @@ Clone to your `custom_nodes` dir
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
### Node.js & Playwright Prerequisites
-Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
+Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
```bash
npx playwright install chromium --with-deps
```
diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png
index f133bdf6f..9f713b4a1 100644
Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png differ
diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png
index 2adb3ad8d..3d0809c96 100644
Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png differ
diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png
index 0341dac37..588674402 100644
Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png differ
diff --git a/browser_tests/tests/releaseNotifications.spec.ts b/browser_tests/tests/releaseNotifications.spec.ts
index 5e5e58001..19d09327d 100644
--- a/browser_tests/tests/releaseNotifications.spec.ts
+++ b/browser_tests/tests/releaseNotifications.spec.ts
@@ -130,4 +130,239 @@ test.describe('Release Notifications', () => {
whatsNewSection.locator('text=No recent releases')
).toBeVisible()
})
+
+ test('should hide "What\'s New" section when notifications are disabled', async ({
+ comfyPage
+ }) => {
+ // Disable version update notifications
+ await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
+
+ // Mock release API with test data
+ await comfyPage.page.route('**/releases**', async (route) => {
+ const url = route.request().url()
+ if (
+ url.includes('api.comfy.org') ||
+ url.includes('stagingapi.comfy.org')
+ ) {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([
+ {
+ id: 1,
+ project: 'comfyui',
+ version: 'v0.3.44',
+ attention: 'high',
+ content: '## New Features\n\n- Added awesome feature',
+ published_at: new Date().toISOString()
+ }
+ ])
+ })
+ } else {
+ await route.continue()
+ }
+ })
+
+ await comfyPage.setup({ mockReleases: false })
+
+ // Open help center
+ const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
+ await helpCenterButton.waitFor({ state: 'visible' })
+ await helpCenterButton.click()
+
+ // Verify help center menu appears
+ const helpMenu = comfyPage.page.locator('.help-center-menu')
+ await expect(helpMenu).toBeVisible()
+
+ // Verify "What's New?" section is hidden
+ const whatsNewSection = comfyPage.page.locator('.whats-new-section')
+ await expect(whatsNewSection).not.toBeVisible()
+
+ // Should not show any popups or toasts
+ await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
+ await expect(
+ comfyPage.page.locator('.release-notification-toast')
+ ).not.toBeVisible()
+ })
+
+ test('should not make API calls when notifications are disabled', async ({
+ comfyPage
+ }) => {
+ // Disable version update notifications
+ await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
+
+ // Track API calls
+ let apiCallCount = 0
+ await comfyPage.page.route('**/releases**', async (route) => {
+ const url = route.request().url()
+ if (
+ url.includes('api.comfy.org') ||
+ url.includes('stagingapi.comfy.org')
+ ) {
+ apiCallCount++
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([])
+ })
+ } else {
+ await route.continue()
+ }
+ })
+
+ await comfyPage.setup({ mockReleases: false })
+
+ // Wait a bit to ensure any potential API calls would have been made
+ await comfyPage.page.waitForTimeout(1000)
+
+ // Verify no API calls were made
+ expect(apiCallCount).toBe(0)
+ })
+
+ test('should show "What\'s New" section when notifications are enabled', async ({
+ comfyPage
+ }) => {
+ // Enable version update notifications (default behavior)
+ await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true)
+
+ // Mock release API with test data
+ await comfyPage.page.route('**/releases**', async (route) => {
+ const url = route.request().url()
+ if (
+ url.includes('api.comfy.org') ||
+ url.includes('stagingapi.comfy.org')
+ ) {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([
+ {
+ id: 1,
+ project: 'comfyui',
+ version: 'v0.3.44',
+ attention: 'medium',
+ content: '## New Features\n\n- Added awesome feature',
+ published_at: new Date().toISOString()
+ }
+ ])
+ })
+ } else {
+ await route.continue()
+ }
+ })
+
+ await comfyPage.setup({ mockReleases: false })
+
+ // Open help center
+ const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
+ await helpCenterButton.waitFor({ state: 'visible' })
+ await helpCenterButton.click()
+
+ // Verify help center menu appears
+ const helpMenu = comfyPage.page.locator('.help-center-menu')
+ await expect(helpMenu).toBeVisible()
+
+ // Verify "What's New?" section is visible
+ const whatsNewSection = comfyPage.page.locator('.whats-new-section')
+ await expect(whatsNewSection).toBeVisible()
+
+ // Should show the release
+ await expect(
+ whatsNewSection.locator('text=Comfy v0.3.44 Release')
+ ).toBeVisible()
+ })
+
+ test('should toggle "What\'s New" section when setting changes', async ({
+ comfyPage
+ }) => {
+ // Mock release API with test data
+ await comfyPage.page.route('**/releases**', async (route) => {
+ const url = route.request().url()
+ if (
+ url.includes('api.comfy.org') ||
+ url.includes('stagingapi.comfy.org')
+ ) {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([
+ {
+ id: 1,
+ project: 'comfyui',
+ version: 'v0.3.44',
+ attention: 'low',
+ content: '## Bug Fixes\n\n- Fixed minor issue',
+ published_at: new Date().toISOString()
+ }
+ ])
+ })
+ } else {
+ await route.continue()
+ }
+ })
+
+ // Start with notifications enabled
+ await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true)
+ await comfyPage.setup({ mockReleases: false })
+
+ // Open help center
+ const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
+ await helpCenterButton.waitFor({ state: 'visible' })
+ await helpCenterButton.click()
+
+ // Verify "What's New?" section is visible
+ const whatsNewSection = comfyPage.page.locator('.whats-new-section')
+ await expect(whatsNewSection).toBeVisible()
+
+ // Close help center
+ await comfyPage.page.click('.help-center-backdrop')
+
+ // Disable notifications
+ await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
+
+ // Reopen help center
+ await helpCenterButton.click()
+
+ // Verify "What's New?" section is now hidden
+ await expect(whatsNewSection).not.toBeVisible()
+ })
+
+ test('should handle edge case with empty releases and disabled notifications', async ({
+ comfyPage
+ }) => {
+ // Disable notifications
+ await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
+
+ // Mock empty releases
+ await comfyPage.page.route('**/releases**', async (route) => {
+ const url = route.request().url()
+ if (
+ url.includes('api.comfy.org') ||
+ url.includes('stagingapi.comfy.org')
+ ) {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([])
+ })
+ } else {
+ await route.continue()
+ }
+ })
+
+ await comfyPage.setup({ mockReleases: false })
+
+ // Open help center
+ const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
+ await helpCenterButton.waitFor({ state: 'visible' })
+ await helpCenterButton.click()
+
+ // Verify help center still works
+ const helpMenu = comfyPage.page.locator('.help-center-menu')
+ await expect(helpMenu).toBeVisible()
+
+ // Section should be hidden regardless of empty releases
+ const whatsNewSection = comfyPage.page.locator('.whats-new-section')
+ await expect(whatsNewSection).not.toBeVisible()
+ })
})
diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts
new file mode 100644
index 000000000..69a40ced9
--- /dev/null
+++ b/browser_tests/tests/useSettingSearch.spec.ts
@@ -0,0 +1,289 @@
+import { expect } from '@playwright/test'
+
+import { comfyPageFixture as test } from '../fixtures/ComfyPage'
+
+test.describe('Settings Search functionality', () => {
+ test.beforeEach(async ({ comfyPage }) => {
+ // Register test settings to verify hidden/deprecated filtering
+ await comfyPage.page.evaluate(() => {
+ window['app'].registerExtension({
+ name: 'TestSettingsExtension',
+ settings: [
+ {
+ id: 'TestHiddenSetting',
+ name: 'Test Hidden Setting',
+ type: 'hidden',
+ defaultValue: 'hidden_value',
+ category: ['Test', 'Hidden']
+ },
+ {
+ id: 'TestDeprecatedSetting',
+ name: 'Test Deprecated Setting',
+ type: 'text',
+ defaultValue: 'deprecated_value',
+ deprecated: true,
+ category: ['Test', 'Deprecated']
+ },
+ {
+ id: 'TestVisibleSetting',
+ name: 'Test Visible Setting',
+ type: 'text',
+ defaultValue: 'visible_value',
+ category: ['Test', 'Visible']
+ }
+ ]
+ })
+ })
+ })
+
+ test('can open settings dialog and use search box', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Find the search box
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await expect(searchBox).toBeVisible()
+
+ // Verify search box has the correct placeholder
+ await expect(searchBox).toHaveAttribute(
+ 'placeholder',
+ expect.stringContaining('Search')
+ )
+ })
+
+ test('search box is functional and accepts input', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Find and interact with the search box
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await searchBox.fill('Comfy')
+
+ // Verify the input was accepted
+ await expect(searchBox).toHaveValue('Comfy')
+ })
+
+ test('search box clears properly', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Find and interact with the search box
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await searchBox.fill('test')
+ await expect(searchBox).toHaveValue('test')
+
+ // Clear the search box
+ await searchBox.clear()
+ await expect(searchBox).toHaveValue('')
+ })
+
+ test('settings categories are visible in sidebar', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Check that the sidebar has categories
+ const categories = comfyPage.page.locator(
+ '.settings-sidebar .p-listbox-option'
+ )
+ expect(await categories.count()).toBeGreaterThan(0)
+
+ // Check that at least one category is visible
+ await expect(categories.first()).toBeVisible()
+ })
+
+ test('can select different categories in sidebar', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Get categories and click on different ones
+ const categories = comfyPage.page.locator(
+ '.settings-sidebar .p-listbox-option'
+ )
+ const categoryCount = await categories.count()
+
+ if (categoryCount > 1) {
+ // Click on the second category
+ await categories.nth(1).click()
+
+ // Verify the category is selected
+ await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/)
+ }
+ })
+
+ test('settings content area is visible', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Check that the content area is visible
+ const contentArea = comfyPage.page.locator('.settings-content')
+ await expect(contentArea).toBeVisible()
+
+ // Check that tab panels are visible
+ const tabPanels = comfyPage.page.locator('.settings-tab-panels')
+ await expect(tabPanels).toBeVisible()
+ })
+
+ test('search functionality affects UI state', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Find the search box
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+
+ // Type in search box
+ await searchBox.fill('graph')
+ await comfyPage.page.waitForTimeout(200) // Wait for debounce
+
+ // Verify that the search input is handled
+ await expect(searchBox).toHaveValue('graph')
+ })
+
+ test('settings dialog can be closed', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Close with escape key
+ await comfyPage.page.keyboard.press('Escape')
+
+ // Verify dialog is closed
+ await expect(settingsDialog).not.toBeVisible()
+ })
+
+ test('search box has proper debouncing behavior', async ({ comfyPage }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Type rapidly in search box
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await searchBox.fill('a')
+ await searchBox.fill('ab')
+ await searchBox.fill('abc')
+ await searchBox.fill('abcd')
+
+ // Wait for debounce
+ await comfyPage.page.waitForTimeout(200)
+
+ // Verify final value
+ await expect(searchBox).toHaveValue('abcd')
+ })
+
+ test('search excludes hidden settings from results', async ({
+ comfyPage
+ }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Search for our test settings
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await searchBox.fill('Test')
+ await comfyPage.page.waitForTimeout(300) // Wait for debounce
+
+ // Get all settings content
+ const settingsContent = comfyPage.page.locator('.settings-tab-panels')
+
+ // Should show visible setting but not hidden setting
+ await expect(settingsContent).toContainText('Test Visible Setting')
+ await expect(settingsContent).not.toContainText('Test Hidden Setting')
+ })
+
+ test('search excludes deprecated settings from results', async ({
+ comfyPage
+ }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Search for our test settings
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await searchBox.fill('Test')
+ await comfyPage.page.waitForTimeout(300) // Wait for debounce
+
+ // Get all settings content
+ const settingsContent = comfyPage.page.locator('.settings-tab-panels')
+
+ // Should show visible setting but not deprecated setting
+ await expect(settingsContent).toContainText('Test Visible Setting')
+ await expect(settingsContent).not.toContainText('Test Deprecated Setting')
+ })
+
+ test('search shows visible settings but excludes hidden and deprecated', async ({
+ comfyPage
+ }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ // Search for our test settings
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ await searchBox.fill('Test')
+ await comfyPage.page.waitForTimeout(300) // Wait for debounce
+
+ // Get all settings content
+ const settingsContent = comfyPage.page.locator('.settings-tab-panels')
+
+ // Should only show the visible setting
+ await expect(settingsContent).toContainText('Test Visible Setting')
+
+ // Should not show hidden or deprecated settings
+ await expect(settingsContent).not.toContainText('Test Hidden Setting')
+ await expect(settingsContent).not.toContainText('Test Deprecated Setting')
+ })
+
+ test('search by setting name excludes hidden and deprecated', async ({
+ comfyPage
+ }) => {
+ // Open settings dialog
+ await comfyPage.page.keyboard.press('Control+,')
+ const settingsDialog = comfyPage.page.locator('.settings-container')
+ await expect(settingsDialog).toBeVisible()
+
+ const searchBox = comfyPage.page.locator('.settings-search-box input')
+ const settingsContent = comfyPage.page.locator('.settings-tab-panels')
+
+ // Search specifically for hidden setting by name
+ await searchBox.clear()
+ await searchBox.fill('Hidden')
+ await comfyPage.page.waitForTimeout(300)
+
+ // Should not show the hidden setting even when searching by name
+ await expect(settingsContent).not.toContainText('Test Hidden Setting')
+
+ // Search specifically for deprecated setting by name
+ await searchBox.clear()
+ await searchBox.fill('Deprecated')
+ await comfyPage.page.waitForTimeout(300)
+
+ // Should not show the deprecated setting even when searching by name
+ await expect(settingsContent).not.toContainText('Test Deprecated Setting')
+
+ // Search for visible setting by name - should work
+ await searchBox.clear()
+ await searchBox.fill('Visible')
+ await comfyPage.page.waitForTimeout(300)
+
+ // Should show the visible setting
+ await expect(settingsContent).toContainText('Test Visible Setting')
+ })
+})
diff --git a/build/plugins/addElementVnodeExportPlugin.ts b/build/plugins/addElementVnodeExportPlugin.ts
deleted file mode 100644
index 1266d13e2..000000000
--- a/build/plugins/addElementVnodeExportPlugin.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Plugin } from 'vite'
-
-/**
- * Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode.
- *
- * This plugin addresses compatibility issues where some components or libraries
- * might be using the older createElementVNode function name instead of createBaseVNode.
- * It modifies the Vue vendor chunk during build to add the alias export.
- *
- * @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports
- */
-export function addElementVnodeExportPlugin(): Plugin {
- return {
- name: 'add-element-vnode-export-plugin',
-
- renderChunk(code, chunk, _options) {
- if (chunk.name.startsWith('vendor-vue')) {
- const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/
- const match = code.match(exportRegex)
-
- if (match) {
- const existingExports = match[2].trim()
- const exportsArray = existingExports
- .split(',')
- .map((e) => e.trim())
- .filter(Boolean)
-
- const hasCreateBaseVNode = exportsArray.some((e) =>
- e.startsWith('createBaseVNode')
- )
- const hasCreateElementVNode = exportsArray.some((e) =>
- e.includes('createElementVNode')
- )
-
- if (hasCreateBaseVNode && !hasCreateElementVNode) {
- const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}`
- const newCode = code.replace(exportRegex, newExportStatement)
-
- console.log(
- `[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.`
- )
-
- return { code: newCode, map: null }
- } else if (!hasCreateBaseVNode) {
- console.warn(
- `[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.`
- )
- }
- } else {
- console.warn(
- `[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.`
- )
- }
- }
-
- return null
- }
- }
-}
diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts
index c6661a811..80ccb6c9f 100644
--- a/build/plugins/generateImportMapPlugin.ts
+++ b/build/plugins/generateImportMapPlugin.ts
@@ -1,9 +1,24 @@
-import type { OutputOptions } from 'rollup'
-import { HtmlTagDescriptor, Plugin } from 'vite'
+import glob from 'fast-glob'
+import fs from 'fs-extra'
+import { dirname, join } from 'node:path'
+import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
-interface VendorLibrary {
+interface ImportMapSource {
name: string
- pattern: RegExp
+ pattern: string | RegExp
+ entry: string
+ recursiveDependence?: boolean
+ override?: Record>
+}
+
+const parseDeps = (root: string, pkg: string) => {
+ const pkgPath = join(root, 'node_modules', pkg, 'package.json')
+ if (fs.existsSync(pkgPath)) {
+ const content = fs.readFileSync(pkgPath, 'utf-8')
+ const pkg = JSON.parse(content)
+ return Object.keys(pkg.dependencies || {})
+ }
+ return []
}
/**
@@ -23,53 +38,89 @@ interface VendorLibrary {
* @returns {Plugin} A Vite plugin that generates and injects an import map
*/
export function generateImportMapPlugin(
- vendorLibraries: VendorLibrary[]
+ importMapSources: ImportMapSource[]
): Plugin {
const importMapEntries: Record = {}
+ const resolvedImportMapSources: Map = new Map()
+ const assetDir = 'assets/lib'
+ let root: string
return {
name: 'generate-import-map-plugin',
// Configure manual chunks during the build process
configResolved(config) {
+ root = config.root
+
if (config.build) {
// Ensure rollupOptions exists
if (!config.build.rollupOptions) {
config.build.rollupOptions = {}
}
- const outputOptions: OutputOptions = {
- manualChunks: (id: string) => {
- for (const lib of vendorLibraries) {
- if (lib.pattern.test(id)) {
- return `vendor-${lib.name}`
- }
+ for (const source of importMapSources) {
+ resolvedImportMapSources.set(source.name, source)
+ if (source.recursiveDependence) {
+ const deps = parseDeps(root, source.name)
+
+ while (deps.length) {
+ const dep = deps.shift()!
+ const depSource = Object.assign({}, source, {
+ name: dep,
+ pattern: dep,
+ ...source.override?.[dep]
+ })
+ resolvedImportMapSources.set(depSource.name, depSource)
+
+ const _deps = parseDeps(root, depSource.name)
+ deps.unshift(..._deps)
}
- return null
- },
- // Disable minification of internal exports to preserve function names
- minifyInternalExports: false
+ }
}
- config.build.rollupOptions.output = outputOptions
+
+ const external: (string | RegExp)[] = []
+ for (const [, source] of resolvedImportMapSources) {
+ external.push(source.pattern)
+ }
+ config.build.rollupOptions.external = external
}
},
- generateBundle(_options, bundle) {
- for (const fileName in bundle) {
- const chunk = bundle[fileName]
- if (chunk.type === 'chunk' && !chunk.isEntry) {
- // Find matching vendor library by chunk name
- const vendorLib = vendorLibraries.find(
- (lib) => chunk.name === `vendor-${lib.name}`
- )
+ generateBundle(_options) {
+ for (const [, source] of resolvedImportMapSources) {
+ if (source.entry) {
+ const moduleFile = join(source.name, source.entry)
+ const sourceFile = join(root, 'node_modules', moduleFile)
+ const targetFile = join(root, 'dist', assetDir, moduleFile)
- if (vendorLib) {
- const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}`
- importMapEntries[vendorLib.name] = relativePath
+ importMapEntries[source.name] =
+ './' + normalizePath(join(assetDir, moduleFile))
- console.log(
- `[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'`
- )
+ const targetDir = dirname(targetFile)
+ if (!fs.existsSync(targetDir)) {
+ fs.mkdirSync(targetDir, { recursive: true })
+ }
+ fs.copyFileSync(sourceFile, targetFile)
+ }
+
+ if (source.recursiveDependence) {
+ const files = glob.sync(['**/*.{js,mjs}'], {
+ cwd: join(root, 'node_modules', source.name)
+ })
+
+ for (const file of files) {
+ const moduleFile = join(source.name, file)
+ const sourceFile = join(root, 'node_modules', moduleFile)
+ const targetFile = join(root, 'dist', assetDir, moduleFile)
+
+ importMapEntries[normalizePath(join(source.name, dirname(file)))] =
+ './' + normalizePath(join(assetDir, moduleFile))
+
+ const targetDir = dirname(targetFile)
+ if (!fs.existsSync(targetDir)) {
+ fs.mkdirSync(targetDir, { recursive: true })
+ }
+ fs.copyFileSync(sourceFile, targetFile)
}
}
}
diff --git a/build/plugins/index.ts b/build/plugins/index.ts
index c67473f7a..f8c2d695c 100644
--- a/build/plugins/index.ts
+++ b/build/plugins/index.ts
@@ -1,3 +1,2 @@
-export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin'
export { comfyAPIPlugin } from './comfyAPIPlugin'
export { generateImportMapPlugin } from './generateImportMapPlugin'
diff --git a/package-lock.json b/package-lock.json
index 6cae51239..b3443857d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
- "version": "1.24.0-0",
+ "version": "1.24.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
- "version": "1.24.0-0",
+ "version": "1.24.0",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
- "@comfyorg/litegraph": "^0.16.3",
+ "@comfyorg/litegraph": "^0.16.6",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -949,9 +949,9 @@
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
- "version": "0.16.3",
- "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.3.tgz",
- "integrity": "sha512-dst29g8+aZW8sWTYxj3LK1W4lX07elBPWFB1L4HLTkYgkzQoyBkHR1O2lSvAn+7bKagi0Q5PjIcZnWG+JAi0lg==",
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.6.tgz",
+ "integrity": "sha512-pRmJYZ39rIpGIaJAaOLicRFe3KyeNTXNAAB0+Thz8cPGpu2dBv8W6PlOu94VYNRc+pBhEwV+jJVlXb5YyAvBXQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
diff --git a/package.json b/package.json
index ba674deb4..3993a8697 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
- "version": "1.24.0-0",
+ "version": "1.24.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -77,7 +77,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
- "@comfyorg/litegraph": "^0.16.3",
+ "@comfyorg/litegraph": "^0.16.6",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
diff --git a/scripts/check-unused-i18n-keys.ts b/scripts/check-unused-i18n-keys.ts
new file mode 100755
index 000000000..f459b8c23
--- /dev/null
+++ b/scripts/check-unused-i18n-keys.ts
@@ -0,0 +1,192 @@
+#!/usr/bin/env tsx
+import { execSync } from 'child_process'
+import * as fs from 'fs'
+import { globSync } from 'glob'
+
+interface LocaleData {
+ [key: string]: any
+}
+
+// Configuration
+const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*']
+const IGNORE_PATTERNS = [
+ // Keys that might be dynamically constructed
+ /^commands\./, // Command definitions are loaded dynamically
+ /^settings\..*\.options\./, // Setting options are rendered dynamically
+ /^nodeDefs\./, // Node definitions are loaded from backend
+ /^templateWorkflows\./, // Template workflows are loaded dynamically
+ /^dataTypes\./, // Data types might be referenced dynamically
+ /^contextMenu\./, // Context menu items might be dynamic
+ /^color\./, // Color names might be used dynamically
+ // Auto-generated categories from collect-i18n-general.ts
+ /^menuLabels\./, // Menu labels generated from command labels
+ /^settingsCategories\./, // Settings categories generated from setting definitions
+ /^serverConfigItems\./, // Server config items generated from SERVER_CONFIG_ITEMS
+ /^serverConfigCategories\./, // Server config categories generated from config categories
+ /^nodeCategories\./, // Node categories generated from node definitions
+ // Setting option values that are dynamically generated
+ /\.options\./ // All setting options are rendered dynamically
+]
+
+// Get list of staged locale files
+function getStagedLocaleFiles(): string[] {
+ try {
+ const output = execSync('git diff --cached --name-only --diff-filter=AM', {
+ encoding: 'utf-8'
+ })
+ return output
+ .split('\n')
+ .filter(
+ (file) => file.startsWith('src/locales/') && file.endsWith('.json')
+ )
+ } catch {
+ return []
+ }
+}
+
+// Extract all keys from a nested object
+function extractKeys(obj: any, prefix = ''): string[] {
+ const keys: string[] = []
+
+ for (const [key, value] of Object.entries(obj)) {
+ const fullKey = prefix ? `${prefix}.${key}` : key
+
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ keys.push(...extractKeys(value, fullKey))
+ } else {
+ keys.push(fullKey)
+ }
+ }
+
+ return keys
+}
+
+// Get new keys added in staged files
+function getNewKeysFromStagedFiles(stagedFiles: string[]): Set {
+ const newKeys = new Set()
+
+ for (const file of stagedFiles) {
+ try {
+ // Get the staged content
+ const stagedContent = execSync(`git show :${file}`, { encoding: 'utf-8' })
+ const stagedData: LocaleData = JSON.parse(stagedContent)
+ const stagedKeys = new Set(extractKeys(stagedData))
+
+ // Get the current HEAD content (if file exists)
+ let headKeys = new Set()
+ try {
+ const headContent = execSync(`git show HEAD:${file}`, {
+ encoding: 'utf-8'
+ })
+ const headData: LocaleData = JSON.parse(headContent)
+ headKeys = new Set(extractKeys(headData))
+ } catch {
+ // File is new, all keys are new
+ }
+
+ // Find keys that are in staged but not in HEAD
+ stagedKeys.forEach((key) => {
+ if (!headKeys.has(key)) {
+ newKeys.add(key)
+ }
+ })
+ } catch (error) {
+ console.error(`Error processing ${file}:`, error)
+ }
+ }
+
+ return newKeys
+}
+
+// Check if a key should be ignored
+function shouldIgnoreKey(key: string): boolean {
+ return IGNORE_PATTERNS.some((pattern) => pattern.test(key))
+}
+
+// Search for key usage in source files
+function isKeyUsed(key: string, sourceFiles: string[]): boolean {
+ // Escape special regex characters
+ const escapeRegex = (str: string) =>
+ str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ const escapedKey = escapeRegex(key)
+ const lastPart = key.split('.').pop()
+ const escapedLastPart = lastPart ? escapeRegex(lastPart) : ''
+
+ // Common patterns for i18n key usage
+ const patterns = [
+ // Direct usage: $t('key'), t('key'), i18n.t('key')
+ new RegExp(`[t$]\\s*\\(\\s*['"\`]${escapedKey}['"\`]`, 'g'),
+ // With namespace: $t('g.key'), t('namespace.key')
+ new RegExp(`[t$]\\s*\\(\\s*['"\`][^'"]+\\.${escapedLastPart}['"\`]`, 'g'),
+ // Dynamic keys might reference parts of the key
+ new RegExp(`['"\`]${escapedKey}['"\`]`, 'g')
+ ]
+
+ for (const file of sourceFiles) {
+ const content = fs.readFileSync(file, 'utf-8')
+
+ for (const pattern of patterns) {
+ if (pattern.test(content)) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// Main function
+async function checkNewUnusedKeys() {
+ const stagedLocaleFiles = getStagedLocaleFiles()
+
+ if (stagedLocaleFiles.length === 0) {
+ // No locale files staged, nothing to check
+ process.exit(0)
+ }
+
+ // Get all new keys from staged files
+ const newKeys = getNewKeysFromStagedFiles(stagedLocaleFiles)
+
+ if (newKeys.size === 0) {
+ // Silent success - no output needed
+ process.exit(0)
+ }
+
+ // Get all source files
+ const sourceFiles = globSync(SOURCE_PATTERNS)
+
+ // Check each new key
+ const unusedNewKeys: string[] = []
+
+ newKeys.forEach((key) => {
+ if (!shouldIgnoreKey(key) && !isKeyUsed(key, sourceFiles)) {
+ unusedNewKeys.push(key)
+ }
+ })
+
+ // Report results
+ if (unusedNewKeys.length > 0) {
+ console.log('\n⚠️ Warning: Found unused NEW i18n keys:\n')
+
+ for (const key of unusedNewKeys.sort()) {
+ console.log(` - ${key}`)
+ }
+
+ console.log(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
+ console.log(
+ '\nThese keys were added but are not used anywhere in the codebase.'
+ )
+ console.log('Consider using them or removing them in a future update.')
+
+ // Changed from process.exit(1) to process.exit(0) for warning only
+ process.exit(0)
+ } else {
+ // Silent success - no output needed
+ }
+}
+
+// Run the check
+checkNewUnusedKeys().catch((err) => {
+ console.error('Error checking unused keys:', err)
+ process.exit(1)
+})
diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue
index 4189c02a2..c6aa2aea7 100644
--- a/src/components/graph/GraphCanvas.vue
+++ b/src/components/graph/GraphCanvas.vue
@@ -77,6 +77,7 @@ import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
+import { newUserService } from '@/services/newUserService'
import { useWorkflowService } from '@/services/workflowService'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
@@ -305,6 +306,9 @@ onMounted(async () => {
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})
+
+ await newUserService().initializeIfNewUser(settingStore)
+
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas
diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue
index a7e180529..45a6da119 100644
--- a/src/components/helpcenter/HelpCenterMenuContent.vue
+++ b/src/components/helpcenter/HelpCenterMenuContent.vue
@@ -4,6 +4,7 @@