Compare commits

...

23 Commits

Author SHA1 Message Date
snomiao
6606060802 [feat] Add automatic keybinding migration from event.key to event.code
- Implement keybindingMigration utility to convert old format to new format
- Add migration logic in registerUserKeybindings() for backward compatibility
- Update tests to use event.code format in KeyboardEvent creation
- Add comprehensive migration test suite
- Automatically migrate user keybindings on first load after update

This ensures existing user custom keybindings continue working after switching
from event.key to event.code for layout-independent shortcut handling.

Relates to #5252
2025-10-16 16:21:42 +00:00
snomiao
06ec45c1c5 [docs] Add comprehensive keyboard shortcut system research and analysis
- Document current implementation using event.key
- Analyze problems with non-English keyboard layouts
- Research event.key vs event.code technical differences
- Propose solution using event.code for layout independence
- Detail migration strategy for backward compatibility
- Include testing strategy and implementation phases

Relates to #5252
2025-10-16 16:14:33 +00:00
snomiao
8c05ac8eb6 Merge branch 'main' into 5252-keyboard-shortcuts-dont-work-on-non-english-non-latin-keyboard-layouts 2025-10-02 23:33:37 +09:00
filtered
706ff953de Adopt catalog references for all matching dependencies (#5889)
## Summary

Converts 81 package dependencies to use pnpm catalog references for
centralized version management.

## Changes

- **What**: All dependencies matching catalog versions now use
`catalog:` references
- **Dependencies**: axios catalog entry corrected from ^1.11.0 to ^1.8.2
- Also removes a redundant knip config line

### Some things that shouldn't matter

- TypeScript was updated from ^5.4.5 to catalog reference (^5.9.2), but
the project was already resolving to 5.9.2 so this has no practical
impact.
- axios catalog version was corrected from ^1.11.0 back to ^1.8.2 to
match the main package version.
- Autoformatted LGraphNode.ts from another PR by running pnpm lint.
Oops.
2025-10-01 23:54:01 -07:00
Alexander Piskun
9c97fb359d feat(auth): Allow SSO login only for whitelisted addresses (localhost) (#5815)
## Summary

Hide Google/GitHub SSO login options when the UI is accessed from
**non‑local** addresses.
This PR also adds a **static whitelist** (editable in code) so we can
allow additional hosts if needed.

Default whitelisted addresses:

1. `localhost` and any subdomain: `*.localhost`
2. IPv4 loopback `127.0.0.0/8` (e.g., `127.x.y.z`)
4. IPv6 loopback `::1` (including equivalent textual forms such as
`::0001`)

## Changes

- **What**: 
* Add `src/utils/hostWhitelist.ts` with `normalizeHost` and
`isHostWhitelisted` helpers.
  * Update `SignInContent.vue` to **hide** SSO options when
`isHostWhitelisted(normalizeHost(window.location.hostname))` returns
`false`.
- **Breaking**:
* Users accessing from Runpod or other previously allowed **non‑local**
hosts will **lose** SSO login options.
If we need to keep SSO there, we should add those hosts to the whitelist
in `hostWhitelist.ts`.

## Review Focus

1. Verify that logging in from local addresses (`localhost`,
`*.localhost`, `127.0.0.1`, `::1`) **does not change** the current
behavior: SSO is visible.
2. Verify that from a **non‑local** address, SSO options are **not**
displayed.

## Screenshots (if applicable)

UI opened from `192.168.2.109` address:

<img width="500" height="990" alt="Screenshot From 2025-09-27 13-22-15"
src="https://github.com/user-attachments/assets/c97b10a1-b069-43e4-a26b-a71eeb228a51"
/>

UI opened from default `127.0.0.1` address(nothing changed):

<img width="462" height="955" alt="Screenshot From 2025-09-27 13-35-27"
src="https://github.com/user-attachments/assets/bb2bf21c-dc8d-49cb-b48e-8fc6e408023c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5815-feat-auth-Allow-SSO-login-only-for-whitelisted-addresses-localhost-27b6d73d365081ccbe84c034cf8e416d)
by [Unito](https://www.unito.io)
2025-10-01 23:09:11 -07:00
snomiao
c662c77305 Add Playwright composite action to reduce workflow duplication (#5754)
This PR introduces a reusable composite action for Playwright setup to
reduce duplication across workflows.

## Changes
- Created `.github/actions/setup-playwright/action.yml` composite action
that:
  - Detects or uses provided Playwright version
  - Caches Playwright browsers with intelligent cache keys
  - Installs browsers only when cache miss occurs
  - Installs OS dependencies when cache hit occurs
  
## Technical Details
- **Important:** The composite action requires `shell: bash` for all
`run` steps as per [GitHub Actions requirements for composite
actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action#creating-an-action-metadata-file).
This is a mandatory field for composite actions, unlike regular workflow
steps.
- Updated workflow paths to account for repository checkout locations
(some workflows checkout to subdirectories like `ComfyUI_frontend/`)
- Uses conditional caching to avoid redundant browser installations

## Benefits
- Reduces code duplication across 6 workflow files
- Centralizes Playwright caching logic  
- Consistent browser setup across all workflows
- Easier maintenance and updates
- Faster CI runs through intelligent caching

## Affected Workflows
- `.github/workflows/test-ui.yaml` (2 uses)
- `.github/workflows/i18n-custom-nodes.yaml`
- `.github/workflows/i18n-node-defs.yaml`
- `.github/workflows/i18n.yaml`
- `.github/workflows/test-browser-exp.yaml`

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-02 14:20:48 +09:00
Marcel Petrick
d0e81cdd33 fix(docs): correct typos in comments and strings found during code view (#5880)
Non-functional changes only:
- Fixed minor spelling mistakes in comments
- Corrected typos in user-facing strings
- No variables, logic, or functional code was modified.

Signed-off-by: Marcel Petrick <mail@marcelpetrick.it>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5880-fix-docs-correct-typos-in-comments-and-strings-found-during-code-view-27f6d73d3650815db62af6115991304a)
by [Unito](https://www.unito.io)

---------

Signed-off-by: Marcel Petrick <mail@marcelpetrick.it>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-10-01 18:35:38 -07:00
Johnpaul Chiwetelu
01b3aeae68 Prune console.log() (#5867)
Introduce a no-console rule in ESLint configuration and remove existing
console log statements throughout the codebase, replacing some with
warnings or comments.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5867-Prune-console-log-27e6d73d365081bcbad8c36cfb5b258c)
by [Unito](https://www.unito.io)
2025-10-01 18:34:58 -07:00
Terry Jia
20731fe3f0 pass nodeId to widget component (#5853)
## Summary

pass nodeId to widget component, which is a prerequirist for
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5853-pass-nodeData-to-widget-component-27e6d73d3650811e9e48ceb7c3a00545)
by [Unito](https://www.unito.io)
2025-10-01 21:31:49 -04:00
Benjamin Lu
bf9659fb2c ci: fork-safe checkout for lint workflow (#5887)
Use conditional ref to support forks and avoid checkout failures.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5887-ci-fork-safe-checkout-for-lint-workflow-2806d73d36508139b9effa6d18e1cadb)
by [Unito](https://www.unito.io)
2025-10-01 17:26:21 -07:00
Alexander Brown
e9352f613e fix: Remove extra arguments to checkout in favor of the GitHub defaults (#5883)
## Summary

Should allow this action to run on fork PRs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5883-fix-Remove-extra-arguments-to-checkout-in-favor-of-the-GitHub-defaults-27f6d73d365081a780f7cf4a5a62c368)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-01 16:47:34 -07:00
Benjamin Lu
d76b1abc46 Rename workflows to match workflow names (#5866)
## Summary
- rename each GitHub Actions workflow file so its filename matches the
workflow `name` value for easier discovery

------
https://chatgpt.com/codex/tasks/task_e_68dc213f0a808330869ed73c27858eb9

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5866-Rename-workflows-to-match-workflow-names-27e6d73d36508103bca7ea2746f73a3b)
by [Unito](https://www.unito.io)
2025-10-01 13:27:14 -07:00
Christian Byrne
fd757027a9 [ci] allow Claude review even when Playwright and Vitest checks have failed (#5882)
Currently the claude review action will be skipped if any tests have
failed. This is not really necessary, it will be more efficient to allow
claude to review while still waiting for tests.

This accounts for scenario where there is an expected visual baseline
change, but the PR author doesn't want to regenerate baselines until
everything is approved and ready to merge (as generating right away
before you know whether changes will be requested can be a hassle).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5882-ci-allow-Claude-review-even-when-Playwright-and-Vitest-checks-have-failed-27f6d73d365081ccbcdaff7104edc2fd)
by [Unito](https://www.unito.io)
2025-10-01 13:16:14 -07:00
Benjamin Lu
1efc2233c5 Fix Claude review workflow checkout ref (#5874)
By default, in this case the checkout action will checkout to github's
temporary merge base ref, which may include changes from the base branch
when the base branch moves.

This happened in this review:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5866#pullrequestreview-3287366541

To prevent this, the PR's HEAD SHA was specified to be used
specifically, keeping claude's reviews only looking at that PR's branch.
2025-10-01 12:52:03 -07:00
Comfy Org PR Bot
557b2fdb0e 1.28.4 (#5875)
Patch version increment to 1.28.4

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5875-1-28-4-27f6d73d3650819f984ac83c971197b0)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com>
2025-09-30 23:40:53 -05:00
snomiao
692666ff63 test: Update tests to use event.code format for keyboard shortcuts
- Update keybinding tests to use event.code instead of event.key
- Add forwarding test for keybinding service
- Align tests with the fix for non-English keyboard layouts

This ensures tests match the application's use of event.code for
language-independent keyboard shortcut handling.
2025-10-01 03:26:24 +00:00
snomiao
82a603ef03 [fix] Remove new forwarding test file 2025-10-01 03:23:06 +00:00
snomiao
14255b3512 [fix] Revert test changes - tests should use event.key format 2025-10-01 03:23:05 +00:00
snomiao
e5adc840fe [fix] Update tests to use event.code format for keyboard shortcuts 2025-10-01 03:23:05 +00:00
snomiao
0f44b5ea58 [fix] Change keyboard event handling from event.key to event.code
This fixes keyboard shortcuts not working on non-English (non-Latin) keyboard layouts.

Changes:
- KeyComboImpl.fromEvent() now uses event.code instead of event.key
- Updated isModifier check to use key codes for modifier keys
- Added getDisplayKey() method to convert key codes to readable names
- Updated Escape key handling in keybindingService to use event.code
- Updated KeybindingPanel captureKeybinding to use event.code
- Migrated all core keybindings from event.key to event.code format
- Fixed tests to mock event.code instead of event.key

Fixes #5252

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 03:23:05 +00:00
filtered
7fd2dc304a pnpm catalog for centralized dependency management (#5871)
## Summary

Adds pnpm catalog to centralize dependency versions across the monorepo.

## Changes

- **What**: Consolidates dependencies into single default catalog with
[`prefer` mode](https://pnpm.io/catalogs#catalog-mode)
- **Dependencies**: No new dependencies - reorganizes existing version
management

## Review Focus

The catalog uses `prefer` mode which automatically uses catalog versions
for packages already in the catalog, falling back to direct versions for
packages not yet cataloged.

### Example Usage

When adding a dependency already in the catalog:
```bash
pnpm add vue
```

This automatically uses `"vue": "catalog:"` in `package.json` instead of
a direct version.
2025-09-30 20:05:41 -07:00
Christian Byrne
e8de474d42 [docs] Change grammar in docs (#5868)
## Summary

Updates docs.

This PR is mostly for testing some n8n automation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5868-docs-Change-grammar-in-docs-27e6d73d3650812691d2dcf71efe7bf4)
by [Unito](https://www.unito.io)
2025-09-30 19:10:20 -07:00
Christian Byrne
3f291672d4 fix progress state on Vue nodes in subgraphs (#5842)
## Summary

Fixes two errors with subgraph progress states:

1. Nodes inside subgraphs were not having progress state shown
2. Subgraph nodes (outer representation) themselves did not have a
visible progress state

1 is fixed by using locator IDs instead of local node IDs.

2 is fixed by ensuring the subgraph title button does not wrap to a
newline and thus block the progress bar under the node header.

## Changes

- **What**: Updated `useNodeExecutionState` composable to use
`nodeLocatorId` for tracking execution state across subgraph boundaries
- **What**: Modified NodeHeader layout to fix subgraph enter button
positioning with proper flexbox gap

## Review Focus

Execution state tracking accuracy for nested subgraph nodes and
NodeHeader layout consistency across different node types.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5842-fix-progress-state-on-Vue-nodes-in-subgraphs-27c6d73d365081cb8335c8bb5dbd74f7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-30 15:01:18 -07:00
113 changed files with 2931 additions and 921 deletions

View File

@@ -0,0 +1,31 @@
name: Setup Playwright
description: Cache and install Playwright browsers with dependencies
runs:
using: composite
steps:
- name: Detect Playwright version
id: detect-version
shell: bash
working-directory: ComfyUI_frontend
run: |
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
- name: Cache Playwright Browsers
uses: actions/cache@v4
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'
key: ${{ runner.os }}-playwright-browsers-${{ steps.detect-version.outputs.playwright-version }}
- name: Install Playwright Browsers
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
shell: bash
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers (operating system dependencies)
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
shell: bash
run: pnpm exec playwright install-deps
working-directory: ComfyUI_frontend

View File

@@ -29,11 +29,9 @@ jobs:
- name: Check if we should proceed
id: check-status
run: |
# Get all check runs for this commit
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format|test|playwright-tests")) | {name, conclusion}')
# Check if any required checks failed
if echo "$CHECK_RUNS" | grep -q '"conclusion": "failure"'; then
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
echo "Some CI checks failed - skipping Claude review"
echo "proceed=false" >> $GITHUB_OUTPUT
else
@@ -53,6 +51,7 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -86,4 +85,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
REPOSITORY: ${{ github.repository }}
REPOSITORY: ${{ github.repository }}

View File

@@ -15,9 +15,7 @@ jobs:
- name: Checkout PR
uses: actions/checkout@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -102,4 +100,4 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\npnpm prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\npnpm lint:fix\npnpm format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
})
})

View File

@@ -12,7 +12,6 @@ jobs:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v5
@@ -65,12 +64,6 @@ jobs:
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Playwright Version
id: playwright-version
run: |
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
working-directory: ComfyUI_frontend
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
@@ -123,22 +116,8 @@ jobs:
working-directory: ComfyUI
- name: Cache Playwright Browsers
uses: actions/cache@v4
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
- name: Install Playwright Browsers
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers (operating system dependencies)
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps
working-directory: ComfyUI_frontend
- name: Setup Playwright
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
- name: Start ComfyUI server
run: |
@@ -202,22 +181,8 @@ jobs:
pip install wait-for-it
working-directory: ComfyUI
- name: Cache Playwright Browsers
uses: actions/cache@v4
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
- name: Install Playwright Browsers
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers (operating system dependencies)
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps
working-directory: ComfyUI_frontend
- name: Setup Playwright
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
- name: Start ComfyUI server
run: |

View File

@@ -77,9 +77,8 @@ jobs:
python main.py --cpu --multi-user &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Setup Playwright
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.

View File

@@ -26,16 +26,8 @@ jobs:
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
i18n-tools-cache-${{ runner.os }}-
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.

View File

@@ -13,11 +13,9 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.

View File

@@ -14,16 +14,8 @@ jobs:
uses: actions/checkout@v5
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests and update snapshots
id: playwright-tests
run: pnpm exec playwright test --update-snapshots

1
.npmrc
View File

@@ -1 +1,2 @@
ignore-workspace-root-check=true
catalog-mode=prefer

View File

@@ -16,7 +16,7 @@ Without this flag, parallel tests will conflict and fail randomly.
### ComfyUI devtools
ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
ComfyUI_devtools is included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
For local development, copy the devtools files to your ComfyUI installation:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

1039
docs/keybinding.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -90,6 +90,7 @@ export default defineConfig([
}
],
'unused-imports/no-unused-imports': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
'vue/no-v-html': 'off',
// Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'],
@@ -207,5 +208,11 @@ export default defineConfig([
}
]
}
},
{
files: ['**/*.{test,spec,stories}.ts', '**/*.stories.vue'],
rules: {
'no-console': 'off'
}
}
])

View File

@@ -20,7 +20,6 @@ const config: KnipConfig = {
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
},
'packages/registry-types': {
entry: ['src/comfyRegistryTypes.ts'],
project: ['src/**/*.{js,ts}']
}
},

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.28.3",
"version": "1.28.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -42,82 +42,82 @@
"devtools:pycheck": "python3 -m compileall -q tools/devtools"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
"@lobehub/i18n-cli": "^1.25.1",
"@nx/eslint": "21.4.1",
"@nx/playwright": "21.4.1",
"@nx/storybook": "21.4.1",
"@nx/vite": "21.4.1",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
"@storybook/addon-docs": "^9.1.1",
"@storybook/vue3": "^9.1.1",
"@storybook/vue3-vite": "^9.1.1",
"@tailwindcss/vite": "^4.1.12",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/fs-extra": "^11.0.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.0.0",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-storybook": "^9.1.6",
"eslint-plugin-unused-imports": "^4.2.0",
"eslint-plugin-vue": "^10.4.0",
"@eslint/js": "catalog:",
"@intlify/eslint-plugin-vue-i18n": "catalog:",
"@lobehub/i18n-cli": "catalog:",
"@nx/eslint": "catalog:",
"@nx/playwright": "catalog:",
"@nx/storybook": "catalog:",
"@nx/vite": "catalog:",
"@pinia/testing": "catalog:",
"@playwright/test": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",
"@tailwindcss/vite": "catalog:",
"@trivago/prettier-plugin-sort-imports": "catalog:",
"@types/fs-extra": "catalog:",
"@types/jsdom": "catalog:",
"@types/node": "catalog:",
"@types/semver": "catalog:",
"@types/three": "catalog:",
"@vitejs/plugin-vue": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"@vue/test-utils": "catalog:",
"eslint": "catalog:",
"eslint-config-prettier": "catalog:",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
"eslint-plugin-vue": "catalog:",
"fs-extra": "^11.2.0",
"globals": "^15.9.0",
"happy-dom": "^15.11.0",
"husky": "^9.0.11",
"jiti": "2.4.2",
"jsdom": "^26.1.0",
"knip": "^5.62.0",
"lint-staged": "^15.2.7",
"nx": "21.4.1",
"prettier": "^3.3.2",
"storybook": "^9.1.6",
"tailwindcss": "^4.1.12",
"tailwindcss-primeui": "^0.6.1",
"tsx": "^4.15.6",
"tw-animate-css": "^1.3.8",
"typescript": "^5.4.5",
"typescript-eslint": "^8.44.0",
"unplugin-icons": "^0.22.0",
"unplugin-vue-components": "^0.28.0",
"globals": "catalog:",
"happy-dom": "catalog:",
"husky": "catalog:",
"jiti": "catalog:",
"jsdom": "catalog:",
"knip": "catalog:",
"lint-staged": "catalog:",
"nx": "catalog:",
"prettier": "catalog:",
"storybook": "catalog:",
"tailwindcss": "catalog:",
"tailwindcss-primeui": "catalog:",
"tsx": "catalog:",
"tw-animate-css": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:",
"unplugin-icons": "catalog:",
"unplugin-vue-components": "catalog:",
"uuid": "^11.1.0",
"vite": "^5.4.19",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^3.2.4",
"vue-component-type-helpers": "^3.0.7",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.7",
"vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vite-plugin-html": "catalog:",
"vite-plugin-vue-devtools": "catalog:",
"vitest": "catalog:",
"vue-component-type-helpers": "catalog:",
"vue-eslint-parser": "catalog:",
"vue-tsc": "catalog:",
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
"zod-to-json-schema": "catalog:"
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@alloc/quick-lru": "catalog:",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "0.4.73-0",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@iconify/json": "^2.2.380",
"@primeuix/forms": "0.0.2",
"@primeuix/styled": "0.3.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "^4.2.5",
"@primevue/forms": "^4.2.5",
"@primevue/icons": "4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
"@iconify/json": "catalog:",
"@primeuix/forms": "catalog:",
"@primeuix/styled": "catalog:",
"@primeuix/utils": "catalog:",
"@primevue/core": "catalog:",
"@primevue/forms": "catalog:",
"@primevue/icons": "catalog:",
"@primevue/themes": "catalog:",
"@sentry/vue": "catalog:",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -125,39 +125,39 @@
"@tiptap/extension-table-header": "^2.10.4",
"@tiptap/extension-table-row": "^2.10.4",
"@tiptap/starter-kit": "^2.10.4",
"@vueuse/core": "^11.0.0",
"@vueuse/integrations": "^13.9.0",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0",
"algoliasearch": "^5.21.0",
"axios": "^1.8.2",
"algoliasearch": "catalog:",
"axios": "catalog:",
"chart.js": "^4.5.0",
"dompurify": "^3.2.5",
"dotenv": "^16.4.5",
"dotenv": "catalog:",
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"fast-glob": "^3.3.3",
"firebase": "^11.6.0",
"firebase": "catalog:",
"fuse.js": "^7.0.0",
"glob": "^11.0.3",
"jsondiffpatch": "^0.6.0",
"loglevel": "^1.9.2",
"marked": "^15.0.11",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"pinia": "catalog:",
"primeicons": "catalog:",
"primevue": "catalog:",
"reka-ui": "^2.5.0",
"semver": "^7.7.2",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.5.13",
"vue-i18n": "^9.14.3",
"vue-router": "^4.4.3",
"vuefire": "^3.2.1",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
"vue": "catalog:",
"vue-i18n": "catalog:",
"vue-router": "catalog:",
"vuefire": "catalog:",
"yjs": "catalog:",
"zod": "catalog:",
"zod-validation-error": "catalog:"
}
}

View File

@@ -4,10 +4,7 @@
"description": "Shared design system for ComfyUI Frontend",
"type": "module",
"exports": {
"./tailwind-config": {
"import": "./tailwind.config.ts",
"types": "./tailwind.config.ts"
},
"./tailwind-config": "./tailwind.config.ts",
"./css/*": "./src/css/*"
},
"scripts": {
@@ -20,12 +17,12 @@
]
},
"dependencies": {
"@iconify-json/lucide": "^1.1.178",
"@iconify/tailwind": "^1.1.3"
"@iconify-json/lucide": "catalog:",
"@iconify/tailwind": "catalog:"
},
"devDependencies": {
"tailwindcss": "^3.4.17",
"typescript": "^5.4.5"
"tailwindcss": "catalog:",
"typescript": "catalog:"
},
"packageManager": "pnpm@10.17.1"
}

View File

@@ -152,7 +152,7 @@
}
}
/* Everthing below here to be cleaned up over time. */
/* Everything below here to be cleaned up over time. */
body {
width: 100vw;

View File

@@ -1125,7 +1125,7 @@ export interface paths {
}
get?: never
put?: never
/** Create a new custom node using admin priviledge */
/** Create a new custom node using admin privilege */
post: operations['adminCreateNode']
delete?: never
options?: never
@@ -16383,7 +16383,7 @@ export interface operations {
}
}
responses: {
/** @description Webhook processed succesfully */
/** @description Webhook processed successfully */
200: {
headers: {
[name: string]: unknown

View File

@@ -14,9 +14,9 @@
"./networkUtil": "./src/networkUtil.ts"
},
"dependencies": {
"axios": "^1.11.0"
"axios": "catalog:"
},
"devDependencies": {
"typescript": "^5.9.2"
"typescript": "catalog:"
}
}

View File

@@ -6,10 +6,7 @@
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit"
@@ -25,6 +22,6 @@
"tailwind-merge": "^2.2.0"
},
"devDependencies": {
"typescript": "^5.4.5"
"typescript": "catalog:"
}
}
}

887
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,115 @@ packages:
- apps/**
- packages/**
catalog:
# Core frameworks
typescript: ^5.9.2
vue: ^3.5.13
# Build tools
'@nx/eslint': 21.4.1
'@nx/playwright': 21.4.1
'@nx/storybook': 21.4.1
'@nx/vite': 21.4.1
nx: 21.4.1
tsx: ^4.15.6
vite: ^5.4.19
'@vitejs/plugin-vue': ^5.1.4
'vite-plugin-dts': ^4.5.4
vue-tsc: ^3.0.7
# Testing
'happy-dom': ^15.11.0
jsdom: ^26.1.0
'@pinia/testing': ^0.1.5
'@playwright/test': ^1.52.0
'@vitest/coverage-v8': ^3.2.4
'@vitest/ui': ^3.0.0
vitest: ^3.2.4
'@vue/test-utils': ^2.4.6
# Linting & Formatting
'@eslint/js': ^9.35.0
eslint: ^9.34.0
'eslint-config-prettier': ^10.1.8
'eslint-plugin-prettier': ^5.5.4
'eslint-plugin-storybook': ^9.1.6
'eslint-plugin-unused-imports': ^4.2.0
'eslint-plugin-vue': ^10.4.0
globals: ^15.9.0
'@intlify/eslint-plugin-vue-i18n': ^4.1.0
prettier: ^3.3.2
'typescript-eslint': ^8.44.0
'vue-eslint-parser': ^10.2.0
# Vue ecosystem
'@sentry/vue': ^8.48.0
'@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0
'vite-plugin-html': ^3.2.2
'vite-plugin-vue-devtools': ^7.7.6
pinia: ^2.1.7
'vue-i18n': ^9.14.3
'vue-router': ^4.4.3
vuefire: ^3.2.1
# PrimeVue UI framework
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
'@primevue/core': ^4.2.5
'@primevue/forms': ^4.2.5
'@primevue/icons': 4.2.5
'@primevue/themes': ^4.2.5
primeicons: ^7.0.0
primevue: ^4.2.5
# Tailwind CSS and design
'@iconify/json': ^2.2.380
'@iconify-json/lucide': ^1.1.178
'@iconify/tailwind': ^1.1.3
'@tailwindcss/vite': ^4.1.12
tailwindcss: ^4.1.12
'tailwindcss-primeui': ^0.6.1
'tw-animate-css': ^1.3.8
'unplugin-icons': ^0.22.0
'unplugin-vue-components': ^0.28.0
# Storybook
'@storybook/addon-docs': ^9.1.1
storybook: ^9.1.6
'@storybook/vue3': ^9.1.1
'@storybook/vue3-vite': ^9.1.1
# Data and validation
algoliasearch: ^5.21.0
axios: ^1.8.2
firebase: ^11.6.0
yjs: ^13.6.27
zod: ^3.23.8
'zod-validation-error': ^3.3.0
# Dev tools
dotenv: ^16.4.5
husky: ^9.0.11
jiti: 2.4.2
knip: ^5.62.0
'lint-staged': ^15.2.7
# Type definitions
'@types/fs-extra': ^11.0.4
'@types/jsdom': ^21.1.7
'@types/node': ^20.14.8
'@types/semver': ^7.7.0
'@types/three': ^0.169.0
'vue-component-type-helpers': ^3.0.7
'zod-to-json-schema': ^3.24.1
# i18n
'@alloc/quick-lru': ^5.2.0
'@lobehub/i18n-cli': ^1.25.1
'@trivago/prettier-plugin-sort-imports': ^5.2.0
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs

View File

@@ -44,7 +44,6 @@ const showContextMenu = (event: MouseEvent) => {
onMounted(() => {
// @ts-expect-error fixme ts strict error
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
console.log('ComfyUI Front-end version:', config.app_version)
if (isElectron()) {
document.addEventListener('contextmenu', showContextMenu)

View File

@@ -69,7 +69,7 @@ const terminalCreated = (
await loadLogEntries()
} catch (err) {
console.error('Error loading logs', err)
// On older backends the endpoints wont exist
// On older backends the endpoints won't exist
errorMessage.value =
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
return

View File

@@ -45,37 +45,39 @@
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
</Divider>
<!-- Social Login Buttons -->
<!-- Social Login Buttons (hidden if host not whitelisted) -->
<div class="flex flex-col gap-6">
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGoogle')
: t('auth.signup.signUpWithGoogle')
}}
</Button>
<template v-if="ssoAllowed">
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGoogle')
: t('auth.signup.signUpWithGoogle')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGithub')
: t('auth.signup.signUpWithGithub')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGithub')
: t('auth.signup.signUpWithGithub')
}}
</Button>
</template>
<Button
type="button"
@@ -149,6 +151,7 @@ import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist'
import { isInChina } from '@/utils/networkUtil'
import ApiKeyForm from './signin/ApiKeyForm.vue'
@@ -164,6 +167,7 @@ const authActions = useFirebaseAuthActions()
const isSecureContext = window.isSecureContext
const isSignIn = ref(true)
const showApiKeyForm = ref(false)
const ssoAllowed = isHostWhitelisted(normalizeHost(window.location.hostname))
const toggleState = () => {
isSignIn.value = !isSignIn.value

View File

@@ -237,7 +237,7 @@ async function removeKeybinding(commandData: ICommandData) {
async function captureKeybinding(event: KeyboardEvent) {
// Allow the use of keyboard shortcuts when adding keyboard shortcuts
if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
switch (event.key) {
switch (event.code) {
case 'Escape':
cancelEdit()
return

View File

@@ -127,7 +127,7 @@
</template>
<script setup lang="ts">
import { computed, provide, ref, watch } from 'vue'
import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
@@ -202,12 +202,4 @@ const selectedSort = ref<string>('popular')
const selectedNavItem = ref<string | null>('installed')
const gridStyle = computed(() => createGridStyle())
watch(searchText, (newQuery) => {
console.log('searchText:', searchText.value, newQuery)
})
watch(searchQuery, (newQuery) => {
console.log('searchQuery:', searchQuery.value, newQuery)
})
</script>

View File

@@ -86,7 +86,7 @@ const createStoryTemplate = (args: StoryArgs) => ({
const t = (k: string) => k
const onClose = () => {
console.log('OnClose invoked')
// OnClose handler for story
}
provide(OnCloseKey, onClose)

View File

@@ -300,9 +300,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const modeValue = String(modeWidget.value)
const durationValue = String(durationWidget.value)
const modelValue = String(modelWidget.value)
console.log('modelValue', modelValue)
console.log('modeValue', modeValue)
console.log('durationValue', durationValue)
// Same pricing matrix as KlingTextToVideoNode
if (modelValue.includes('v1-6') || modelValue.includes('v1-5')) {
@@ -356,9 +353,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const modeValue = String(modeWidget.value)
const durationValue = String(durationWidget.value)
const modelValue = String(modelWidget.value)
console.log('modelValue', modelValue)
console.log('modeValue', modeValue)
console.log('durationValue', durationValue)
// Same pricing matrix as KlingTextToVideoNode
if (
@@ -564,9 +558,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const model = String(modelWidget.value)
const resolution = String(resolutionWidget.value).toLowerCase()
const duration = String(durationWidget.value)
console.log('model', model)
console.log('resolution', resolution)
console.log('duration', duration)
if (model.includes('ray-flash-2')) {
if (duration.includes('5s')) {

View File

@@ -26,58 +26,58 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: 'r'
key: 'KeyR'
},
commandId: 'Comfy.RefreshNodeDefinitions'
},
{
combo: {
key: 'q'
key: 'KeyQ'
},
commandId: 'Workspace.ToggleSidebarTab.queue'
},
{
combo: {
key: 'w'
key: 'KeyW'
},
commandId: 'Workspace.ToggleSidebarTab.workflows'
},
{
combo: {
key: 'n'
key: 'KeyN'
},
commandId: 'Workspace.ToggleSidebarTab.node-library'
},
{
combo: {
key: 'm'
key: 'KeyM'
},
commandId: 'Workspace.ToggleSidebarTab.model-library'
},
{
combo: {
key: 's',
key: 'KeyS',
ctrl: true
},
commandId: 'Comfy.SaveWorkflow'
},
{
combo: {
key: 'o',
key: 'KeyO',
ctrl: true
},
commandId: 'Comfy.OpenWorkflow'
},
{
combo: {
key: 'g',
key: 'KeyG',
ctrl: true
},
commandId: 'Comfy.Graph.GroupSelectedNodes'
},
{
combo: {
key: ',',
key: 'Comma',
ctrl: true
},
commandId: 'Comfy.ShowSettingsDialog'
@@ -85,7 +85,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
// For '=' both holding shift and not holding shift
{
combo: {
key: '=',
key: 'Equal',
alt: true
},
commandId: 'Comfy.Canvas.ZoomIn',
@@ -93,7 +93,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: '+',
key: 'Equal',
alt: true,
shift: true
},
@@ -103,7 +103,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
// For number pad '+'
{
combo: {
key: '+',
key: 'NumpadAdd',
alt: true
},
commandId: 'Comfy.Canvas.ZoomIn',
@@ -111,7 +111,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: '-',
key: 'Minus',
alt: true
},
commandId: 'Comfy.Canvas.ZoomOut',
@@ -119,21 +119,21 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: '.'
key: 'Period'
},
commandId: 'Comfy.Canvas.FitView',
targetElementId: 'graph-canvas-container'
},
{
combo: {
key: 'p'
key: 'KeyP'
},
commandId: 'Comfy.Canvas.ToggleSelected.Pin',
targetElementId: 'graph-canvas-container'
},
{
combo: {
key: 'c',
key: 'KeyC',
alt: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
@@ -141,7 +141,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: 'b',
key: 'KeyB',
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
@@ -149,7 +149,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: 'm',
key: 'KeyM',
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
@@ -157,20 +157,20 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: '`',
key: 'Backquote',
ctrl: true
},
commandId: 'Workspace.ToggleBottomPanelTab.logs-terminal'
},
{
combo: {
key: 'f'
key: 'KeyF'
},
commandId: 'Workspace.ToggleFocusMode'
},
{
combo: {
key: 'e',
key: 'KeyE',
ctrl: true,
shift: true
},
@@ -178,7 +178,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
{
combo: {
key: 'm',
key: 'KeyM',
alt: true
},
commandId: 'Comfy.Canvas.ToggleMinimap'
@@ -187,19 +187,19 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
combo: {
ctrl: true,
shift: true,
key: 'k'
key: 'KeyK'
},
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
},
{
combo: {
key: 'v'
key: 'KeyV'
},
commandId: 'Comfy.Canvas.Unlock'
},
{
combo: {
key: 'h'
key: 'KeyH'
},
commandId: 'Comfy.Canvas.Lock'
},

View File

@@ -344,7 +344,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
type: 'number',
defaultValue: null,
tooltip:
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.'
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reserved depending on your OS.'
},
// Misc settings

View File

@@ -135,7 +135,7 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
* @param {string} property - The name of the accessed value.
* Checked for conditional logic, but never changed
* @param {object} receiver - The object the result is set to
* and the vlaue used as 'this' if property is a get/set method
* and the value used as 'this' if property is a get/set method
* @param {unknown} value - only used on set calls. The thing being assigned
*/
const handler = {

View File

@@ -147,7 +147,7 @@ app.registerExtension({
// @ts-expect-error fixme ts strict error
node[WEBCAM_READY].then((v) => {
video = v
// If width isnt specified then use video output resolution
// If width isn't specified then use video output resolution
// @ts-expect-error fixme ts strict error
if (!w.value) {
// @ts-expect-error fixme ts strict error

View File

@@ -149,7 +149,7 @@ export class PrimitiveNode extends LGraphNode {
target_slot: number
) {
// Fires before the link is made allowing us to reject it if it isn't valid
// No widget, we cant connect
// No widget, we can't connect
if (!input.widget && !(input.type in ComfyWidgets)) {
return false
}
@@ -388,7 +388,7 @@ export class PrimitiveNode extends LGraphNode {
}
onLastDisconnect() {
// We cant remove + re-add the output here as if you drag a link over the same link
// We can't remove + re-add the output here as if you drag a link over the same link
// it removes, then re-adds, causing it to break
this.outputs[0].type = '*'
this.outputs[0].name = 'connect to widget input'
@@ -595,7 +595,7 @@ app.registerExtension({
this.graph?.add(node)
// Calculate a position that wont directly overlap another node
// Calculate a position that won't directly overlap another node
const pos: [number, number] = [
this.pos[0] - node.size[0] - 30,
this.pos[1]

View File

@@ -146,8 +146,8 @@ Litegraph has no runtime dependencies. The build tooling has been tested on Node
Use GitHub actions to release normal versions.
1. Run the `Release a New Version` action, selecting the version incrment type
1. Merge the resultion PR
1. Run the `Release a New Version` action, selecting the version increment type
1. Merge the resolution PR
1. A GitHub release is automatically published on merge
### Pre-release

View File

@@ -274,8 +274,6 @@ export class LGraph
* @param o data from previous serialization [optional]
*/
constructor(o?: ISerialisedGraph | SerialisableGraph) {
if (LiteGraph.debug) console.log('Graph created')
/** @see MapProxyHandler */
const links = this._links
MapProxyHandler.bindAllMethods(links)
@@ -532,7 +530,7 @@ export class LGraph
this.errors_in_execution = true
if (LiteGraph.throw_errors) throw error
if (LiteGraph.debug) console.log('Error during execution:', error)
if (LiteGraph.debug) console.error('Error during execution:', error)
this.stop()
}
}
@@ -1128,7 +1126,7 @@ export class LGraph
/**
* Snaps the provided items to a grid.
*
* Item positions are reounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
* Item positions are rounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
*
* When {@link LiteGraph.alwaysSnapToGrid} is enabled
* and the grid size is falsy, a default of 1 is used.
@@ -1167,7 +1165,7 @@ export class LGraph
const ctor = LiteGraph.registered_node_types[node.type]
if (node.constructor == ctor) continue
console.log('node being replaced by newer version:', node.type)
console.warn('node being replaced by newer version:', node.type)
const newnode = LiteGraph.createNode(node.type)
if (!newnode) continue
_nodes[i] = newnode
@@ -1229,9 +1227,6 @@ export class LGraph
/* Called when something visually changed (not the graph!) */
change(): void {
if (LiteGraph.debug) {
console.log('Graph changed')
}
this.canvasAction((c) => c.setDirty(true, true))
this.on_change?.(this)
}
@@ -1626,12 +1621,6 @@ export class LGraph
} else {
throw new TypeError('Subgraph input node is not a SubgraphInput')
}
console.debug(
'Reconnect input links in parent graph',
{ ...link },
this.links.get(link.id),
this.links.get(link.id) === link
)
for (const resolved of others) {
resolved.link.disconnect(this)
@@ -2233,7 +2222,7 @@ export class LGraph
let node = LiteGraph.createNode(String(n_info.type), n_info.title)
if (!node) {
if (LiteGraph.debug)
console.log('Node not found or has errors:', n_info.type)
console.warn('Node not found or has errors:', n_info.type)
// in case of error we create a replacement node to avoid losing info
node = new LGraphNode('')

View File

@@ -461,7 +461,7 @@ export class LGraphCanvas
}
const baseFontSize = LiteGraph.NODE_TEXT_SIZE // 14px
const dprAdjustment = Math.sqrt(window.devicePixelRatio || 1) //Using sqrt here because higher DPR monitors do not linearily scale the readability of the font, instead they increase the font by some heurisitc, and to approximate we use sqrt to say bascially a DPR of 2 increases the readibility by 40%, 3 by 70%
const dprAdjustment = Math.sqrt(window.devicePixelRatio || 1) //Using sqrt here because higher DPR monitors do not linearily scale the readability of the font, instead they increase the font by some heurisitc, and to approximate we use sqrt to say basically a DPR of 2 increases the readability by 40%, 3 by 70%
// Calculate the zoom level where text becomes unreadable
this._lowQualityZoomThreshold =
@@ -547,7 +547,7 @@ export class LGraphCanvas
linkMarkerShape: LinkMarkerShape = LinkMarkerShape.Circle
links_render_mode: number
/** Minimum font size in pixels before switching to low quality rendering.
* This intializes first and if we cant get the value from the settings we default to 8px
* This initializes first and if we can't get the value from the settings we default to 8px
*/
private _min_font_size_for_lod: number = 8
@@ -1228,7 +1228,7 @@ export class LGraphCanvas
className: 'event'
})
}
// add callback for modifing the menu elements onMenuNodeOutputs
// add callback for modifying the menu elements onMenuNodeOutputs
const retEntries = node.onMenuNodeOutputs?.(entries)
if (retEntries) entries = retEntries
@@ -3902,7 +3902,7 @@ export class LGraphCanvas
for (const item of [...parsed.nodes, ...parsed.reroutes]) {
if (item.pos == null)
throw new TypeError(
'Invalid node encounterd on paste. `pos` was null.'
'Invalid node encountered on paste. `pos` was null.'
)
if (item.pos[0] < offsetX) offsetX = item.pos[0]
@@ -6406,7 +6406,7 @@ export class LGraphCanvas
return true
}
console.log(`failed creating ${nodeNewType}`)
console.error(`failed creating ${nodeNewType}`)
}
}
return false
@@ -6818,7 +6818,7 @@ export class LGraphCanvas
canvas.focus()
root_document.body.style.overflow = ''
// important, if canvas loses focus keys wont be captured
// important, if canvas loses focus keys won't be captured
setTimeout(() => canvas.focus(), 20)
dialog.remove()
}
@@ -7095,7 +7095,7 @@ export class LGraphCanvas
)
}
} else {
// console.warn("cant find slot " + options.slot_from);
// console.warn("can't find slot " + options.slot_from);
}
}
if (options.node_to) {
@@ -7140,7 +7140,7 @@ export class LGraphCanvas
)
}
} else {
// console.warn("cant find slot_nodeTO " + options.slot_from);
// console.warn("can't find slot_nodeTO " + options.slot_from);
}
}
@@ -7478,7 +7478,7 @@ export class LGraphCanvas
return dialog
}
// TODO refactor, theer are different dialog, some uses createDialog, some dont
// TODO refactor, there are different dialog, some uses createDialog, some dont
createDialog(html: string, options: IDialogOptions): IDialog {
const def_options = {
checkForInput: false,

View File

@@ -167,8 +167,8 @@ input|output: every connection
general properties:
+ clip_area: if you render outside the node, it will be clipped
+ unsafe_execution: not allowed for safe execution
+ skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
+ resizable: if set to false it wont be resizable with the mouse
+ skip_repeated_outputs: when adding new outputs, it won't show if there is one already connected
+ resizable: if set to false it won't be resizable with the mouse
+ widgets_start_y: widgets start at y distance from the top of the node
flags object:
@@ -902,7 +902,7 @@ export class LGraphNode
if (this.onSerialize?.(o))
console.warn(
'node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter'
"node onSerialize shouldn't return anything, data should be stored in the object pass in the first parameter"
)
return o
@@ -1950,7 +1950,7 @@ export class LGraphNode
try {
this.removeWidget(widget)
} catch (error) {
console.debug('Failed to remove widget', error)
console.error('Failed to remove widget', error)
}
}
@@ -2351,7 +2351,7 @@ export class LGraphNode
/**
* returns the output (or input) slot with a given type, -1 if not found
* @param input uise inputs instead of outputs
* @param input use inputs instead of outputs
* @param type the type of the slot to find
* @param returnObj if the obj itself wanted
* @param preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway)
@@ -2583,12 +2583,7 @@ export class LGraphNode
if (slotIndex !== undefined)
return this.connect(slot, target_node, slotIndex, optsIn?.afterRerouteId)
console.debug(
'[connectByType]: no way to connect type:',
target_slotType,
'to node:',
target_node
)
// No compatible slot found - connection not possible
return null
}
@@ -2621,7 +2616,7 @@ export class LGraphNode
if (slotIndex !== undefined)
return source_node.connect(slotIndex, this, slot, optsIn?.afterRerouteId)
console.debug(
console.error(
'[connectByType]: no way to connect type:',
source_slotType,
'to node:',
@@ -2661,7 +2656,7 @@ export class LGraphNode
if (!graph) {
// could be connected before adding it to a graph
// due to link ids being associated with graphs
console.log(
console.error(
"Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them."
)
return null
@@ -2672,11 +2667,12 @@ export class LGraphNode
slot = this.findOutputSlot(slot)
if (slot == -1) {
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${slot}`)
console.error(`Connect: Error, no slot of name ${slot}`)
return null
}
} else if (!outputs || slot >= outputs.length) {
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
if (LiteGraph.debug)
console.error('Connect: Error, slot number not found')
return null
}
@@ -2696,7 +2692,7 @@ export class LGraphNode
targetIndex = target_node.findInputSlot(target_slot)
if (targetIndex == -1) {
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${targetIndex}`)
console.error(`Connect: Error, no slot of name ${targetIndex}`)
return null
}
} else if (target_slot === LiteGraph.EVENT) {
@@ -2728,7 +2724,8 @@ export class LGraphNode
!target_node.inputs ||
targetIndex >= target_node.inputs.length
) {
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
if (LiteGraph.debug)
console.error('Connect: Error, slot number not found')
return null
}
@@ -2914,7 +2911,7 @@ export class LGraphNode
const fromLastFloatingReroute =
parentReroute?.floating?.slotType === 'output'
// Adding from an ouput, or a floating reroute that is NOT the tip of an existing floating chain
// Adding from an output, or a floating reroute that is NOT the tip of an existing floating chain
if (afterRerouteId == null || !fromLastFloatingReroute) {
const link = new LLink(
-1,
@@ -2955,11 +2952,12 @@ export class LGraphNode
slot = this.findOutputSlot(slot)
if (slot == -1) {
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${slot}`)
console.error(`Connect: Error, no slot of name ${slot}`)
return false
}
} else if (!this.outputs || slot >= this.outputs.length) {
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
if (LiteGraph.debug)
console.error('Connect: Error, slot number not found')
return false
}
@@ -3075,19 +3073,19 @@ export class LGraphNode
slot = this.findInputSlot(slot)
if (slot == -1) {
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${slot}`)
console.error(`Connect: Error, no slot of name ${slot}`)
return false
}
} else if (!this.inputs || slot >= this.inputs.length) {
if (LiteGraph.debug) {
console.log('Connect: Error, slot number not found')
console.error('Connect: Error, slot number not found')
}
return false
}
const input = this.inputs[slot]
if (!input) {
console.debug('disconnectInput: input not found', slot, this.inputs)
console.error('disconnectInput: input not found', slot, this.inputs)
return false
}
@@ -3116,19 +3114,16 @@ export class LGraphNode
const target_node = graph.getNodeById(link_info.origin_id)
if (!target_node) {
console.debug(
'disconnectInput: target node not found',
link_info.origin_id
console.error(
'disconnectInput: output not found',
link_info.origin_slot
)
return false
}
const output = target_node.outputs[link_info.origin_slot]
if (!output?.links?.length) {
console.debug(
'disconnectInput: output not found',
link_info.origin_slot
)
// Output not found - may have been removed
return false
}

View File

@@ -241,10 +241,10 @@ export class LiteGraphGlobal {
*/
do_add_triggers_slots = false
/** [false!] being events, it is strongly reccomended to use them sequentially, one by one */
/** [false!] being events, it is strongly recommended to use them sequentially, one by one */
allow_multi_output_for_events = true
/** [true!] allows to create and connect a ndoe clicking with the third button (wheel) */
/** [true!] allows to create and connect a node clicking with the third button (wheel) */
middle_click_slot_add_default_node = false
/** [true!] dragging a link to empty space will open a menu, add from list, search or defaults */
@@ -398,8 +398,6 @@ export class LiteGraphGlobal {
throw 'Cannot register a simple object, it must be a class with a prototype'
base_class.type = type
if (this.debug) console.log('Node registered:', type)
const classname = base_class.name
const pos = type.lastIndexOf('/')
@@ -415,7 +413,7 @@ export class LiteGraphGlobal {
const prev = this.registered_node_types[type]
if (prev && this.debug) {
console.log('replacing node type:', type)
console.warn('replacing node type:', type)
}
this.registered_node_types[type] = base_class
@@ -430,7 +428,7 @@ export class LiteGraphGlobal {
`LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end`
)
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
// TODO one would want to know input and output :: this would allow through registerNodeAndSlotType to get all the slots types
if (this.auto_load_slot_types) new base_class(base_class.title || 'tmpnode')
}
@@ -524,7 +522,7 @@ export class LiteGraphGlobal {
): LGraphNode | null {
const base_class = this.registered_node_types[type]
if (!base_class) {
if (this.debug) console.log(`GraphNode type "${type}" not registered.`)
if (this.debug) console.warn(`GraphNode type "${type}" not registered.`)
return null
}
@@ -637,7 +635,6 @@ export class LiteGraphGlobal {
continue
try {
if (this.debug) console.log('Reloading:', src)
const dynamicScript = document.createElement('script')
dynamicScript.type = 'text/javascript'
dynamicScript.src = src
@@ -645,11 +642,9 @@ export class LiteGraphGlobal {
script_file.remove()
} catch (error) {
if (this.throw_errors) throw error
if (this.debug) console.log('Error while reloading', src)
if (this.debug) console.error('Error while reloading', src)
}
}
if (this.debug) console.log('Nodes reloaded')
}
// separated just to improve if it doesn't work
@@ -749,7 +744,7 @@ export class LiteGraphGlobal {
// convert pointerevents to touch event when not available
if (sMethod == 'pointer' && !window.PointerEvent) {
console.warn("sMethod=='pointer' && !window.PointerEvent")
console.log(
console.warn(
`Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..`
)
switch (sEvent) {
@@ -774,7 +769,7 @@ export class LiteGraphGlobal {
break
}
case 'enter': {
console.log('debug: Should I send a move event?') // ???
// TODO: Determine if a move event should be sent
break
}
// case "over": case "out": not used at now

View File

@@ -906,7 +906,6 @@ export class LinkConnector {
if (connectingTo === 'output') {
// Dropping new output link
const output = node.findOutputByType(firstLink.fromSlot.type)?.slot
console.debug('out', node, output, firstLink.fromSlot)
if (output === undefined) {
console.warn(
`Could not find slot for link type: [${firstLink.fromSlot.type}].`
@@ -918,7 +917,6 @@ export class LinkConnector {
} else if (connectingTo === 'input') {
// Dropping new input link
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
console.debug('in', node, input, firstLink.fromSlot)
if (input === undefined) {
console.warn(
`Could not find slot for link type: [${firstLink.fromSlot.type}].`

View File

@@ -57,7 +57,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
#id: ExecutionId
/**
* The path to the acutal node through subgraph instances, represented as a list of all subgraph node IDs (instances),
* The path to the actual node through subgraph instances, represented as a list of all subgraph node IDs (instances),
* followed by the actual original node ID within the subgraph. Each segment is separated by `:`.
*
* e.g. `1:2:3`:
@@ -104,7 +104,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
readonly subgraphNodePath: readonly NodeId[],
/** A flattened map of all DTOs in this node network. Subgraph instances have been expanded into their inner nodes. */
readonly nodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>,
/** The actual subgraph instance that contains this node, otherise undefined. */
/** The actual subgraph instance that contains this node, otherwise undefined. */
readonly subgraphNode?: SubgraphNode
) {
if (!node.graph) throw new NullGraphError()
@@ -271,9 +271,9 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
// Bypass nodes by finding first input with matching type
const matchingIndex = this.#getBypassSlotIndex(slot, type)
// No input types match
// No input types match - bypass not possible
if (matchingIndex === -1) {
console.debug(
console.warn(
`[ExecutableNodeDTO.resolveOutput] No input types match type [${type}] for id [${this.id}] slot [${slot}]`,
this
)

View File

@@ -175,8 +175,6 @@ export class SubgraphInput extends SubgraphSlot {
}
widgets.push(widget)
} else {
console.debug('No input found on link id', linkId, link)
}
}
return widgets

View File

@@ -188,7 +188,7 @@ export class SubgraphInputNode
const subgraphInput = this.slots.at(subgraphInputIndex)
if (!subgraphInput) {
console.debug(
console.warn(
'disconnectNodeInput: subgraphInput not found',
this,
subgraphInputIndex
@@ -201,7 +201,7 @@ export class SubgraphInputNode
if (index !== -1) {
subgraphInput.linkIds.splice(index, 1)
} else {
console.debug(
console.warn(
'disconnectNodeInput: link ID not found in subgraphInput linkIds',
link.id
)

View File

@@ -430,7 +430,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const inputSlot = this.subgraph.inputNode.slots[slot]
const innerLinks = inputSlot.getLinks()
if (innerLinks.length === 0) {
console.debug(
console.warn(
`[SubgraphNode.resolveSubgraphInputLinks] No inner links found for input slot [${slot}] ${inputSlot.name}`,
this
)
@@ -447,9 +447,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
resolveSubgraphOutputLink(slot: number): ResolvedConnection | undefined {
const outputSlot = this.subgraph.outputNode.slots[slot]
const innerLink = outputSlot.getLinks().at(0)
if (innerLink) return innerLink.resolve(this.subgraph)
console.debug(
if (innerLink) {
return innerLink.resolve(this.subgraph)
}
console.warn(
`[SubgraphNode.resolveSubgraphOutputLink] No inner link found for output slot [${slot}] ${outputSlot.name}`,
this
)

View File

@@ -116,7 +116,7 @@ export function getBoundaryLinks(
const resolved = LLink.resolve(input.link, graph)
if (!resolved) {
console.debug(`Failed to resolve link ID [${input.link}]`)
console.warn(`Failed to resolve link ID [${input.link}]`)
continue
}

View File

@@ -157,7 +157,7 @@ export interface SubgraphIO extends SubgraphIOShared {
id: UUID
/** The data type this slot uses. Unlike nodes, this does not support legacy numeric types. */
type: string
/** Links connected to this slot, or `undefined` if not connected. An ouptut slot should only ever have one link. */
/** Links connected to this slot, or `undefined` if not connected. An output slot should only ever have one link. */
linkIds?: LinkId[]
}

View File

@@ -426,7 +426,6 @@ describe('ExecutableNodeDTO Integration', () => {
describe('ExecutableNodeDTO Scale Testing', () => {
it('should create DTOs at scale', () => {
const graph = new LGraph()
const startTime = performance.now()
const dtos: ExecutableNodeDTO[] = []
// Create DTOs to test performance
@@ -440,16 +439,11 @@ describe('ExecutableNodeDTO Scale Testing', () => {
dtos.push(dto)
}
const endTime = performance.now()
const duration = endTime - startTime
expect(dtos).toHaveLength(1000)
// Test deterministic properties instead of flaky timing
expect(dtos[0].id).toBe('parent:0')
expect(dtos[999].id).toBe('parent:999')
expect(dtos.every((dto, i) => dto.id === `parent:${i}`)).toBe(true)
console.log(`Created 1000 DTOs in ${duration.toFixed(2)}ms`)
})
it('should handle complex path generation correctly', () => {

View File

@@ -56,7 +56,7 @@ describe('SubgraphConversion', () => {
expect(graph.nodes.length).toBe(2)
expect(graph.links.size).toBe(1)
})
it('Should merge boundry links', () => {
it('Should merge boundary links', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'value', type: 'number' }],
outputs: [{ name: 'value', type: 'number' }]

View File

@@ -134,7 +134,7 @@ describe('SubgraphSlot visual feedback', () => {
expect(globalAlphaValues).toContain(0.4)
})
// "not implmeneted yet"
// "not implemented yet"
// it("should render slots with full opacity when dragging between compatible SubgraphInput and SubgraphOutput", () => {
// const subgraph = createTestSubgraph()

View File

@@ -57,12 +57,10 @@ export const Default: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: AssetDisplayItem) => {
console.log('Selected asset:', asset)
}
const onClose = () => {
console.log('Modal closed')
const onAssetSelect = (_asset: AssetDisplayItem) => {
// Asset selection handler for story
}
const onClose = () => {}
return {
...args,
@@ -97,11 +95,11 @@ export const SingleAssetType: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: AssetDisplayItem) => {
console.log('Selected asset:', asset)
const onAssetSelect = (_asset: AssetDisplayItem) => {
// Asset selection handler for story
}
const onClose = () => {
console.log('Modal closed')
// Modal close handler for story
}
// Create assets with only one type (checkpoints)
@@ -146,11 +144,11 @@ export const NoLeftPanel: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: AssetDisplayItem) => {
console.log('Selected asset:', asset)
const onAssetSelect = (_asset: AssetDisplayItem) => {
// Asset selection handler for story
}
const onClose = () => {
console.log('Modal closed')
// Modal close handler for story
}
return { ...args, onAssetSelect, onClose, assets: mockAssets }

View File

@@ -198,10 +198,6 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
assetId: string,
onSelect?: (filename: string) => void
): Promise<void> {
if (import.meta.env.DEV) {
console.debug('Asset selected:', assetId)
}
if (!onSelect) {
return
}

View File

@@ -29,7 +29,6 @@ const DialogDemoComponent = {
}
const handleAssetSelected = (assetPath: string) => {
console.log('Asset selected:', assetPath)
alert(`Selected asset: ${assetPath}`)
isDialogOpen.value = false // Auto-close like the real composable
}

View File

@@ -986,7 +986,7 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Auto Save',
type: 'combo',
options: ['off', 'after delay'], // Room for other options like on focus change, tab change, window change
defaultValue: 'off', // Popular requst by users (https://github.com/Comfy-Org/ComfyUI_frontend/issues/1584#issuecomment-2536610154)
defaultValue: 'off', // Popular request by users (https://github.com/Comfy-Org/ComfyUI_frontend/issues/1584#issuecomment-2536610154)
versionAdded: '1.16.0'
},
{

View File

@@ -195,7 +195,7 @@ defineExpose({
bottom: calc(
var(--sidebar-width) * 2 + var(--sidebar-icon-size) / 2 -
var(--whats-new-popup-bottom)
); /* Position to center of help center icon (2 icons below + half icon height for center - whats new popup bottom position ) */
); /* Position to center of help center icon (2 icons below + half icon height for center - what's new popup bottom position ) */
}
/* Sidebar positioning classes applied by parent */

View File

@@ -87,7 +87,6 @@ export class ComfyWorkflow extends UserFile {
}
// Note: originalContent is populated by super.load()
console.debug('load and start tracking of workflow', this.path)
this.changeTracker = markRaw(
new ChangeTracker(
this,
@@ -98,7 +97,6 @@ export class ComfyWorkflow extends UserFile {
}
override unload(): void {
console.debug('unload workflow', this.path)
this.changeTracker = null
super.unload()
}
@@ -302,7 +300,6 @@ export const useWorkflowStore = defineStore('workflow', () => {
const loadedWorkflow = await workflow.load()
activeWorkflow.value = loadedWorkflow
comfyApp.canvas.bg_tint = loadedWorkflow.tintCanvasBg
console.debug('[workflowStore] open workflow', workflow.path)
return loadedWorkflow
}
@@ -379,7 +376,6 @@ export const useWorkflowStore = defineStore('workflow', () => {
} else {
workflow.unload()
}
console.debug('[workflowStore] close workflow', workflow.path)
}
/**

View File

@@ -219,7 +219,7 @@ const zSubgraphIO = zNodeInput.extend({
id: z.string().uuid(),
/** The data type this slot uses. Unlike nodes, this does not support legacy numeric types. */
type: z.string(),
/** Links connected to this slot, or `undefined` if not connected. An ouptut slot should only ever have one link. */
/** Links connected to this slot, or `undefined` if not connected. An output slot should only ever have one link. */
linkIds: z.array(z.number()).optional()
})

View File

@@ -211,7 +211,8 @@ const isSelected = computed(() => {
})
// Use execution state composable
const { executing, progress } = useNodeExecutionState(() => nodeData.id)
const nodeLocatorId = computed(() => getLocatorIdFromNodeData(nodeData))
const { executing, progress } = useNodeExecutionState(nodeLocatorId)
// Direct access to execution store for error state
const executionStore = useExecutionStore()

View File

@@ -9,7 +9,7 @@
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between relative">
<div class="flex items-center justify-between gap-2.5 relative">
<!-- Collapse/Expand Button -->
<button
v-show="!readonly"
@@ -43,24 +43,22 @@
data-testid="node-pin-indicator"
/>
</div>
<div v-if="!readonly" class="flex items-center lod-toggle shrink-0">
<IconButton
v-if="isSubgraphNode"
size="sm"
type="transparent"
class="text-stone-200 dark-theme:text-slate-300"
data-testid="subgraph-enter-button"
title="Enter Subgraph"
@click.stop="handleEnterSubgraph"
@dblclick.stop
>
<i class="pi pi-external-link"></i>
</IconButton>
</div>
<LODFallback />
</div>
<!-- Title Buttons -->
<div v-if="!readonly" class="flex items-center lod-toggle">
<IconButton
v-if="isSubgraphNode"
size="sm"
type="transparent"
class="text-stone-200 dark-theme:text-slate-300"
data-testid="subgraph-enter-button"
title="Enter Subgraph"
@click.stop="handleEnterSubgraph"
@dblclick.stop
>
<i class="pi pi-external-link"></i>
</IconButton>
</div>
</div>
</template>

View File

@@ -45,6 +45,7 @@
:widget="widget.simplified"
:model-value="widget.value"
:readonly="readonly"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
class="flex-1"
@update:model-value="widget.updateHandler"
/>

View File

@@ -13,8 +13,8 @@ import { cn } from '@/utils/tailwindUtil'
*
*
* IMPORTANT: this escape is needed for many reason due to primevue's directive tooltip system.
* We cannot use PT to conditonally render the tooltips because the entire PT object only run
* once during the intialization of the directive not every mount/unmount.
* We cannot use PT to conditionally render the tooltips because the entire PT object only run
* once during the initialization of the directive not every mount/unmount.
* Once the directive is constructed its no longer reactive in the traditional sense.
* We have to use something non destructive like mouseevents to dismiss the tooltip.
*

View File

@@ -9,27 +9,27 @@ import { useExecutionStore } from '@/stores/executionStore'
* Provides reactive access to execution state and progress for a specific node
* by injecting execution data from the parent GraphCanvas provider.
*
* @param nodeIdMaybe - The ID of the node to track execution state for
* @param nodeLocatorIdMaybe - Locator ID (root or subgraph scoped) of the node to track
* @returns Object containing reactive execution state and progress
*/
export const useNodeExecutionState = (
nodeIdMaybe: MaybeRefOrGetter<string>
nodeLocatorIdMaybe: MaybeRefOrGetter<string | undefined>
) => {
const nodeId = toValue(nodeIdMaybe)
const { uniqueExecutingNodeIdStrings, nodeProgressStates } =
storeToRefs(useExecutionStore())
const locatorId = computed(() => toValue(nodeLocatorIdMaybe) ?? '')
const { nodeLocationProgressStates } = storeToRefs(useExecutionStore())
const executing = computed(() => {
return uniqueExecutingNodeIdStrings.value.has(nodeId)
const progressState = computed(() => {
const id = locatorId.value
return id ? nodeLocationProgressStates.value[id] : undefined
})
const executing = computed(() => progressState.value?.state === 'running')
const progress = computed(() => {
const state = nodeProgressStates.value[nodeId]
return state?.max > 0 ? state.value / state.max : undefined
const state = progressState.value
return state && state.max > 0 ? state.value / state.max : undefined
})
const progressState = computed(() => nodeProgressStates.value[nodeId])
const progressPercentage = computed(() => {
const prog = progress.value
return prog !== undefined ? Math.round(prog * 100) : undefined

View File

@@ -86,7 +86,6 @@ describe('WidgetInputNumberSlider Value Binding', () => {
it('renders input field', () => {
const widget = createMockWidget(5)
const wrapper = mountComponent(widget, 5)
console.log(wrapper.html())
expect(wrapper.find('input[inputmode="numeric"]').exists()).toBe(true)
})

View File

@@ -106,7 +106,7 @@ export const useImageUploadWidget = () => {
}
// On load if we have a value then render the image
// The value isnt set immediately so we need to wait a moment
// The value isn't set immediately so we need to wait a moment
// No change callbacks seem to be fired on initial setting of the value
requestAnimationFrame(() => {
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {

View File

@@ -524,7 +524,7 @@ export class ComfyApi extends EventTarget {
if (msg.data.sid) {
const clientId = msg.data.sid
this.clientId = clientId
window.name = clientId // use window name so it isnt reused when duplicating tabs
window.name = clientId // use window name so it isn't reused when duplicating tabs
sessionStorage.setItem('clientId', clientId) // store in session storage so duplicate tab can load correct workflow
}
this.dispatchCustomEvent('status', msg.data.status ?? null)

View File

@@ -69,9 +69,6 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
const timeSinceLastAttempt =
Date.now() - providerState.lastAttempt.getTime()
if (timeSinceLastAttempt > CIRCUIT_BREAKER_TIMEOUT) {
console.info(
`Retrying ${providerState.name} provider after circuit breaker timeout`
)
return true
}
}

View File

@@ -8,6 +8,7 @@ import {
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { migrateKeybindings } from '@/utils/keybindingMigration'
export const useKeybindingService = () => {
const keybindingStore = useKeybindingStore()
@@ -51,7 +52,7 @@ export const useKeybindingService = () => {
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
// Special handling for Escape key - let dialogs handle it first
if (
event.key === 'Escape' &&
event.code === 'Escape' &&
!event.ctrlKey &&
!event.altKey &&
!event.metaKey
@@ -88,7 +89,7 @@ export const useKeybindingService = () => {
}
// Escape key: close the first open modal found, and all dialogs
if (event.key === 'Escape') {
if (event.code === 'Escape') {
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
for (const modal of modals) {
const modalDisplay = window
@@ -111,14 +112,41 @@ export const useKeybindingService = () => {
}
}
function registerUserKeybindings() {
// Unset bindings first as new bindings might conflict with default bindings.
async function registerUserKeybindings() {
// Load user keybindings from settings
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
for (const keybinding of unsetBindings) {
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
// Migrate keybindings from old event.key format to new event.code format
const migratedUnset = migrateKeybindings(unsetBindings)
const migratedNew = migrateKeybindings(newBindings)
// Save migrated keybindings back to settings if any migration occurred
if (migratedUnset.migrated) {
await settingStore.set(
'Comfy.Keybinding.UnsetBindings',
migratedUnset.keybindings
)
console.warn(
'[Keybindings] Migrated unset keybindings to event.code format'
)
}
if (migratedNew.migrated) {
await settingStore.set(
'Comfy.Keybinding.NewBindings',
migratedNew.keybindings
)
console.warn(
'[Keybindings] Migrated custom keybindings to event.code format'
)
}
// Unset bindings first as new bindings might conflict with default bindings.
for (const keybinding of migratedUnset.keybindings) {
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
}
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
for (const keybinding of newBindings) {
for (const keybinding of migratedNew.keybindings) {
keybindingStore.addUserKeybinding(new KeybindingImpl(keybinding))
}
}

View File

@@ -14,7 +14,7 @@ export interface ElectronDownload
status?: DownloadStatus
}
/** Electron donwloads store handler */
/** Electron downloads store handler */
export const useElectronDownloadStore = defineStore('downloads', () => {
const downloads = ref<ElectronDownload[]>([])
const { DownloadManager } = electronAPI()

View File

@@ -404,10 +404,6 @@ export const useExecutionStore = defineStore('execution', () => {
...queuedPrompt.nodes
}
queuedPrompt.workflow = workflow
console.debug(
`queued task ${id} with ${Object.values(queuedPrompt.nodes).length} nodes`
)
}
/**

View File

@@ -65,7 +65,7 @@ export const useExtensionStore = defineStore('extension', () => {
}
if (disabledExtensionNames.value.has(extension.name)) {
console.log(`Extension ${extension.name} is disabled.`)
console.warn(`Extension ${extension.name} is disabled.`)
}
extensionByName.value[extension.name] = markRaw(extension)

View File

@@ -44,7 +44,7 @@ export class KeyComboImpl implements KeyCombo {
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
key: event.code,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
@@ -75,7 +75,16 @@ export class KeyComboImpl implements KeyCombo {
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
return [
'ControlLeft',
'ControlRight',
'MetaLeft',
'MetaRight',
'AltLeft',
'AltRight',
'ShiftLeft',
'ShiftRight'
].includes(this.key)
}
get modifierCount(): number {
@@ -106,9 +115,95 @@ export class KeyComboImpl implements KeyCombo {
if (this.shift) {
sequences.push('Shift')
}
sequences.push(this.key)
sequences.push(this.getDisplayKey())
return sequences
}
getDisplayKey(): string {
// Convert key codes to display names
const keyMap: Record<string, string> = {
// Letters
KeyA: 'A',
KeyB: 'B',
KeyC: 'C',
KeyD: 'D',
KeyE: 'E',
KeyF: 'F',
KeyG: 'G',
KeyH: 'H',
KeyI: 'I',
KeyJ: 'J',
KeyK: 'K',
KeyL: 'L',
KeyM: 'M',
KeyN: 'N',
KeyO: 'O',
KeyP: 'P',
KeyQ: 'Q',
KeyR: 'R',
KeyS: 'S',
KeyT: 'T',
KeyU: 'U',
KeyV: 'V',
KeyW: 'W',
KeyX: 'X',
KeyY: 'Y',
KeyZ: 'Z',
// Numbers
Digit0: '0',
Digit1: '1',
Digit2: '2',
Digit3: '3',
Digit4: '4',
Digit5: '5',
Digit6: '6',
Digit7: '7',
Digit8: '8',
Digit9: '9',
// Function keys
F1: 'F1',
F2: 'F2',
F3: 'F3',
F4: 'F4',
F5: 'F5',
F6: 'F6',
F7: 'F7',
F8: 'F8',
F9: 'F9',
F10: 'F10',
F11: 'F11',
F12: 'F12',
// Special keys
Space: 'Space',
Enter: 'Enter',
Tab: 'Tab',
Escape: 'Escape',
Backspace: 'Backspace',
Delete: 'Delete',
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
Home: 'Home',
End: 'End',
PageUp: 'PageUp',
PageDown: 'PageDown',
Insert: 'Insert',
// Punctuation
Minus: '-',
Equal: '=',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Semicolon: ';',
Quote: "'",
Backquote: '`',
Comma: ',',
Period: '.',
Slash: '/'
}
return keyMap[this.key] || this.key
}
}
export const useKeybindingStore = defineStore('keybinding', () => {

View File

@@ -151,7 +151,6 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
/** @todo Refreshes Electron tasks only. */
const refreshDesktopTasks = async () => {
isRefreshing.value = true
console.log('Refreshing desktop tasks')
await electron.Validation.validateInstallation(processUpdate)
}

View File

@@ -18,7 +18,7 @@ export class ExecutableGroupNodeChildDTO extends ExecutableNodeDTO {
subgraphNodePath: readonly NodeId[],
/** A flattened map of all DTOs in this node network. Subgraph instances have been expanded into their inner nodes. */
nodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>,
/** The actual subgraph instance that contains this node, otherise undefined. */
/** The actual subgraph instance that contains this node, otherwise undefined. */
subgraphNode?: SubgraphNode | undefined,
groupNodeHandler?: GroupNodeHandler
) {

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