Merge branch 'main' into sno-license-check

This commit is contained in:
sno
2025-12-03 19:35:47 +09:00
committed by GitHub
348 changed files with 16931 additions and 9579 deletions

View File

@@ -1,5 +1,5 @@
# Description: When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Electron API Types' name: 'Api: Update Electron API Types'
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
on: on:
workflow_dispatch: workflow_dispatch:

View File

@@ -1,5 +1,5 @@
# Description: When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Manager API Types' name: 'Api: Update Manager API Types'
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
on: on:
# Manual trigger # Manual trigger

View File

@@ -1,5 +1,5 @@
# Description: When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Registry API Types' name: 'Api: Update Registry API Types'
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
on: on:
# Manual trigger # Manual trigger

View File

@@ -1,5 +1,5 @@
# Description: Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq
name: "CI: JSON Validation" name: "CI: JSON Validation"
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
on: on:
push: push:

View File

@@ -1,5 +1,5 @@
# Description: Linting and code formatting validation for pull requests
name: "CI: Lint Format" name: "CI: Lint Format"
description: "Linting and code formatting validation for pull requests"
on: on:
pull_request: pull_request:

View File

@@ -1,5 +1,5 @@
# Description: Validates Python code in tools/devtools directory
name: "CI: Python Validation" name: "CI: Python Validation"
description: "Validates Python code in tools/devtools directory"
on: on:
pull_request: pull_request:

View File

@@ -1,5 +1,5 @@
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
name: "CI: Tests E2E (Deploy for Forks)" name: "CI: Tests E2E (Deploy for Forks)"
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
on: on:
workflow_run: workflow_run:

View File

@@ -1,5 +1,5 @@
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
name: "CI: Tests E2E" name: "CI: Tests E2E"
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
on: on:
push: push:

View File

@@ -1,5 +1,5 @@
# Description: Deploys Storybook previews from forked PRs (forks can't access deployment secrets)
name: "CI: Tests Storybook (Deploy for Forks)" name: "CI: Tests Storybook (Deploy for Forks)"
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
on: on:
workflow_run: workflow_run:

View File

@@ -1,10 +1,9 @@
# Description: Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
name: "CI: Tests Storybook" name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
on: on:
workflow_dispatch: # Allow manual triggering workflow_dispatch: # Allow manual triggering
pull_request: pull_request:
branches: [main]
jobs: jobs:
# Post starting comment for non-forked PRs # Post starting comment for non-forked PRs

View File

@@ -1,5 +1,5 @@
# Description: Unit and component testing with Vitest
name: "CI: Tests Unit" name: "CI: Tests Unit"
description: "Unit and component testing with Vitest"
on: on:
push: push:

View File

@@ -1,5 +1,5 @@
# Description: Validates YAML syntax and style using yamllint with relaxed rules
name: "CI: YAML Validation" name: "CI: YAML Validation"
description: "Validates YAML syntax and style using yamllint with relaxed rules"
on: on:
push: push:

View File

@@ -0,0 +1,69 @@
---
name: Cloud Backport Tag
on:
pull_request:
types: ['closed']
branches: [cloud/*]
jobs:
create-tag:
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'backport')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
- name: Create tag for cloud backport
id: tag
run: |
set -euo pipefail
BRANCH="${{ github.event.pull_request.base.ref }}"
if [[ ! "$BRANCH" =~ ^cloud/([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::Base branch '$BRANCH' is not a cloud/x.y branch"
exit 1
fi
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
VERSION=$(node -p "require('./package.json').version")
if [[ "$VERSION" =~ ^${MAJOR}\.${MINOR}\.([0-9]+)(-.+)?$ ]]; then
PATCH="${BASH_REMATCH[1]}"
SUFFIX="${BASH_REMATCH[2]:-}"
else
echo "::error::Version '${VERSION}' does not match cloud branch '${BRANCH}'"
exit 1
fi
TAG="cloud/v${VERSION}"
if git ls-remote --tags origin "${TAG}" | grep -Fq "refs/tags/${TAG}"; then
echo "::notice::Tag ${TAG} already exists; skipping"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
exit 0
fi
git tag "${TAG}" "${{ github.event.pull_request.merge_commit_sha }}"
git push origin "${TAG}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
{
echo "Created tag: ${TAG}"
echo "Branch: ${BRANCH}"
echo "Version: ${VERSION}"
echo "Commit: ${{ github.event.pull_request.merge_commit_sha }}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,5 +1,5 @@
# Description: Generates and updates translations for core ComfyUI components using OpenAI
name: "i18n: Update Core" name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
on: on:
# Manual dispatch for urgent translation updates # Manual dispatch for urgent translation updates

View File

@@ -78,8 +78,7 @@ jobs:
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name') LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else else
LABELS='${{ toJSON(github.event.pull_request.labels) }}' LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH")
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi fi
add_target() { add_target() {
@@ -237,8 +236,8 @@ jobs:
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else else
PR_TITLE="${{ github.event.pull_request.title }}" PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
fi fi
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
@@ -327,8 +326,8 @@ jobs:
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
else else
PR_TITLE="${{ github.event.pull_request.title }}" PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
PR_AUTHOR="${{ github.event.pull_request.user.login }}" PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
fi fi
for backport in ${{ steps.backport.outputs.success }}; do for backport in ${{ steps.backport.outputs.success }}; do
@@ -365,9 +364,9 @@ jobs:
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else else
PR_NUMBER="${{ github.event.pull_request.number }}" PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
PR_AUTHOR="${{ github.event.pull_request.user.login }}" PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
fi fi
for failure in ${{ steps.backport.outputs.failed }}; do for failure in ${{ steps.backport.outputs.failed }}; do

View File

@@ -1,5 +1,5 @@
# Description: AI-powered code review triggered by adding the 'claude-review' label to a PR
name: "PR: Claude Review" name: "PR: Claude Review"
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
permissions: permissions:
contents: read contents: read

View File

@@ -148,10 +148,10 @@ jobs:
done done
{ {
echo "results<<'EOF'" echo "results<<EOF"
cat "$RESULTS_FILE" cat "$RESULTS_FILE"
echo "EOF" echo "EOF"
} >> $GITHUB_OUTPUT } >> "$GITHUB_OUTPUT"
- name: Ensure release labels - name: Ensure release labels
if: steps.check_version.outputs.is_minor_bump == 'true' if: steps.check_version.outputs.is_minor_bump == 'true'

View File

@@ -1,5 +1,5 @@
# Description: Manual workflow to increment package version with semantic versioning support
name: "Release: Version Bump" name: "Release: Version Bump"
description: "Manual workflow to increment package version with semantic versioning support"
on: on:
workflow_dispatch: workflow_dispatch:

View File

@@ -0,0 +1,292 @@
# Automated weekly workflow to bump ComfyUI frontend RC releases
name: "Release: Weekly ComfyUI"
on:
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
schedule:
- cron: '0 20 * * 1'
# Allow manual triggering
workflow_dispatch:
inputs:
comfyui_fork:
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
required: false
default: 'Comfy-Org/ComfyUI'
type: string
jobs:
resolve-version:
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.resolve.outputs.current_version }}
target_version: ${{ steps.resolve.outputs.target_version }}
target_minor: ${{ steps.resolve.outputs.target_minor }}
target_branch: ${{ steps.resolve.outputs.target_branch }}
needs_release: ${{ steps.resolve.outputs.needs_release }}
diff_url: ${{ steps.resolve.outputs.diff_url }}
latest_patch_tag: ${{ steps.resolve.outputs.latest_patch_tag }}
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
with:
fetch-depth: 0
path: frontend
- name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5
with:
repository: comfyanonymous/ComfyUI
sparse-checkout: |
requirements.txt
path: comfyui
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile
- name: Resolve release information
id: resolve
working-directory: frontend
run: |
set -euo pipefail
# Run the resolver script
if ! RESULT=$(pnpm exec tsx scripts/cicd/resolve-comfyui-release.ts ../comfyui .); then
echo "Failed to resolve release information"
exit 1
fi
echo "Resolver output:"
echo "$RESULT"
# Validate JSON output
if ! echo "$RESULT" | jq empty 2>/dev/null; then
echo "Invalid JSON output from resolver"
exit 1
fi
# Parse JSON output and set outputs
echo "current_version=$(echo "$RESULT" | jq -r '.current_version')" >> $GITHUB_OUTPUT
echo "target_version=$(echo "$RESULT" | jq -r '.target_version')" >> $GITHUB_OUTPUT
echo "target_minor=$(echo "$RESULT" | jq -r '.target_minor')" >> $GITHUB_OUTPUT
echo "target_branch=$(echo "$RESULT" | jq -r '.target_branch')" >> $GITHUB_OUTPUT
echo "needs_release=$(echo "$RESULT" | jq -r '.needs_release')" >> $GITHUB_OUTPUT
echo "diff_url=$(echo "$RESULT" | jq -r '.diff_url')" >> $GITHUB_OUTPUT
echo "latest_patch_tag=$(echo "$RESULT" | jq -r '.latest_patch_tag')" >> $GITHUB_OUTPUT
- name: Summary
run: |
echo "## Release Information" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Current version: ${{ steps.resolve.outputs.current_version }}" >> $GITHUB_STEP_SUMMARY
echo "- Target version: ${{ steps.resolve.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
echo "- Target branch: ${{ steps.resolve.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
echo "- Needs release: ${{ steps.resolve.outputs.needs_release }}" >> $GITHUB_STEP_SUMMARY
echo "- Diff: [${{ steps.resolve.outputs.current_version }}...${{ steps.resolve.outputs.target_version }}](${{ steps.resolve.outputs.diff_url }})" >> $GITHUB_STEP_SUMMARY
trigger-release-if-needed:
needs: resolve-version
if: needs.resolve-version.outputs.needs_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger release workflow
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
run: |
set -euo pipefail
echo "Triggering release workflow for branch ${{ needs.resolve-version.outputs.target_branch }}"
# Trigger the release-version-bump workflow
if ! gh workflow run release-version-bump.yaml \
--repo Comfy-Org/ComfyUI_frontend \
--ref main \
--field version_type=patch \
--field branch=${{ needs.resolve-version.outputs.target_branch }}; then
echo "Failed to trigger release workflow"
exit 1
fi
echo "Release workflow triggered successfully for ${{ needs.resolve-version.outputs.target_branch }}"
- name: Summary
run: |
echo "## Release Workflow Triggered" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Branch: ${{ needs.resolve-version.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
echo "- Target version: ${{ needs.resolve-version.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
create-comfyui-pr:
needs: [resolve-version, trigger-release-if-needed]
if: always() && needs.resolve-version.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout ComfyUI fork
uses: actions/checkout@v5
with:
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
token: ${{ secrets.PR_GH_TOKEN }}
path: comfyui
- name: Sync with upstream
working-directory: comfyui
run: |
set -euo pipefail
# Fetch latest upstream to base our branch on fresh code
# Note: This only affects the local checkout, NOT the fork's master branch
# We only push the automation branch, leaving the fork's master untouched
echo "Fetching upstream master..."
if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then
echo "Failed to fetch upstream master"
exit 1
fi
echo "Checking out upstream master..."
if ! git checkout FETCH_HEAD; then
echo "Failed to checkout FETCH_HEAD"
exit 1
fi
echo "Successfully synced with upstream master"
- name: Update requirements.txt
working-directory: comfyui
run: |
set -euo pipefail
TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}"
echo "Updating comfyui-frontend-package to ${TARGET_VERSION}"
# Update the comfyui-frontend-package version (POSIX-compatible)
sed -i.bak "s/comfyui-frontend-package==[0-9.][0-9.]*/comfyui-frontend-package==${TARGET_VERSION}/" requirements.txt
rm requirements.txt.bak
# Verify the change was made
if ! grep -q "comfyui-frontend-package==${TARGET_VERSION}" requirements.txt; then
echo "Failed to update requirements.txt"
exit 1
fi
echo "Updated requirements.txt:"
grep comfyui-frontend-package requirements.txt
- name: Build PR description
id: pr-body
run: |
BODY=$(cat <<'EOF'
Bumps frontend to ${{ needs.resolve-version.outputs.target_version }}
Test quickly:
```bash
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${{ needs.resolve-version.outputs.target_version }}
```
- Diff: [v${{ needs.resolve-version.outputs.current_version }}...v${{ needs.resolve-version.outputs.target_version }}](${{ needs.resolve-version.outputs.diff_url }})
- PyPI: https://pypi.org/project/comfyui-frontend-package/${{ needs.resolve-version.outputs.target_version }}/
- npm: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${{ needs.resolve-version.outputs.target_version }}
EOF
)
# Add release PR note if release was triggered
if [ "${{ needs.resolve-version.outputs.needs_release }}" = "true" ]; then
RELEASE_NOTE="⚠️ **Release PR must be merged first** - check [release workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)"
BODY=$''"${RELEASE_NOTE}"$'\n\n'"${BODY}"
fi
# Save to file for later use
printf '%s\n' "$BODY" > pr-body.txt
cat pr-body.txt
- name: Create PR to ComfyUI
working-directory: comfyui
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
COMFYUI_FORK: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
run: |
set -euo pipefail
# Extract fork owner from repository name
FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1)
echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI"
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create/update branch (reuse same branch name each week)
BRANCH="automation/comfyui-frontend-bump"
git checkout -B "$BRANCH"
git add requirements.txt
if ! git diff --cached --quiet; then
git commit -m "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}"
else
echo "No changes to commit"
exit 0
fi
# Force push to fork (overwrites previous week's branch)
# Note: This intentionally destroys branch history to maintain a single PR
# Any review comments or manual commits will need to be re-applied
if ! git push -f origin "$BRANCH"; then
echo "Failed to push branch to fork"
exit 1
fi
# Create draft PR from fork to upstream
PR_BODY=$(cat ../pr-body.txt)
# Try to create PR, ignore error if it already exists
if ! gh pr create \
--repo comfyanonymous/ComfyUI \
--head "${FORK_OWNER}:${BRANCH}" \
--base master \
--title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \
--body "$PR_BODY" \
--draft 2>&1; then
# Check if PR already exists
set +e
EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
PR_LIST_EXIT=$?
set -e
if [ $PR_LIST_EXIT -ne 0 ]; then
echo "Failed to check for existing PR: $EXISTING_PR"
exit 1
fi
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
echo "PR already exists (#${EXISTING_PR}), updating branch will update the PR"
else
echo "Failed to create PR and no existing PR found"
exit 1
fi
fi
- name: Summary
run: |
echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
cat pr-body.txt >> $GITHUB_STEP_SUMMARY

View File

@@ -1,5 +1,5 @@
# Description: Automated weekly documentation accuracy check and update via Claude
name: "Weekly Documentation Check" name: "Weekly Documentation Check"
description: "Automated weekly documentation accuracy check and update via Claude"
permissions: permissions:
contents: write contents: write

View File

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

View File

@@ -38,6 +38,7 @@
"typescript/no-redundant-type-constituents": "off", "typescript/no-redundant-type-constituents": "off",
"typescript/restrict-template-expressions": "off", "typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off", "typescript/unbound-method": "off",
"typescript/no-floating-promises": "error" "typescript/no-floating-promises": "error",
"vue/no-import-compiler-macros": "error"
} }
} }

View File

@@ -64,7 +64,6 @@ const config: StorybookConfig = {
deep: true, deep: true,
extensions: ['vue'] extensions: ['vue']
}) })
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
], ],
server: { server: {
allowedHosts: true allowedHosts: true

View File

@@ -1,8 +1,11 @@
# Global Ownership
* @Comfy-org/comfy_frontend_devs
# Desktop/Electron # Desktop/Electron
/apps/desktop-ui/ @webfiltered /apps/desktop-ui/ @benceruleanlu
/src/stores/electronDownloadStore.ts @webfiltered /src/stores/electronDownloadStore.ts @benceruleanlu
/src/extensions/core/electronAdapter.ts @webfiltered /src/extensions/core/electronAdapter.ts @benceruleanlu
/vite.electron.config.mts @webfiltered /vite.electron.config.mts @benceruleanlu
# Common UI Components # Common UI Components
/src/components/chip/ @viva-jinyi /src/components/chip/ @viva-jinyi
@@ -31,10 +34,7 @@
/src/components/graph/selectionToolbox/ @Myestery /src/components/graph/selectionToolbox/ @Myestery
# Minimap # Minimap
/src/renderer/extensions/minimap/ @jtydhr88 /src/renderer/extensions/minimap/ @jtydhr88 @Myestery
# Assets
/src/platform/assets/ @arjansingh
# Workflow Templates # Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki /src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
@@ -53,11 +53,12 @@
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata /src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations # Translations
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer /src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
/src/locales/pt-BR/ @JonatanAtila @Yorha4D @KarryCharon @shinshin86
# LLM Instructions (blank on purpose) # LLM Instructions (blank on purpose)
.claude/ .claude/
.cursor/ .cursor/
.cursorrules .cursorrules
**/AGENTS.md **/AGENTS.md
**/CLAUDE.md **/CLAUDE.md

View File

@@ -1,6 +1,6 @@
{ {
"name": "@comfyorg/desktop-ui", "name": "@comfyorg/desktop-ui",
"version": "0.0.3", "version": "0.0.4",
"type": "module", "type": "module",
"nx": { "nx": {
"tags": [ "tags": [

View File

@@ -59,7 +59,8 @@ const LOCALES = [
['fr', 'Français'], ['fr', 'Français'],
['es', 'Español'], ['es', 'Español'],
['ar', 'عربي'], ['ar', 'عربي'],
['tr', 'Türkçe'] ['tr', 'Türkçe'],
['pt-BR', 'Português (BR)']
] as const satisfies ReadonlyArray<[string, string]> ] as const satisfies ReadonlyArray<[string, string]>
type SupportedLocale = (typeof LOCALES)[number][0] type SupportedLocale = (typeof LOCALES)[number][0]

View File

@@ -22,7 +22,11 @@
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300"> <h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
{{ title }} {{ title }}
</h1> </h1>
<p v-if="statusText" class="text-lg text-neutral-400"> <p
v-if="statusText"
class="text-lg text-neutral-400"
data-testid="startup-status-text"
>
{{ statusText }} {{ statusText }}
</p> </p>
</div> </div>

View File

@@ -40,7 +40,8 @@ const localeLoaders: Record<
ru: () => import('@frontend-locales/ru/main.json'), ru: () => import('@frontend-locales/ru/main.json'),
tr: () => import('@frontend-locales/tr/main.json'), tr: () => import('@frontend-locales/tr/main.json'),
zh: () => import('@frontend-locales/zh/main.json'), zh: () => import('@frontend-locales/zh/main.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/main.json') 'zh-TW': () => import('@frontend-locales/zh-TW/main.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/main.json')
} }
const nodeDefsLoaders: Record< const nodeDefsLoaders: Record<
@@ -55,7 +56,8 @@ const nodeDefsLoaders: Record<
ru: () => import('@frontend-locales/ru/nodeDefs.json'), ru: () => import('@frontend-locales/ru/nodeDefs.json'),
tr: () => import('@frontend-locales/tr/nodeDefs.json'), tr: () => import('@frontend-locales/tr/nodeDefs.json'),
zh: () => import('@frontend-locales/zh/nodeDefs.json'), zh: () => import('@frontend-locales/zh/nodeDefs.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json') 'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/nodeDefs.json')
} }
const commandsLoaders: Record< const commandsLoaders: Record<
@@ -70,7 +72,8 @@ const commandsLoaders: Record<
ru: () => import('@frontend-locales/ru/commands.json'), ru: () => import('@frontend-locales/ru/commands.json'),
tr: () => import('@frontend-locales/tr/commands.json'), tr: () => import('@frontend-locales/tr/commands.json'),
zh: () => import('@frontend-locales/zh/commands.json'), zh: () => import('@frontend-locales/zh/commands.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json') 'zh-TW': () => import('@frontend-locales/zh-TW/commands.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/commands.json')
} }
const settingsLoaders: Record< const settingsLoaders: Record<
@@ -85,7 +88,8 @@ const settingsLoaders: Record<
ru: () => import('@frontend-locales/ru/settings.json'), ru: () => import('@frontend-locales/ru/settings.json'),
tr: () => import('@frontend-locales/tr/settings.json'), tr: () => import('@frontend-locales/tr/settings.json'),
zh: () => import('@frontend-locales/zh/settings.json'), zh: () => import('@frontend-locales/zh/settings.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json') 'zh-TW': () => import('@frontend-locales/zh-TW/settings.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/settings.json')
} }
// Track which locales have been loaded // Track which locales have been loaded

View File

@@ -557,7 +557,7 @@ export class ComfyPage {
async dragAndDrop(source: Position, target: Position) { async dragAndDrop(source: Position, target: Position) {
await this.page.mouse.move(source.x, source.y) await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down() await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y) await this.page.mouse.move(target.x, target.y, { steps: 100 })
await this.page.mouse.up() await this.page.mouse.up()
await this.nextFrame() await this.nextFrame()
} }
@@ -1651,7 +1651,10 @@ export const comfyPageFixture = base.extend<{
// Set tutorial completed to true to avoid loading the tutorial workflow. // Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true, 'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize, 'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false 'Comfy.VueNodes.AutoScaleLayout': false,
// Disable toast warning about version compatibility, as they may or
// may not appear - depending on upstream ComfyUI dependencies
'Comfy.VersionCompatibility.DisableWarnings': true
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@@ -65,7 +65,9 @@ export class VueNodeHelpers {
* Select a specific Vue node by ID * Select a specific Vue node by ID
*/ */
async selectNode(nodeId: string): Promise<void> { async selectNode(nodeId: string): Promise<void> {
await this.page.locator(`[data-node-id="${nodeId}"]`).click() await this.page
.locator(`[data-node-id="${nodeId}"] .lg-node-header`)
.click()
} }
/** /**
@@ -77,11 +79,13 @@ export class VueNodeHelpers {
// Select first node normally // Select first node normally
await this.selectNode(nodeIds[0]) await this.selectNode(nodeIds[0])
// Add additional nodes with Ctrl+click // Add additional nodes with Ctrl+click on header
for (let i = 1; i < nodeIds.length; i++) { for (let i = 1; i < nodeIds.length; i++) {
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({ await this.page
modifiers: ['Control'] .locator(`[data-node-id="${nodeIds[i]}"] .lg-node-header`)
}) .click({
modifiers: ['Control']
})
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -94,7 +94,7 @@ async function connectSlots(
const fromLoc = slotLocator(page, from.nodeId, from.index, false) const fromLoc = slotLocator(page, from.nodeId, from.index, false)
const toLoc = slotLocator(page, to.nodeId, to.index, true) const toLoc = slotLocator(page, to.nodeId, to.index, true)
await expectVisibleAll(fromLoc, toLoc) await expectVisibleAll(fromLoc, toLoc)
await fromLoc.dragTo(toLoc) await fromLoc.dragTo(toLoc, { force: true })
await nextFrame() await nextFrame()
} }
@@ -183,7 +183,7 @@ test.describe('Vue Node Link Interaction', () => {
const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true) const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true)
await expectVisibleAll(outputSlot, inputSlot) await expectVisibleAll(outputSlot, inputSlot)
await outputSlot.dragTo(inputSlot) await outputSlot.dragTo(inputSlot, { force: true })
await comfyPage.nextFrame() await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0) expect(await samplerOutput.getLinkCount()).toBe(0)
@@ -210,7 +210,7 @@ test.describe('Vue Node Link Interaction', () => {
const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true) const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true)
await expectVisibleAll(outputSlot, inputSlot) await expectVisibleAll(outputSlot, inputSlot)
await outputSlot.dragTo(inputSlot) await outputSlot.dragTo(inputSlot, { force: true })
await comfyPage.nextFrame() await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0) expect(await samplerOutput.getLinkCount()).toBe(0)
@@ -828,55 +828,55 @@ test.describe('Vue Node Link Interaction', () => {
}) })
test.describe('Release actions (Shift-drop)', () => { test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({ test.fixme(
comfyPage, 'Context menu opens and endpoint is pinned on Shift-drop',
comfyMouse async ({ comfyPage, comfyMouse }) => {
}) => { await comfyPage.setSetting(
await comfyPage.setSetting( 'Comfy.LinkRelease.ActionShift',
'Comfy.LinkRelease.ActionShift', 'context menu'
'context menu' )
)
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy() expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter( const outputCenter = await getSlotCenter(
comfyPage.page, comfyPage.page,
samplerNode.id, samplerNode.id,
0, 0,
false false
) )
const dropPos = { x: outputCenter.x + 180, y: outputCenter.y - 140 } const dropPos = { x: outputCenter.x + 180, y: outputCenter.y - 140 }
await comfyMouse.move(outputCenter) await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift') await comfyPage.page.keyboard.down('Shift')
try { try {
await comfyMouse.drag(dropPos) await comfyMouse.drag(dropPos)
await comfyMouse.drop() await comfyMouse.drop()
} finally { } finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {}) await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
// Context menu should be visible
const contextMenu = comfyPage.page.locator('.litecontextmenu')
await expect(contextMenu).toBeVisible()
// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()
// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
} }
)
// Context menu should be visible
const contextMenu = comfyPage.page.locator('.litecontextmenu')
await expect(contextMenu).toBeVisible()
// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()
// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
})
test('Context menu -> Search pre-filters by link type and connects after selection', async ({ test('Context menu -> Search pre-filters by link type and connects after selection', async ({
comfyPage, comfyPage,
@@ -897,7 +897,7 @@ test.describe('Vue Node Link Interaction', () => {
0, 0,
false false
) )
const dropPos = { x: outputCenter.x + 200, y: outputCenter.y - 120 } const dropPos = { x: outputCenter.x + 200, y: outputCenter.y - 100 }
await comfyMouse.move(outputCenter) await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift') await comfyPage.page.keyboard.down('Shift')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,54 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Node Resizing', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should resize node without position drift after selecting', async ({
comfyPage
}) => {
// Get a Vue node fixture
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
const initialBox = await node.boundingBox()
if (!initialBox) throw new Error('Node bounding box not found')
// Select the node first (this was causing the bug)
await node.header.click()
await comfyPage.page.waitForTimeout(100) // Brief pause after selection
// Get position after selection
const selectedBox = await node.boundingBox()
if (!selectedBox)
throw new Error('Node bounding box not found after select')
// Verify position unchanged after selection
expect(selectedBox.x).toBeCloseTo(initialBox.x, 1)
expect(selectedBox.y).toBeCloseTo(initialBox.y, 1)
// Now resize from bottom-right corner
const resizeStartX = selectedBox.x + selectedBox.width - 5
const resizeStartY = selectedBox.y + selectedBox.height - 5
await comfyPage.page.mouse.move(resizeStartX, resizeStartY)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(resizeStartX + 50, resizeStartY + 30)
await comfyPage.page.mouse.up()
// Get final position and size
const finalBox = await node.boundingBox()
if (!finalBox) throw new Error('Node bounding box not found after resize')
// Position should NOT have changed (the bug was position drift)
expect(finalBox.x).toBeCloseTo(initialBox.x, 1)
expect(finalBox.y).toBeCloseTo(initialBox.y, 1)
// Size should have increased
expect(finalBox.width).toBeGreaterThan(initialBox.width)
expect(finalBox.height).toBeGreaterThan(initialBox.height)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -1,49 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Vue Nodes - LOD', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
await comfyPage.setup()
await comfyPage.loadWorkflow('default')
})
test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {
await comfyPage.vueNodes.waitForNodes()
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
expect(initialNodeCount).toBeGreaterThan(0)
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png')
const vueNodesContainer = comfyPage.vueNodes.nodes
const textboxesInNodes = vueNodesContainer.getByRole('textbox')
const comboboxesInNodes = vueNodesContainer.getByRole('combobox')
await expect(textboxesInNodes.first()).toBeVisible()
await expect(comboboxesInNodes.first()).toBeVisible()
await comfyPage.zoom(120, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png')
await expect(textboxesInNodes.first()).toBeHidden()
await expect(comboboxesInNodes.first()).toBeHidden()
await comfyPage.zoom(-120, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-lod-inactive.png'
)
await expect(textboxesInNodes.first()).toBeVisible()
await expect(comboboxesInNodes.first()).toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -88,12 +88,14 @@ export function comfyAPIPlugin(isDev: boolean): Plugin {
if (result.exports.length > 0) { if (result.exports.length > 0) {
const projectRoot = process.cwd() const projectRoot = process.cwd()
const relativePath = path.relative(path.join(projectRoot, 'src'), id) const relativePath = path
.relative(path.join(projectRoot, 'src'), id)
.replace(/\\/g, '/')
const shimFileName = relativePath.replace(/\.ts$/, '.js') const shimFileName = relativePath.replace(/\.ts$/, '.js')
let shimContent = `// Shim for ${relativePath}\n` let shimContent = `// Shim for ${relativePath}\n`
const fileKey = relativePath.replace(/\.ts$/, '').replace(/\\/g, '/') const fileKey = relativePath.replace(/\.ts$/, '')
const warningMessage = getWarningMessage(fileKey, shimFileName) const warningMessage = getWarningMessage(fileKey, shimFileName)
if (warningMessage) { if (warningMessage) {

View File

@@ -1,154 +0,0 @@
import glob from 'fast-glob'
import fs from 'fs-extra'
import { dirname, join } from 'node:path'
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
interface ImportMapSource {
name: string
pattern: string | RegExp
entry: string
recursiveDependence?: boolean
override?: Record<string, Partial<ImportMapSource>>
}
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 []
}
/**
* Vite plugin that generates an import map for vendor chunks.
*
* This plugin creates a browser-compatible import map that maps module specifiers
* (like 'vue' or 'primevue') to their actual file locations in the build output.
* This improves module loading in modern browsers and enables better caching.
*
* The plugin:
* 1. Tracks vendor chunks during bundle generation
* 2. Creates mappings between module names and their file paths
* 3. Injects an import map script tag into the HTML head
* 4. Configures manual chunk splitting for vendor libraries
*
* @param vendorLibraries - An array of vendor libraries to split into separate chunks
* @returns {Plugin} A Vite plugin that generates and injects an import map
*/
export function generateImportMapPlugin(
importMapSources: ImportMapSource[]
): Plugin {
const importMapEntries: Record<string, string> = {}
const resolvedImportMapSources: Map<string, ImportMapSource> = 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 = {}
}
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)
}
}
}
const external: (string | RegExp)[] = []
for (const [, source] of resolvedImportMapSources) {
external.push(source.pattern)
}
config.build.rollupOptions.external = external
}
},
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)
importMapEntries[source.name] =
'./' + normalizePath(join(assetDir, moduleFile))
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)
}
}
}
},
transformIndexHtml(html) {
if (Object.keys(importMapEntries).length === 0) {
console.warn(
'[ImportMap Plugin] No vendor chunks found to create import map.'
)
return html
}
const importMap = {
imports: importMapEntries
}
const importMapTag: HtmlTagDescriptor = {
tag: 'script',
attrs: { type: 'importmap' },
children: JSON.stringify(importMap, null, 2),
injectTo: 'head'
}
return {
html,
tags: [importMapTag]
}
}
}
}

View File

@@ -1,2 +1 @@
export { comfyAPIPlugin } from './comfyAPIPlugin' export { comfyAPIPlugin } from './comfyAPIPlugin'
export { generateImportMapPlugin } from './generateImportMapPlugin'

View File

@@ -191,6 +191,19 @@ export default defineConfig([
'@intlify/vue-i18n/no-raw-text': [ '@intlify/vue-i18n/no-raw-text': [
'error', 'error',
{ {
attributes: {
'/.+/': [
'aria-label',
'aria-placeholder',
'aria-roledescription',
'aria-valuetext',
'label',
'placeholder',
'title',
'v-tooltip'
],
img: ['alt']
},
// Ignore strings that are: // Ignore strings that are:
// 1. Less than 2 characters // 1. Less than 2 characters
// 2. Only symbols/numbers/whitespace (no letters) // 2. Only symbols/numbers/whitespace (no letters)
@@ -200,24 +213,27 @@ export default defineConfig([
ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'], ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'],
// Brand names and technical terms that shouldn't be translated // Brand names and technical terms that shouldn't be translated
ignoreText: [ ignoreText: [
'ComfyUI',
'GitHub',
'OpenAI',
'API', 'API',
'URL',
'JSON',
'YAML',
'GPU',
'CPU',
'RAM',
'GB',
'MB',
'KB',
'ms',
'fps',
'px',
'App Data:', 'App Data:',
'App Path:' 'App Path:',
'ComfyUI',
'CPU',
'fps',
'GB',
'GitHub',
'GPU',
'JSON',
'KB',
'LoRA',
'MB',
'ms',
'OpenAI',
'png',
'px',
'RAM',
'URL',
'YAML',
'1.2 MB'
] ]
} }
] ]

View File

@@ -27,7 +27,7 @@ const config: KnipConfig = {
project: ['src/**/*.{js,ts}'] project: ['src/**/*.{js,ts}']
} }
}, },
ignoreBinaries: ['python3'], ignoreBinaries: ['python3', 'gh'],
ignoreDependencies: [ ignoreDependencies: [
// Weird importmap things // Weird importmap things
'@iconify/json', '@iconify/json',

View File

@@ -1,7 +1,7 @@
{ {
"name": "@comfyorg/comfyui-frontend", "name": "@comfyorg/comfyui-frontend",
"private": true, "private": true,
"version": "1.33.2", "version": "1.34.5",
"type": "module", "type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org", "homepage": "https://comfy.org",
@@ -79,6 +79,7 @@
"@vitest/coverage-v8": "catalog:", "@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:", "@vitest/ui": "catalog:",
"@vue/test-utils": "catalog:", "@vue/test-utils": "catalog:",
"@webgpu/types": "catalog:",
"cross-env": "catalog:", "cross-env": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"eslint-config-prettier": "catalog:", "eslint-config-prettier": "catalog:",
@@ -116,6 +117,7 @@
"typescript": "catalog:", "typescript": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"unplugin-icons": "catalog:", "unplugin-icons": "catalog:",
"unplugin-typegpu": "catalog:",
"unplugin-vue-components": "catalog:", "unplugin-vue-components": "catalog:",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vite": "catalog:", "vite": "catalog:",
@@ -166,7 +168,6 @@
"es-toolkit": "^1.39.9", "es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27", "extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129", "extendable-media-recorder-wav-encoder": "^7.0.129",
"fast-glob": "^3.3.3",
"firebase": "catalog:", "firebase": "catalog:",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"glob": "^11.0.3", "glob": "^11.0.3",
@@ -180,6 +181,7 @@
"semver": "^7.7.2", "semver": "^7.7.2",
"three": "^0.170.0", "three": "^0.170.0",
"tiptap-markdown": "^0.8.10", "tiptap-markdown": "^0.8.10",
"typegpu": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-i18n": "catalog:", "vue-i18n": "catalog:",
"vue-router": "catalog:", "vue-router": "catalog:",

View File

@@ -194,7 +194,7 @@
--node-component-executing: var(--color-blue-500); --node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color); --node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-ash-800); --node-component-header-icon: var(--color-ash-800);
--node-component-header-surface: var(--color-white); --node-component-header-surface: var(--color-smoke-400);
--node-component-outline: var(--color-black); --node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%); --node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%);
--node-component-slot-dot-outline-opacity-mult: 1; --node-component-slot-dot-outline-opacity-mult: 1;
@@ -1190,24 +1190,19 @@ dialog::backdrop {
.litegraph.litecontextmenu, .litegraph.litecontextmenu,
.litegraph.litecontextmenu.dark { .litegraph.litecontextmenu.dark {
z-index: 9999 !important; z-index: 9999 !important;
background-color: var(--comfy-menu-bg) !important; background-color: var(--comfy-menu-bg);
} }
.litegraph.litecontextmenu
.litemenu-entry:hover:not(.disabled):not(.separator) {
background-color: var(--comfy-menu-hover-bg, var(--border-color)) !important;
color: var(--fg-color);
}
.litegraph.litecontextmenu .litemenu-entry.submenu, .litegraph.litecontextmenu .litemenu-entry.submenu,
.litegraph.litecontextmenu.dark .litemenu-entry.submenu { .litegraph.litecontextmenu.dark .litemenu-entry.submenu {
background-color: var(--comfy-menu-bg) !important; background-color: var(--comfy-menu-bg);
color: var(--input-text); color: var(--fg-color);
} }
.litegraph.litecontextmenu input { .litegraph.litecontextmenu input {
background-color: var(--comfy-input-bg) !important; background-color: var(--comfy-input-bg);
color: var(--input-text) !important; color: var(--input-text);
} }
.comfy-context-menu-filter { .comfy-context-menu-filter {
@@ -1248,14 +1243,14 @@ dialog::backdrop {
.litegraph.litesearchbox { .litegraph.litesearchbox {
z-index: 9999 !important; z-index: 9999 !important;
background-color: var(--comfy-menu-bg) !important; background-color: var(--comfy-menu-bg);
overflow: hidden; overflow: hidden;
display: block; display: block;
} }
.litegraph.litesearchbox input, .litegraph.litesearchbox input,
.litegraph.litesearchbox select { .litegraph.litesearchbox select {
background-color: var(--comfy-input-bg) !important; background-color: var(--comfy-input-bg);
color: var(--input-text); color: var(--input-text);
} }
@@ -1320,66 +1315,6 @@ audio.comfy-audio.empty-audio-widget {
font-size 0.1s ease; font-size 0.1s ease;
} }
/* Performance optimization during canvas interaction */
.transform-pane--interacting .lg-node * {
transition: none !important;
}
.transform-pane--interacting .lg-node {
will-change: transform;
}
/* START LOD specific styles */
/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */
.isLOD .lg-node {
box-shadow: none;
filter: none;
backdrop-filter: none;
text-shadow: none;
mask-image: none;
clip-path: none;
background-image: none;
text-rendering: optimizeSpeed;
border-radius: 0;
contain: layout style;
transition: none;
}
.isLOD .lg-node-header {
border-radius: 0;
pointer-events: none;
}
.isLOD .lg-node-widgets {
pointer-events: none;
}
.lod-toggle {
visibility: visible;
}
.isLOD .lod-toggle {
visibility: hidden;
}
.lod-fallback {
display: none;
}
.isLOD .lod-fallback {
display: block;
}
.isLOD .image-preview img {
image-rendering: pixelated;
}
.isLOD .slot-dot {
border-radius: 0;
}
/* END LOD specific styles */
/* ===================== Mask Editor Styles ===================== */ /* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */ /* To be migrated to Tailwind later */
#maskEditor_brush { #maskEditor_brush {

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,17 @@ export function formatSize(value?: number) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}` return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
} }
/**
* Formats a commit hash by truncating long (40-char) hashes to 7 chars.
* Returns the original string if not a valid full commit hash.
*/
export function formatCommitHash(value: string): string {
if (/^[a-f0-9]{40}$/i.test(value)) {
return value.slice(0, 7)
}
return value
}
/** /**
* Returns various filename components. * Returns various filename components.
* Example: * Example:

103
pnpm-lock.yaml generated
View File

@@ -15,15 +15,9 @@ catalogs:
'@eslint/js': '@eslint/js':
specifier: ^9.35.0 specifier: ^9.35.0
version: 9.35.0 version: 9.35.0
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.66
'@iconify/json': '@iconify/json':
specifier: ^2.2.380 specifier: ^2.2.380
version: 2.2.380 version: 2.2.380
'@iconify/tailwind':
specifier: ^1.1.3
version: 1.2.0
'@intlify/eslint-plugin-vue-i18n': '@intlify/eslint-plugin-vue-i18n':
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
@@ -126,6 +120,9 @@ catalogs:
'@vueuse/integrations': '@vueuse/integrations':
specifier: ^13.9.0 specifier: ^13.9.0
version: 13.9.0 version: 13.9.0
'@webgpu/types':
specifier: ^0.1.66
version: 0.1.66
algoliasearch: algoliasearch:
specifier: ^5.21.0 specifier: ^5.21.0
version: 5.21.0 version: 5.21.0
@@ -246,6 +243,9 @@ catalogs:
tw-animate-css: tw-animate-css:
specifier: ^1.3.8 specifier: ^1.3.8
version: 1.3.8 version: 1.3.8
typegpu:
specifier: ^0.8.2
version: 0.8.2
typescript: typescript:
specifier: ^5.9.2 specifier: ^5.9.2
version: 5.9.2 version: 5.9.2
@@ -255,6 +255,9 @@ catalogs:
unplugin-icons: unplugin-icons:
specifier: ^0.22.0 specifier: ^0.22.0
version: 0.22.0 version: 0.22.0
unplugin-typegpu:
specifier: 0.8.0
version: 0.8.0
unplugin-vue-components: unplugin-vue-components:
specifier: ^0.28.0 specifier: ^0.28.0
version: 0.28.0 version: 0.28.0
@@ -422,9 +425,6 @@ importers:
extendable-media-recorder-wav-encoder: extendable-media-recorder-wav-encoder:
specifier: ^7.0.129 specifier: ^7.0.129
version: 7.0.129 version: 7.0.129
fast-glob:
specifier: ^3.3.3
version: 3.3.3
firebase: firebase:
specifier: 'catalog:' specifier: 'catalog:'
version: 11.6.0 version: 11.6.0
@@ -464,6 +464,9 @@ importers:
tiptap-markdown: tiptap-markdown:
specifier: ^0.8.10 specifier: ^0.8.10
version: 0.8.10(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) version: 0.8.10(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
typegpu:
specifier: 'catalog:'
version: 0.8.2
vue: vue:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.5.13(typescript@5.9.2) version: 3.5.13(typescript@5.9.2)
@@ -561,6 +564,9 @@ importers:
'@vue/test-utils': '@vue/test-utils':
specifier: 'catalog:' specifier: 'catalog:'
version: 2.4.6 version: 2.4.6
'@webgpu/types':
specifier: 'catalog:'
version: 0.1.66
cross-env: cross-env:
specifier: 'catalog:' specifier: 'catalog:'
version: 10.1.0 version: 10.1.0
@@ -672,6 +678,9 @@ importers:
unplugin-icons: unplugin-icons:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.22.0(@vue/compiler-sfc@3.5.13) version: 0.22.0(@vue/compiler-sfc@3.5.13)
unplugin-typegpu:
specifier: 'catalog:'
version: 0.8.0(typegpu@0.8.2)
unplugin-vue-components: unplugin-vue-components:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)) version: 0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2))
@@ -1431,6 +1440,10 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/standalone@7.28.5':
resolution: {integrity: sha512-1DViPYJpRU50irpGMfLBQ9B4kyfQuL6X7SS7pwTeWeZX0mNkjzPi0XFqxCjSdddZXUQy4AhnQnnesA/ZHnvAdw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2': '@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -3790,8 +3803,8 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.5.0 vue: ^3.5.0
'@webgpu/types@0.1.51': '@webgpu/types@0.1.66':
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==} resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
'@xstate/fsm@1.6.5': '@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
@@ -6038,6 +6051,10 @@ packages:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true hasBin: true
magic-string-ast@1.0.3:
resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
engines: {node: '>=20.19.0'}
magic-string@0.30.19: magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
@@ -7411,6 +7428,14 @@ packages:
tinybench@2.9.0: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyest-for-wgsl@0.1.3:
resolution: {integrity: sha512-Wm5ADG1UyDxykf42S1gLYP4U9e1QP/TdtJeovQi6y68zttpiFLKqQGioHmPs9Mjysh7YMSAr/Lpuk0cD2MVdGA==}
engines: {node: '>=12.20.0'}
tinyest@0.1.2:
resolution: {integrity: sha512-aHRmouyowIq1P5jrTF+YK6pGX+WuvFtSCLbqk91yHnU3SWQRIcNIamZLM5XF6lLqB13AWz0PGPXRff2QGDsxIg==}
engines: {node: '>=12.20.0'}
tinyexec@0.3.2: tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
@@ -7537,6 +7562,13 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
typed-binary@4.3.2:
resolution: {integrity: sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ==}
typegpu@0.8.2:
resolution: {integrity: sha512-wkMJWhJE0pSkw2G/FesjqjbtHkREyOKu1Zmyj19xfmaX5+65YFwgfQNKSK8CxqN4kJkP7JFelLDJTSYY536TYg==}
engines: {node: '>=12.20.0'}
typescript-eslint@8.44.0: typescript-eslint@8.44.0:
resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==} resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -7641,6 +7673,11 @@ packages:
vue-template-es2015-compiler: vue-template-es2015-compiler:
optional: true optional: true
unplugin-typegpu@0.8.0:
resolution: {integrity: sha512-VJHdXSXGOkAx0WhwFczhVUjAI6HyDkrQXk20HnwyuzIE3FdqE5l9sJTCYZzoVGo3z8i/IA5TMHCDzzP0Bc97Cw==}
peerDependencies:
typegpu: ^0.8.0
unplugin-vue-components@0.28.0: unplugin-vue-components@0.28.0:
resolution: {integrity: sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==} resolution: {integrity: sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -7831,8 +7868,8 @@ packages:
vue-component-type-helpers@3.1.1: vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==} resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
vue-component-type-helpers@3.1.4: vue-component-type-helpers@3.1.5:
resolution: {integrity: sha512-Uws7Ew1OzTTqHW8ZVl/qLl/HB+jf08M0NdFONbVWAx0N4gMLK8yfZDgeB77hDnBmaigWWEn5qP8T9BG59jIeyQ==} resolution: {integrity: sha512-7V3yJuNWW7/1jxCcI1CswnpDsvs02Qcx/N43LkV+ZqhLj2PKj50slUflHAroNkN4UWiYfzMUUUXiNuv9khmSpQ==}
vue-demi@0.14.10: vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -8969,6 +9006,8 @@ snapshots:
'@babel/runtime@7.28.4': {} '@babel/runtime@7.28.4': {}
'@babel/standalone@7.28.5': {}
'@babel/template@7.27.2': '@babel/template@7.27.2':
dependencies: dependencies:
'@babel/code-frame': 7.27.1 '@babel/code-frame': 7.27.1
@@ -10633,7 +10672,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2) vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.4 vue-component-type-helpers: 3.1.5
'@swc/helpers@0.5.17': '@swc/helpers@0.5.17':
dependencies: dependencies:
@@ -11016,7 +11055,7 @@ snapshots:
'@tweenjs/tween.js': 23.1.3 '@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.3 '@types/stats.js': 0.17.3
'@types/webxr': 0.5.20 '@types/webxr': 0.5.20
'@webgpu/types': 0.1.51 '@webgpu/types': 0.1.66
fflate: 0.8.2 fflate: 0.8.2
meshoptimizer: 0.18.1 meshoptimizer: 0.18.1
@@ -11519,7 +11558,7 @@ snapshots:
dependencies: dependencies:
vue: 3.5.13(typescript@5.9.2) vue: 3.5.13(typescript@5.9.2)
'@webgpu/types@0.1.51': {} '@webgpu/types@0.1.66': {}
'@xstate/fsm@1.6.5': {} '@xstate/fsm@1.6.5': {}
@@ -14000,6 +14039,10 @@ snapshots:
lz-string@1.5.0: {} lz-string@1.5.0: {}
magic-string-ast@1.0.3:
dependencies:
magic-string: 0.30.19
magic-string@0.30.19: magic-string@0.30.19:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -15864,6 +15907,12 @@ snapshots:
tinybench@2.9.0: {} tinybench@2.9.0: {}
tinyest-for-wgsl@0.1.3:
dependencies:
tinyest: 0.1.2
tinyest@0.1.2: {}
tinyexec@0.3.2: {} tinyexec@0.3.2: {}
tinyexec@1.0.1: {} tinyexec@1.0.1: {}
@@ -15995,6 +16044,13 @@ snapshots:
reflect.getprototypeof: 1.0.10 reflect.getprototypeof: 1.0.10
optional: true optional: true
typed-binary@4.3.2: {}
typegpu@0.8.2:
dependencies:
tinyest: 0.1.2
typed-binary: 4.3.2
typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2): typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
@@ -16090,6 +16146,19 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
unplugin-typegpu@0.8.0(typegpu@0.8.2):
dependencies:
'@babel/standalone': 7.28.5
defu: 6.1.4
estree-walker: 3.0.3
magic-string-ast: 1.0.3
pathe: 2.0.3
picomatch: 4.0.3
tinyest: 0.1.2
tinyest-for-wgsl: 0.1.3
typegpu: 0.8.2
unplugin: 2.3.5
unplugin-vue-components@0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)): unplugin-vue-components@0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)):
dependencies: dependencies:
'@antfu/utils': 0.7.10 '@antfu/utils': 0.7.10
@@ -16370,7 +16439,7 @@ snapshots:
vue-component-type-helpers@3.1.1: {} vue-component-type-helpers@3.1.1: {}
vue-component-type-helpers@3.1.4: {} vue-component-type-helpers@3.1.5: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)): vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies: dependencies:

View File

@@ -43,6 +43,7 @@ catalog:
'@vue/test-utils': ^2.4.6 '@vue/test-utils': ^2.4.6
'@vueuse/core': ^11.0.0 '@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0 '@vueuse/integrations': ^13.9.0
'@webgpu/types': ^0.1.66
algoliasearch: ^5.21.0 algoliasearch: ^5.21.0
axios: ^1.8.2 axios: ^1.8.2
cross-env: ^10.1.0 cross-env: ^10.1.0
@@ -83,9 +84,11 @@ catalog:
tailwindcss-primeui: ^0.6.1 tailwindcss-primeui: ^0.6.1
tsx: ^4.15.6 tsx: ^4.15.6
tw-animate-css: ^1.3.8 tw-animate-css: ^1.3.8
typegpu: ^0.8.2
typescript: ^5.9.2 typescript: ^5.9.2
typescript-eslint: ^8.44.0 typescript-eslint: ^8.44.0
unplugin-icons: ^0.22.0 unplugin-icons: ^0.22.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^0.28.0 unplugin-vue-components: ^0.28.0
vite: ^5.4.19 vite: ^5.4.19
vite-plugin-dts: ^4.5.4 vite-plugin-dts: ^4.5.4

View File

@@ -1 +1,9 @@
Thanks to OpenArt (https://openart.ai) for providing the sorted-custom-node-map data, captured in September 2024. Node usage data merged from two sources:
- Mixpanel "app:node_search_result_selected" events (Nov 2025): 1,112 nodes, 46,514 selections
Reflects actual user search behavior - what users choose when searching.
- OpenArt workflow data (Sept 2024): 2,600 nodes, 118,676 uses
Reflects overall popularity - what's used in workflows.
Merge strategy: New data overwrites old for 514 overlapping nodes. Old data
normalized by 2.55x scale factor to match new data. Total: 3,198 nodes.
Search-selected nodes prioritized in ranking.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env tsx
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
interface ReleaseInfo {
current_version: string
target_minor: number
target_version: string
target_branch: string
needs_release: boolean
latest_patch_tag: string | null
branch_head_sha: string | null
tag_commit_sha: string | null
diff_url: string
release_pr_url: string | null
}
/**
* Execute a command and return stdout
*/
function exec(command: string, cwd?: string): string {
try {
return execSync(command, {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const cwdInfo = cwd ? ` in directory: ${cwd}` : ''
console.error(
`Command failed: ${command}${cwdInfo}\nError: ${errorMessage}`
)
return ''
}
}
/**
* Parse version from requirements.txt
* Handles formats: comfyui-frontend-package==1.2.3, comfyui-frontend-package>=1.2.3, etc.
*/
function parseRequirementsVersion(requirementsPath: string): string | null {
if (!fs.existsSync(requirementsPath)) {
console.error(`Requirements file not found: ${requirementsPath}`)
return null
}
const content = fs.readFileSync(requirementsPath, 'utf-8')
const match = content.match(
/comfyui-frontend-package\s*(?:==|>=|<=|~=|>|<)\s*([0-9]+\.[0-9]+\.[0-9]+)/
)
if (!match) {
console.error(
'Could not find comfyui-frontend-package version in requirements.txt'
)
return null
}
return match[1]
}
/**
* Validate semantic version string
*/
function isValidSemver(version: string): boolean {
if (!version || typeof version !== 'string') {
return false
}
const parts = version.split('.')
if (parts.length !== 3) {
return false
}
return parts.every((part) => {
const num = Number(part)
return Number.isFinite(num) && num >= 0 && String(num) === part
})
}
/**
* Get the latest patch tag for a given minor version
*/
function getLatestPatchTag(repoPath: string, minor: number): string | null {
// Fetch all tags
exec('git fetch --tags', repoPath)
// Use git's native version sorting to get the latest tag
const latestTag = exec(
`git tag -l 'v1.${minor}.*' --sort=-version:refname | head -n 1`,
repoPath
)
if (!latestTag) {
return null
}
// Validate the tag is a valid semver (vX.Y.Z format)
const validTagRegex = /^v\d+\.\d+\.\d+$/
if (!validTagRegex.test(latestTag)) {
console.error(
`Latest tag for minor version ${minor} is not valid semver: ${latestTag}`
)
return null
}
return latestTag
}
/**
* Resolve the ComfyUI release information
*/
function resolveRelease(
comfyuiRepoPath: string,
frontendRepoPath: string
): ReleaseInfo | null {
// Parse current version from ComfyUI requirements.txt
const requirementsPath = path.join(comfyuiRepoPath, 'requirements.txt')
const currentVersion = parseRequirementsVersion(requirementsPath)
if (!currentVersion) {
return null
}
// Validate version format
if (!isValidSemver(currentVersion)) {
console.error(
`Invalid semantic version format: ${currentVersion}. Expected format: X.Y.Z`
)
return null
}
const [major, currentMinor, patch] = currentVersion.split('.').map(Number)
// Calculate target minor version (next minor)
const targetMinor = currentMinor + 1
const targetBranch = `core/1.${targetMinor}`
// Check if target branch exists in frontend repo
exec('git fetch origin', frontendRepoPath)
const branchExists = exec(
`git rev-parse --verify origin/${targetBranch}`,
frontendRepoPath
)
if (!branchExists) {
console.error(
`Target branch ${targetBranch} does not exist in frontend repo`
)
return null
}
// Get latest patch tag for target minor
const latestPatchTag = getLatestPatchTag(frontendRepoPath, targetMinor)
let needsRelease = false
let branchHeadSha: string | null = null
let tagCommitSha: string | null = null
let targetVersion = currentVersion
if (latestPatchTag) {
// Get commit SHA for the tag
tagCommitSha = exec(`git rev-list -n 1 ${latestPatchTag}`, frontendRepoPath)
// Get commit SHA for branch head
branchHeadSha = exec(
`git rev-parse origin/${targetBranch}`,
frontendRepoPath
)
// Check if there are commits between tag and branch head
const commitsBetween = exec(
`git rev-list ${latestPatchTag}..origin/${targetBranch} --count`,
frontendRepoPath
)
const commitCount = parseInt(commitsBetween, 10)
needsRelease = !isNaN(commitCount) && commitCount > 0
// Parse existing patch number and increment if needed
const tagVersion = latestPatchTag.replace('v', '')
// Validate tag version format
if (!isValidSemver(tagVersion)) {
console.error(
`Invalid tag version format: ${tagVersion}. Expected format: X.Y.Z`
)
return null
}
const [, , existingPatch] = tagVersion.split('.').map(Number)
// Validate existingPatch is a valid number
if (!Number.isFinite(existingPatch) || existingPatch < 0) {
console.error(`Invalid patch number in tag: ${existingPatch}`)
return null
}
if (needsRelease) {
targetVersion = `1.${targetMinor}.${existingPatch + 1}`
} else {
targetVersion = tagVersion
}
} else {
// No tags exist for this minor version, need to create v1.{targetMinor}.0
needsRelease = true
targetVersion = `1.${targetMinor}.0`
branchHeadSha = exec(
`git rev-parse origin/${targetBranch}`,
frontendRepoPath
)
}
const diffUrl = `https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${currentVersion}...v${targetVersion}`
return {
current_version: currentVersion,
target_minor: targetMinor,
target_version: targetVersion,
target_branch: targetBranch,
needs_release: needsRelease,
latest_patch_tag: latestPatchTag,
branch_head_sha: branchHeadSha,
tag_commit_sha: tagCommitSha,
diff_url: diffUrl,
release_pr_url: null // Will be populated by workflow if release is triggered
}
}
// Main execution
const comfyuiRepoPath = process.argv[2]
const frontendRepoPath = process.argv[3] || process.cwd()
if (!comfyuiRepoPath) {
console.error(
'Usage: resolve-comfyui-release.ts <comfyui-repo-path> [frontend-repo-path]'
)
process.exit(1)
}
const releaseInfo = resolveRelease(comfyuiRepoPath, frontendRepoPath)
if (!releaseInfo) {
console.error('Failed to resolve release information')
process.exit(1)
}
// Output as JSON for GitHub Actions
// eslint-disable-next-line no-console
console.log(JSON.stringify(releaseInfo, null, 2))
export { resolveRelease }

View File

@@ -1,5 +1,8 @@
import * as fs from 'fs' import * as fs from 'fs'
// Import Vite define shim to make __DISTRIBUTION__ and other define variables available
import './vite-define-shim'
import { DESKTOP_DIALOGS } from '../apps/desktop-ui/src/constants/desktopDialogs' import { DESKTOP_DIALOGS } from '../apps/desktop-ui/src/constants/desktopDialogs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage' import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { import {

View File

@@ -0,0 +1,46 @@
/**
* Shim for Vite define variables to make them available during Playwright test execution
* This file should be imported before any code that uses Vite define variables
*/
// Define global constants that Vite would normally replace at build time
declare global {
const __COMFYUI_FRONTEND_VERSION__: string
const __SENTRY_ENABLED__: boolean
const __SENTRY_DSN__: string
const __ALGOLIA_APP_ID__: string
const __ALGOLIA_API_KEY__: string
const __USE_PROD_CONFIG__: boolean
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
}
type GlobalWithDefines = typeof globalThis & {
__COMFYUI_FRONTEND_VERSION__: string
__SENTRY_ENABLED__: boolean
__SENTRY_DSN__: string
__ALGOLIA_APP_ID__: string
__ALGOLIA_API_KEY__: string
__USE_PROD_CONFIG__: boolean
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
window?: Record<string, unknown>
}
const globalWithDefines = globalThis as GlobalWithDefines
// Set default values for Playwright test environment
globalWithDefines.__COMFYUI_FRONTEND_VERSION__ =
process.env.npm_package_version || '1.0.0'
globalWithDefines.__SENTRY_ENABLED__ = false
globalWithDefines.__SENTRY_DSN__ = ''
globalWithDefines.__ALGOLIA_APP_ID__ = ''
globalWithDefines.__ALGOLIA_API_KEY__ = ''
globalWithDefines.__USE_PROD_CONFIG__ = false
globalWithDefines.__DISTRIBUTION__ = 'localhost'
// Provide a minimal window shim for Node environment
// This is needed for code that checks window existence during imports
if (typeof window === 'undefined') {
globalWithDefines.window = {}
}
export {}

View File

@@ -14,10 +14,10 @@
</div> </div>
<Splitter <Splitter
key="main-splitter-stable" :key="splitterRefreshKey"
class="splitter-overlay flex-1 overflow-hidden" class="splitter-overlay flex-1 overflow-hidden"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'" :pt:gutter="getSplitterGutterClasses"
:state-key="sidebarStateKey || 'main-splitter'" :state-key="sidebarStateKey"
state-storage="local" state-storage="local"
> >
<SplitterPanel <SplitterPanel
@@ -80,6 +80,16 @@
name="side-bar-panel" name="side-bar-panel"
/> />
</SplitterPanel> </SplitterPanel>
<!-- Right Side Panel - independent of sidebar -->
<SplitterPanel
v-if="rightSidePanelVisible"
class="right-side-panel pointer-events-auto"
:min-size="15"
:size="20"
>
<slot name="right-side-panel" />
</SplitterPanel>
</Splitter> </Splitter>
</div> </div>
</div> </div>
@@ -92,9 +102,11 @@ import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const settingStore = useSettingStore() const settingStore = useSettingStore()
const rightSidePanelStore = useRightSidePanelStore()
const sidebarLocation = computed<'left' | 'right'>(() => const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location') settingStore.get('Comfy.Sidebar.Location')
) )
@@ -109,6 +121,7 @@ const sidebarPanelVisible = computed(
const bottomPanelVisible = computed( const bottomPanelVisible = computed(
() => useBottomPanelStore().bottomPanelVisible () => useBottomPanelStore().bottomPanelVisible
) )
const rightSidePanelVisible = computed(() => rightSidePanelStore.isOpen)
const activeSidebarTabId = computed( const activeSidebarTabId = computed(
() => useSidebarTabStore().activeSidebarTabId () => useSidebarTabStore().activeSidebarTabId
) )
@@ -120,6 +133,21 @@ const sidebarStateKey = computed(() => {
// When no tab is active, use a default key to maintain state // When no tab is active, use a default key to maintain state
return activeSidebarTabId.value ?? 'default-sidebar' return activeSidebarTabId.value ?? 'default-sidebar'
}) })
/**
* Force refresh the splitter when right panel visibility changes to recalculate the width
*/
const splitterRefreshKey = computed(() => {
return rightSidePanelVisible.value
? 'main-splitter-with-right-panel'
: 'main-splitter'
})
// Gutter visibility should be controlled by CSS targeting specific gutters
const getSplitterGutterClasses = computed(() => {
// Empty string - let individual gutter styles handle visibility
return ''
})
</script> </script>
<style scoped> <style scoped>
@@ -135,10 +163,20 @@ const sidebarStateKey = computed(() => {
background-color: var(--p-primary-color); background-color: var(--p-primary-color);
} }
/* Hide sidebar gutter when sidebar is not visible */
:deep(.side-bar-panel[style*='display: none'] + .p-splitter-gutter),
:deep(.p-splitter-gutter + .side-bar-panel[style*='display: none']) {
display: none;
}
.side-bar-panel { .side-bar-panel {
background-color: var(--bg-color); background-color: var(--bg-color);
} }
.right-side-panel {
background-color: var(--bg-color);
}
.bottom-panel { .bottom-panel {
background-color: var(--comfy-menu-bg); background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color); border: 1px solid var(--p-panel-border-color);

View File

@@ -1,5 +1,10 @@
<template> <template>
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1"> <div
v-if="!workspaceStore.focusMode"
class="ml-1 flex gap-x-0.5 pt-1"
@mouseenter="isTopMenuHovered = true"
@mouseleave="isTopMenuHovered = false"
>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<SubgraphBreadcrumb /> <SubgraphBreadcrumb />
</div> </div>
@@ -39,8 +44,25 @@
</IconButton> </IconButton>
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" /> <CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" /> <LoginButton v-else-if="isDesktop" />
<IconButton
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 transition-colors duration-200 ease-in-out hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-pressed="isRightSidePanelOpen"
:aria-label="t('rightSidePanel.togglePanel')"
@click="toggleRightSidePanel"
>
<i
class="icon-[lucide--panel-right] block size-4 text-muted-foreground"
/>
</IconButton>
</div> </div>
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" /> <QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:menu-hovered="isTopMenuHovered"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -60,15 +82,18 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { buildTooltipConfig } from '@/composables/useTooltipConfig' import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { useQueueStore } from '@/stores/queueStore' import { useQueueStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil' import { isElectron } from '@/utils/envUtil'
const workspaceStore = useWorkspaceStore() const workspaceStore = useWorkspaceStore()
const rightSidePanelStore = useRightSidePanelStore()
const { isLoggedIn } = useCurrentUser() const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron() const isDesktop = isElectron()
const { t } = useI18n() const { t } = useI18n()
const isQueueOverlayExpanded = ref(false) const isQueueOverlayExpanded = ref(false)
const queueStore = useQueueStore() const queueStore = useQueueStore()
const isTopMenuHovered = ref(false)
const queuedCount = computed(() => queueStore.pendingTasks.length) const queuedCount = computed(() => queueStore.pendingTasks.length)
const queueHistoryTooltipConfig = computed(() => const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory')) buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
@@ -79,6 +104,16 @@ const queueHistoryButtonBackgroundClass = computed(() =>
: 'bg-secondary-background' : 'bg-secondary-background'
) )
// Right side panel toggle
const isRightSidePanelOpen = computed(() => rightSidePanelStore.isOpen)
const rightSidePanelTooltipConfig = computed(() =>
buildTooltipConfig(t('rightSidePanel.togglePanel'))
)
const toggleRightSidePanel = () => {
rightSidePanelStore.togglePanel()
}
// Maintain support for legacy topbar elements attached by custom scripts // Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>() const legacyCommandsContainerRef = ref<HTMLElement>()
onMounted(() => { onMounted(() => {

View File

@@ -10,7 +10,7 @@
</div> </div>
<Panel <Panel
class="pointer-events-auto z-1010" class="pointer-events-auto"
:style="style" :style="style"
:class="panelClass" :class="panelClass"
:pt="{ :pt="{
@@ -66,12 +66,7 @@ const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
x: 0, x: 0,
y: 0 y: 0
}) })
const { const { x, y, style, isDragging } = useDraggable(panelRef, {
x,
y,
style: style,
isDragging
} = useDraggable(panelRef, {
initialValue: { x: 0, y: 0 }, initialValue: { x: 0, y: 0 },
handle: dragHandleRef, handle: dragHandleRef,
containerElement: document.body, containerElement: document.body,
@@ -257,7 +252,7 @@ watch(isDragging, (dragging) => {
}) })
const actionbarClass = computed(() => const actionbarClass = computed(() =>
cn( cn(
'w-[265px] border-dashed border-blue-500 opacity-80', 'w-[200px] border-dashed border-blue-500 opacity-80',
'm-1.5 flex items-center justify-center self-stretch', 'm-1.5 flex items-center justify-center self-stretch',
'rounded-md before:w-50 before:-ml-50 before:h-full', 'rounded-md before:w-50 before:-ml-50 before:h-full',
'pointer-events-auto', 'pointer-events-auto',
@@ -267,7 +262,7 @@ const actionbarClass = computed(() =>
) )
const panelClass = computed(() => const panelClass = computed(() =>
cn( cn(
'actionbar pointer-events-auto z1000', 'actionbar pointer-events-auto z-1300',
isDragging.value && 'select-none pointer-events-none', isDragging.value && 'select-none pointer-events-none',
isDocked.value isDocked.value
? 'p-0 static mr-2 border-none bg-transparent' ? 'p-0 static mr-2 border-none bg-transparent'

View File

@@ -10,7 +10,6 @@
severity="primary" severity="primary"
size="small" size="small"
:model="queueModeMenuItems" :model="queueModeMenuItems"
:disabled="hasMissingNodes"
data-testid="queue-button" data-testid="queue-button"
@click="queuePrompt" @click="queuePrompt"
> >
@@ -45,17 +44,22 @@ import { useI18n } from 'vue-i18n'
import { isCloud } from '@/platform/distribution/types' import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry' import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useQueueSettingsStore } from '@/stores/queueStore' import { useQueueSettingsStore } from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
import BatchCountEdit from '../BatchCountEdit.vue' import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore() const workspaceStore = useWorkspaceStore()
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore()) const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { hasMissingNodes } = useMissingNodes() const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)
const { t } = useI18n() const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => { const queueModeMenuItemLookup = computed(() => {

View File

@@ -64,11 +64,13 @@ import {
ComfyWorkflow, ComfyWorkflow,
useWorkflowStore useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore' } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { appendJsonExt } from '@/utils/formatUtil' import { appendJsonExt } from '@/utils/formatUtil'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
interface Props { interface Props {
item: MenuItem item: MenuItem
@@ -79,7 +81,10 @@ const props = withDefaults(defineProps<Props>(), {
isActive: false isActive: false
}) })
const { hasMissingNodes } = useMissingNodes() const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)
const { t } = useI18n() const { t } = useI18n()
const menu = ref<InstanceType<typeof Menu> & MenuState>() const menu = ref<InstanceType<typeof Menu> & MenuState>()

View File

@@ -24,7 +24,7 @@ import {
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
interface IconButtonProps extends BaseButtonProps { interface IconButtonProps extends BaseButtonProps {
onClick: (event: Event) => void onClick?: (event: MouseEvent) => void
} }
defineOptions({ defineOptions({

View File

@@ -1,17 +1,15 @@
<template> <template>
<div :class="iconGroupClasses"> <div
:class="
cn(
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg bg-secondary-background shadow-sm transition-all duration-200 cursor-pointer'
)
"
>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
const iconGroupClasses = cn(
'flex justify-center items-center shrink-0',
'outline-hidden border-none p-0 rounded-lg',
'bg-secondary-background shadow-sm',
'transition-all duration-200',
'cursor-pointer'
)
</script> </script>

View File

@@ -47,7 +47,7 @@ const {
} = defineProps<IconTextButtonProps>() } = defineProps<IconTextButtonProps>()
const buttonStyle = computed(() => { const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} justify-start! gap-2` const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size) const sizeClasses = getButtonSizeClasses(size)
const typeClasses = border const typeClasses = border
? getBorderButtonTypeClasses(type) ? getBorderButtonTypeClasses(type)

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="relative inline-flex items-center"> <div class="relative inline-flex items-center">
<IconButton :size="size" :type="type" @click="toggle"> <IconButton :size="size" :type="type" @click="popover?.toggle">
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" /> <i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
<i v-else class="icon-[lucide--more-vertical] text-sm" /> <i v-else class="icon-[lucide--more-vertical] text-sm" />
</IconButton> </IconButton>
@@ -25,8 +25,18 @@
) )
} }
}" }"
@show="$emit('menuOpened')" @show="
@hide="$emit('menuClosed')" () => {
isOpen = true
$emit('menuOpened')
}
"
@hide="
() => {
isOpen = false
$emit('menuClosed')
}
"
> >
<div class="flex min-w-40 flex-col gap-2 p-2"> <div class="flex min-w-40 flex-col gap-2 p-2">
<slot :close="hide" /> <slot :close="hide" />
@@ -48,8 +58,6 @@ interface MoreButtonProps extends BaseButtonProps {
isVertical?: boolean isVertical?: boolean
} }
const popover = ref<InstanceType<typeof Popover>>()
const { const {
size = 'md', size = 'md',
type = 'secondary', type = 'secondary',
@@ -61,11 +69,15 @@ defineEmits<{
menuClosed: [] menuClosed: []
}>() }>()
const toggle = (event: Event) => { const isOpen = ref(false)
popover.value?.toggle(event) const popover = ref<InstanceType<typeof Popover>>()
}
const hide = () => { function hide() {
popover.value?.hide() popover.value?.hide()
} }
defineExpose({
hide,
isOpen
})
</script> </script>

View File

@@ -18,8 +18,8 @@
...inputAttrs ...inputAttrs
} }
}" }"
@keyup.enter="blurInputElement" @keyup.enter.capture.stop="blurInputElement"
@keyup.escape="cancelEditing" @keyup.escape.stop="cancelEditing"
@click.stop @click.stop
@pointerdown.stop.capture @pointerdown.stop.capture
@pointermove.stop.capture @pointermove.stop.capture
@@ -38,7 +38,7 @@ const {
} = defineProps<{ } = defineProps<{
modelValue: string modelValue: string
isEditing?: boolean isEditing?: boolean
inputAttrs?: Record<string, any> inputAttrs?: Record<string, string>
}>() }>()
const emit = defineEmits(['update:modelValue', 'edit', 'cancel']) const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])

View File

@@ -9,29 +9,31 @@
<div class="font-medium"> <div class="font-medium">
{{ col.header }} {{ col.header }}
</div> </div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div> <div>{{ getDisplayValue(col) }}</div>
</template> </template>
</div> </div>
</div> </div>
<Divider /> <template v-if="hasDevices">
<Divider />
<div> <div>
<h2 class="mb-4 text-2xl font-semibold"> <h2 class="mb-4 text-2xl font-semibold">
{{ $t('g.devices') }} {{ $t('g.devices') }}
</h2> </h2>
<TabView v-if="props.stats.devices.length > 1"> <TabView v-if="props.stats.devices.length > 1">
<TabPanel <TabPanel
v-for="device in props.stats.devices" v-for="device in props.stats.devices"
:key="device.index" :key="device.index"
:header="device.name" :header="device.name"
:value="device.index" :value="device.index"
> >
<DeviceInfo :device="device" /> <DeviceInfo :device="device" />
</TabPanel> </TabPanel>
</TabView> </TabView>
<DeviceInfo v-else :device="props.stats.devices[0]" /> <DeviceInfo v-else :device="props.stats.devices[0]" />
</div> </div>
</template>
</div> </div>
</template> </template>
@@ -42,8 +44,9 @@ import TabView from 'primevue/tabview'
import { computed } from 'vue' import { computed } from 'vue'
import DeviceInfo from '@/components/common/DeviceInfo.vue' import DeviceInfo from '@/components/common/DeviceInfo.vue'
import { isCloud } from '@/platform/distribution/types'
import type { SystemStats } from '@/schemas/apiSchema' import type { SystemStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil' import { formatCommitHash, formatSize } from '@/utils/formatUtil'
const props = defineProps<{ const props = defineProps<{
stats: SystemStats stats: SystemStats
@@ -54,20 +57,53 @@ const systemInfo = computed(() => ({
argv: props.stats.system.argv.join(' ') argv: props.stats.system.argv.join(' ')
})) }))
const systemColumns: { field: keyof SystemStats['system']; header: string }[] = const hasDevices = computed(() => props.stats.devices.length > 0)
[
{ field: 'os', header: 'OS' },
{ field: 'python_version', header: 'Python Version' },
{ field: 'embedded_python', header: 'Embedded Python' },
{ field: 'pytorch_version', header: 'Pytorch Version' },
{ field: 'argv', header: 'Arguments' },
{ field: 'ram_total', header: 'RAM Total' },
{ field: 'ram_free', header: 'RAM Free' }
]
const formatValue = (value: any, field: string) => { type SystemInfoKey = keyof SystemStats['system']
if (['ram_total', 'ram_free'].includes(field)) {
return formatSize(value) type ColumnDef = {
field: SystemInfoKey
header: string
format?: (value: string) => string
formatNumber?: (value: number) => string
}
/** Columns for local distribution */
const localColumns: ColumnDef[] = [
{ field: 'os', header: 'OS' },
{ field: 'python_version', header: 'Python Version' },
{ field: 'embedded_python', header: 'Embedded Python' },
{ field: 'pytorch_version', header: 'Pytorch Version' },
{ field: 'argv', header: 'Arguments' },
{ field: 'ram_total', header: 'RAM Total', formatNumber: formatSize },
{ field: 'ram_free', header: 'RAM Free', formatNumber: formatSize }
]
/** Columns for cloud distribution */
const cloudColumns: ColumnDef[] = [
{ field: 'cloud_version', header: 'Cloud Version' },
{
field: 'comfyui_version',
header: 'ComfyUI Version',
format: formatCommitHash
},
{
field: 'comfyui_frontend_version',
header: 'Frontend Version',
format: formatCommitHash
},
{ field: 'workflow_templates_version', header: 'Templates Version' }
]
const systemColumns = computed(() => (isCloud ? cloudColumns : localColumns))
const getDisplayValue = (column: ColumnDef) => {
const value = systemInfo.value[column.field]
if (column.formatNumber && typeof value === 'number') {
return column.formatNumber(value)
}
if (column.format && typeof value === 'string') {
return column.format(value)
} }
return value return value
} }

View File

@@ -35,7 +35,6 @@ import { ValidationState } from '@/utils/validationUtil'
const props = defineProps<{ const props = defineProps<{
modelValue: string modelValue: string
validateUrlFn?: (url: string) => Promise<boolean> validateUrlFn?: (url: string) => Promise<boolean>
disableValidation?: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -102,8 +101,6 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
} }
const validateUrl = async (value: string) => { const validateUrl = async (value: string) => {
if (props.disableValidation) return
if (validationState.value === ValidationState.LOADING) return if (validationState.value === ValidationState.LOADING) return
const url = cleanInput(value) const url = cleanInput(value)

View File

@@ -92,7 +92,7 @@
class="w-62.5" class="w-62.5"
> >
<template #icon> <template #icon>
<i class="icon-[lucide--arrow-up-down]" /> <i class="icon-[lucide--arrow-up-down] text-muted-foreground" />
</template> </template>
</SingleSelect> </SingleSelect>
</div> </div>

View File

@@ -14,6 +14,7 @@
<component <component
:is="item.headerComponent" :is="item.headerComponent"
v-if="item.headerComponent" v-if="item.headerComponent"
v-bind="item.headerProps"
:id="item.key" :id="item.key"
/> />
<h3 v-else :id="item.key"> <h3 v-else :id="item.key">

View File

@@ -0,0 +1,19 @@
<template>
<div
class="flex flex-col px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
>
<p v-if="promptTextReal">
{{ promptTextReal }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
const { promptText } = defineProps<{
promptText?: MaybeRefOrGetter<string>
}>()
const promptTextReal = computed(() => toValue(promptText))
</script>

View File

@@ -0,0 +1,43 @@
<template>
<section class="w-full flex gap-2 justify-end px-2 pb-2">
<TextButton
:label="cancelTextX"
:disabled
type="transparent"
autofocus
@click="$emit('cancel')"
/>
<TextButton
:label="confirmTextX"
:disabled
type="transparent"
:class="confirmClass"
@click="$emit('confirm')"
/>
</section>
</template>
<script setup lang="ts">
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
import TextButton from '@/components/button/TextButton.vue'
const { t } = useI18n()
const { cancelText, confirmText, confirmClass, optionsDisabled } = defineProps<{
cancelText?: string
confirmText?: string
confirmClass?: string
optionsDisabled?: MaybeRefOrGetter<boolean>
}>()
defineEmits<{
cancel: []
confirm: []
}>()
const confirmTextX = computed(() => confirmText || t('g.confirm'))
const cancelTextX = computed(() => cancelText || t('g.cancel'))
const disabled = computed(() => toValue(optionsDisabled))
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div
class="flex items-center gap-2 p-4 font-bold text-sm text-base-foreground font-inter"
>
<span v-if="title" class="flex-auto">{{ title }}</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
title?: string
}>()
</script>

View File

@@ -0,0 +1,31 @@
import ConfirmBody from '@/components/dialog/confirm/ConfirmBody.vue'
import ConfirmFooter from '@/components/dialog/confirm/ConfirmFooter.vue'
import ConfirmHeader from '@/components/dialog/confirm/ConfirmHeader.vue'
import { useDialogStore } from '@/stores/dialogStore'
import type { ComponentAttrs } from 'vue-component-type-helpers'
interface ConfirmDialogOptions {
headerProps?: ComponentAttrs<typeof ConfirmHeader>
props?: ComponentAttrs<typeof ConfirmBody>
footerProps?: ComponentAttrs<typeof ConfirmFooter>
}
export function showConfirmDialog(options: ConfirmDialogOptions = {}) {
const dialogStore = useDialogStore()
const { headerProps, props, footerProps } = options
return dialogStore.showDialog({
headerComponent: ConfirmHeader,
component: ConfirmBody,
footerComponent: ConfirmFooter,
headerProps,
props,
footerProps,
dialogComponentProps: {
pt: {
header: 'py-0! px-0!',
content: 'p-0!',
footer: 'p-0!'
}
}
})
}

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