Compare commits

...

39 Commits

Author SHA1 Message Date
snomiao
c004a2b8bd chore(tsconfig): ensure complete TypeScript coverage for all project files (#5655)
## Summary
- Added missing directories and files to tsconfig.json to ensure
complete TypeScript type checking coverage
- Expanded config file patterns to include all .mts configuration files
- Verified all 908 TypeScript files in the project are now properly
covered

## Changes
- Added `scripts/**/*.ts` to cover all TypeScript files in scripts
directory (i18n collection, CI/CD scripts)
- Added `build/**/*.ts` to cover customIconCollection.ts and future
build scripts
- Changed `vite.config.mts` to `*.config.mts` to include all vite config
files (vite.electron.config.mts, vite.types.config.mts)

## Test plan
- [x] Run `pnpm typecheck` to verify no TypeScript errors
- [x] Verified all TypeScript files are covered by tsconfig patterns
- [x] browser_tests/ directory confirmed to have its own extending
tsconfig

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5655-chore-tsconfig-ensure-complete-TypeScript-coverage-for-all-project-files-2736d73d36508103acbadc53ca2b2913)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
2025-09-23 09:31:30 -07:00
snomiao
d0aee031e9 [feat] Merge ComfyUI_devtools into ComfyUI_frontend (#5166)
## Summary
Merges ComfyUI devtools components into the ComfyUI frontend monorepo to
consolidate development tools.

## Changes
- Added devtools components from ComfyUI repository
- Integrated development nodes and utilities
- Consolidated fake model assets for testing

## Related Issues
Fixes #4683

## Testing
- Devtools components are now available within the frontend monorepo
- Development workflow remains consistent

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-22 23:15:50 -07:00
Simula_r
cec1de0147 feat: vue nodes LOD system (#5631)
## Summary

Replaced reactive (Vue-based) widget LOD with CSS visibility control.
Performance doesn't dramatically improve, but we avoid the mount/unmount
overhead during zoom/pan operations. This PR implements the visual
component of LOD—complex widgets that need lifecycle management will be
addressed separately.

### Problem & Solution
Problem: we want LOD to improve rendering performance and visual
feedback but discovered using reactivity in the current setup for it
meant mounting/unmounting caused worse lag than the performance it aimed
to fix. Switching to render all the details all the time but using css
visibility proved to be the best solution. However, it doesn't improve
rendering performance by much because the GPU texture size is the
bottleneck (from TransformPane.vue CSS transforms) and not
rasterization.

Solution: Keep all nodes/widgets mounted, use CSS visibility: hidden for
LOD. Trade memory for performance stability during zoom/pan/drag
operations.

### Technical Decision
We chose Performance > Memory:

- CSS transforms create a single GPU texture whose size depends on node
count, not widget complexity
- Mounting/unmounting hundreds of widgets during zoom = noticeable lag
from Vue VDOM diffing (since all components are mounted all the time
because of viewport culling challenge/trade off see
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5510.)
- CSS visibility changes = no reactivity overhead, smooth interactions
- Result: Similar performance, but without interaction stutters

This is the visual layer only. If we want a hook into the LOD state per
node / widget that would be the next follow up system to implement.

### Next Steps (maybe)
- Chunked (split up single Transform Pane transform layer) when
rendering 1000+ nodes (maybe)
- ~~Selective unmounting API for widgets that register as "expensive"~~
- ~~Client bound hydration system~~

## Screenshots (if applicable)

<!-- Add screenshots or video recording to help explain your changes -->

<img width="1355" height="960" alt="image"
src="https://github.com/user-attachments/assets/41474d1b-9dbe-4240-a8cf-f4c9ff51d8e0"
/>
<img width="1354" height="963" alt="image"
src="https://github.com/user-attachments/assets/9f55edaa-5858-41b9-b6a8-c2d37e1649bd"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5631-feat-vue-nodes-LOD-system-2726d73d365081c6a6c4e14aa634f19c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-22 20:05:13 -07:00
Christian Byrne
b4976c1ddc Revert: Move VueFire persistence configuration to initialization (#5614) (#5729)
## Summary
This reverts PR #5614 which moved VueFire persistence configuration to
initialization.

## Reason for Revert

It breaks Google SSO login with error:

```
useErrorHandling.ts:12 FirebaseError: Firebase: Error (auth/argument-error).
    at createErrorInternal (index-c92d61ad.js:506:41)
    at _assert (index-c92d61ad.js:512:15)
    at _withDefaultResolver (index-c92d61ad.js:9237:5)
    at signInWithPopup (index-c92d61ad.js:9457:30)
    at executeAuthAction.createCustomer (firebaseAuthStore.ts:263:25)
    at executeAuthAction (firebaseAuthStore.ts:223:28)
    at Proxy.loginWithGoogle (firebaseAuthStore.ts:262:5)
    at Proxy.wrappedAction (pinia.mjs:1405:26)
    at useFirebaseAuthActions.ts:104:28
    at Object.signInWithGoogle (useErrorHandling.ts:39:22)
```

## Changes
- Reverts commit ea4e57b60 "Move VueFire persistence configuration to
initialization (#5614)"
- Restores previous Firebase auth persistence behavior

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5729-Revert-Move-VueFire-persistence-configuration-to-initialization-5614-2776d73d3650814c9b80d9c67c852874)
by [Unito](https://www.unito.io)
2025-09-22 19:04:08 -07:00
Alexander Brown
1611c7a224 Refactor: Further state management cleanup (#5727)
## Summary

Going through the GraphNodeManager and VueNodeLifecycle one property at
a time and removing the pieces that are not currently wired up or used
by the rest of the application

Fixes paste location by updating the layoutStore in LGraphCanvas (which
already mutates layoutStore elsewhere)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5727-WIP-Refactor-Further-state-management-cleanup-2766d73d36508173b379c6009c194a5a)
by [Unito](https://www.unito.io)
2025-09-22 18:47:26 -07:00
Comfy Org PR Bot
d01081dab4 1.28.1 (#5728)
Patch version increment to 1.28.1

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5728-1-28-1-2776d73d365081b287e4fdc5cbcea07b)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com>
2025-09-22 20:36:12 -05:00
Alexander Brown
e5d4d07d32 Refactor: More state management simplification (#5721)
## Summary

Remove more procedural synchronization in favor of using reactive
references.

> Note: Also includes some fixes for issues caused during HMR.

## Review Focus

In testing it seems to work the same, but let me know if I missed
something.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5721-Refactor-More-state-management-simplification-2766d73d3650819b8d7ddc047c460f2b)
by [Unito](https://www.unito.io)
2025-09-22 13:15:33 -07:00
Alexander Piskun
f086377307 add pricing for new api nodes (#5724)
## Summary

Added prices for the new upcoming API nodes. Backport required.
2025-09-22 11:33:00 -07:00
filtered
687b9e659c Fix reroute ID 0 treated as invalid (#5723)
## Summary

Fixes old logic bug from refactor
https://github.com/Comfy-Org/litegraph.js/pull/602/files

## Changes

- Fixes truthy refactor to explicitly check undefined

## Review Focus

No expectation that this will impact prod, however it may impact
extensions IF someone has explicitly been setting link parentId to 0.
This would be very strange, as it would cause unexpected behaviour in
other parts of the code (which all explicitly check `undefined`).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5723-Fix-reroute-ID-0-treated-as-invalid-2766d73d365081568124ce1f85cdf84e)
by [Unito](https://www.unito.io)
2025-09-22 11:13:38 -07:00
Christian Byrne
da0d51311b fix Vue node being dragged when interacting with widgets (e.g., resizing textarea) (#5719)
## Summary

Applying changes in
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5516 to entire widget
wrapper.
 
## Changes

- **What**: Added `.stop` modifier to pointer events in NodeWidgets
component to prevent [event
propagation](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)

## Review Focus

Verify widget interactions remain functional while ensuring parent node
drag/selection behavior is properly isolated.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5719-fix-Vue-node-being-dragged-when-interacting-with-widgets-e-g-resizing-textarea-2766d73d3650815091adcd1d65197c7b)
by [Unito](https://www.unito.io)
2025-09-21 21:56:03 -07:00
Christian Byrne
e314d9cbd9 [refactor] Simplify current user resolved hook implementation (#5718)
## Summary

Refactored `onUserResolved` function in auth composable to use VueUse
`whenever` utility instead of manual watch implementation and use
`immediate` option instead of invoking manually before creating watcher.

## Changes

- **What**: Replaced manual watch + immediate check pattern with [VueUse
whenever](https://vueuse.org/shared/whenever/) utility in
`useCurrentUser.ts:37`

## Review Focus

Behavioral equivalence verification - `whenever` with `immediate: true`
should maintain identical callback timing and cleanup semantics.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5718-refactor-Simplify-current-user-resolved-hook-implementation-2766d73d365081008b6de156dd78f940)
by [Unito](https://www.unito.io)
2025-09-21 21:53:25 -07:00
Christian Byrne
95baf8d2f1 [style] update Vue node tooltip style (#5717)
## Summary

Change Vue node tooltips to align with
[design](https://www.figma.com/design/31uH3r4x3xbIctuRWYW6NM/V3---Nodes?node-id=6267-16837&m=dev)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5717-style-update-Vue-node-tooltip-style-2766d73d365081bdb095faef17f6aeb6)
by [Unito](https://www.unito.io)
2025-09-21 20:01:33 -07:00
Christian Byrne
f951e07cea fix bypass hotkey in vue nodes and fix node data instrumentation setup issue when switching to Vue nodes after initial load (#5715)
## Summary

Fixed Vue node keybinding target element ID to enable
bypass/pin/collapse hotkeys in both LiteGraph and Vue rendering modes.

Also fixed a bug when starting in litegraph mode => switching to Vue
nodes without reloading => `graph.onTrigger` is set to `undefined` which
interferes with proper setup of node data instrumentation, among other
things.

## Changes

- **What**: Updated keybinding `targetElementId` from `graph-canvas` to
`graph-canvas-container` for node manipulation commands (parent of both
the canvas and transform pane -- vue nodes container).
- **What**: Added conditional `onTrigger` handler restoration in slot
layout sync to prevent Vue node manager conflicts

## Review Focus

Event handler precedence between Vue nodes and LiteGraph systems during
mode switching, ensuring hotkeys work consistently across rendering
modes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5715-fix-bypass-hotkey-in-vue-nodes-and-fix-node-data-instrumentation-setup-issue-when-switchi-2756d73d3650815c8ec8d5e4d06232e3)
by [Unito](https://www.unito.io)
2025-09-21 17:32:12 -07:00
Christian Byrne
023e466dba fix using shift modifier to (de-)select Vue nodes (#5714)
## Summary

Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/5688 by
adding shift modifier support for multi-selecting Vue nodes, enabling
standard shift+click selection behavior alongside existing
ctrl/cmd+click.

## Changes

- **What**: Updated Vue node event handlers to include `event.shiftKey`
in multi-select logic
- **Testing**: Added browser tests for both ctrl and shift modifier
selection behaviors

## Review Focus

Multi-select behavior consistency across different input modifiers and
platform compatibility (Windows/Mac/Linux shift key handling).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5714-fix-using-shift-modifier-to-de-select-Vue-nodes-2756d73d365081bcb5e0fe80eacdb2f0)
by [Unito](https://www.unito.io)
2025-09-21 14:39:40 -07:00
Christian Byrne
abd6823744 [refactor] Remove redundant module comment (#5711)
Removes a comment added in initial Vue Nodes commit. The comment is
interpolated between import statements which is stylistically awkward
and it is almost totally redundant with the doc comment on the
composable:


c1d4709e96/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts (L10-L14)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5711-refactor-Remove-redundant-module-comment-2756d73d365081ef9bffe0257b3670f1)
by [Unito](https://www.unito.io)
2025-09-21 14:30:58 -07:00
Alexander Brown
c4c0e52e64 Refactor: Let LGraphNode handle more events itself (#5709)
## Summary

Don't route events up through GraphCanvas if the component itself can
handle the changes

## Changes

- **What**: Reduce the indirect access or action dispatch to
composables/stores.

## Review Focus

The behavior should be either equivalent or a little snappier than
before. Also, the local state in LGraphNode has (almost) all been
removed in favor of reacting to the nodeData prop.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5709-Refactor-Let-LGraphNode-handle-more-events-itself-2756d73d365081e6a88ce6241bceecc0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-20 22:14:30 -07:00
Christian Byrne
295332dc46 update CODEOWNERS (#5667)
Add explicit CODEOWNERS for new features to allow more domain-driven
review/approval/ownership processes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5667-update-CODEOWNERS-2736d73d3650817ea52be9c4a8fe5ff2)
by [Unito](https://www.unito.io)
2025-09-20 20:11:35 -07:00
Christian Byrne
5c498348b8 fix: update to standardized mobile web app meta tag syntax (#5672)
## Summary

Fixed WebKit deprecation warning by updating to standardized mobile web
app meta tag syntax.

## Changes

- **What**: Replaced deprecated `apple-mobile-web-app-capable` with
cross-platform
[`mobile-web-app-capable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name#mobile-web-app-capable)
meta tag to align with WebKit's move toward vendor-neutral standards

## Review Focus

Verify "Add to Home Screen" functionality still works on iOS/iPadOS and
that the WebKit console warning is resolved in production builds.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5672-fix-update-to-standardized-mobile-web-app-meta-tag-syntax-2736d73d3650811cb2a1f0b14ce0a0e7)
by [Unito](https://www.unito.io)
2025-09-20 20:10:51 -07:00
Alexander Brown
8133bd4b7b Refactor: Composable disentangling (#5695)
## Summary

Prerequisite refactor/cleanup to use a global store instead of having
nodes throw up events to a parent component that stores a reference to a
singleton service that itself bootstraps and synchronizes with a
separate service to maintain a partially reactive but not fully reactive
set of states that describe some but not all aspects of the nodes on
either the litegraph, the vue side, or both.

## Changes

- **What**: Refactoring, the behavior should not change.
- **Dependencies**: A type utility to help with Vue component props

## Review Focus

Is there something about the current structure that this could affect
that would not be caught by our tests or using the application?

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5695-Refactor-Composable-disentangling-2746d73d365081e6938ce656932f3e36)
by [Unito](https://www.unito.io)
2025-09-20 13:06:42 -07:00
Arjan Singh
fd12591756 [feat] integrate asset browser with widget system (#5629)
## Summary

Add asset browser dialog integration for combo widgets with full
animation support and proper state management.

(Thank you Claude from saving me me from merge conflict hell on this
one.)

## Changes

- Widget integration: combo widgets now use AssetBrowserModal for
eligible asset types
- Dialog animations: added animateHide() for smooth close transitions
- Async operations: proper sequencing of widget updates and dialog
animations
- Service layer: added getAssetsForNodeType() and getAssetDetails()
methods
- Type safety: comprehensive TypeScript types and error handling
- Test coverage: unit tests for all new functionality
- Bonus: fixed the hardcoded labels in AssetFilterBar

Widget behavior:
- Shows asset browser button for eligible widgets when asset API enabled
- Handles asset selection with proper callback sequencing
- Maintains widget value updates and litegraph notification

## Review Focus

I will call out some stuff inline.

## Screenshots


https://github.com/user-attachments/assets/9d3a72cf-d2b0-445f-8022-4c49daa04637

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5629-feat-integrate-asset-browser-with-widget-system-2726d73d365081a9a98be9a2307aee0b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2025-09-20 11:44:18 -07:00
Christian Byrne
b3c939ff15 fix: add Safari requestIdleCallback polyfill (#5664)
## Summary

Implemented cross-browser requestIdleCallback polyfill to fix Safari
crashes during graph initialization.

## Changes

- **What**: Added
[requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
polyfill following [VS Code's
pattern](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts)
with setTimeout fallback for Safari
- **Breaking**: None - maintains existing GraphView behavior

## Review Focus

Safari compatibility testing and timeout handling in the 15ms fallback
window. Verify that initialization tasks (keybindings, server config,
model loading) still execute properly on Safari iOS.

## References

- [VS Code async.ts
implementation](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts)
- Source pattern for our polyfill
- [MDN
requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
- Browser API documentation
- [Safari requestIdleCallback
support](https://caniuse.com/requestidlecallback) - Browser
compatibility table

Fixes CLOUD-FRONTEND-STAGING-N

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5664-fix-add-Safari-requestIdleCallback-polyfill-2736d73d365081cdbcf1fb816fe098d6)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-19 23:34:15 -07:00
Christian Byrne
0801778f60 feat: Add Vue node subgraph title button and fix subgraph navigation with vue nodes (#5572)
## Summary
- Adds subgraph title button to Vue node headers (matching LiteGraph
behavior)
- Fixes Vue node lifecycle issues during subgraph navigation and tab
switching
- Extracts reusable `useSubgraphNavigation` composable with
callback-based API
- Adds comprehensive tests for subgraph functionality
- Ensures proper graph context restoration during tab switches



https://github.com/user-attachments/assets/fd4ff16a-4071-4da6-903f-b2be8dd6e672



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5572-feat-Add-Vue-node-subgraph-title-button-with-lifecycle-management-26f6d73d365081bfbd9cfd7d2775e1ef)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
2025-09-19 14:19:06 -07:00
Johnpaul Chiwetelu
8ffe63f54e Layoutstore Minimap calculation (#5547)
This pull request refactors the minimap rendering system to use a
unified, extensible data source abstraction for all minimap operations.
By introducing a data source interface and factory, the minimap can now
seamlessly support multiple sources of node layout (such as the
`LayoutStore` or the underlying `LiteGraph`), improving maintainability
and future extensibility. Rendering logic and change detection
throughout the minimap have been updated to use this new abstraction,
resulting in cleaner code and easier support for new data models.

**Core architecture improvements:**

* Introduced a new `IMinimapDataSource` interface and related data types
(`MinimapNodeData`, `MinimapLinkData`, `MinimapGroupData`) to
standardize node, link, and group data for minimap rendering.
* Added an abstract base class `AbstractMinimapDataSource` that provides
shared logic for bounds and group/link extraction, and implemented two
concrete data sources: `LiteGraphDataSource` (for classic graph data)
and `LayoutStoreDataSource` (for layout store data).
[[1]](diffhunk://#diff-ea46218fc9ffced84168a5ff975e4a30e43f7bf134ee8f02ed2eae66efbb729dR1-R95)
[[2]](diffhunk://#diff-9a6b7c6be25b4dbeb358fea18f3a21e78797058ccc86c818ed1e5f69c7355273R1-R30)
[[3]](diffhunk://#diff-f200ba9495a03157198abff808ed6c3761746071404a52adbad98f6a9d01249bR1-R42)
* Created a `MinimapDataSourceFactory` that selects the appropriate data
source based on the presence of layout store data, enabling seamless
switching between data models.

**Minimap rendering and logic refactoring:**

* Updated all minimap rendering functions (`renderGroups`,
`renderNodes`, `renderConnections`) and the main `renderMinimapToCanvas`
entry point to use the unified data source interface, significantly
simplifying the rendering code and decoupling it from the underlying
graph structure.
[[1]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L1-R11)
[[2]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121R33-R75)
[[3]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L66-R124)
[[4]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L134-R161)
[[5]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L153-R187)
[[6]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L187-L188)
[[7]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121R227-R231)
[[8]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L230-R248)
* Refactored minimap viewport and graph change detection logic to use
the data source abstraction for bounds, node, and link change detection,
and to respond to layout store version changes.
[[1]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6L2-R10)
[[2]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6R33-R35)
[[3]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6L99-R141)
[[4]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6R157-R160)
[[5]](diffhunk://#diff-338d14c67dabffaf6f68fbf09b16e8d67bead2b9df340e46601b2fbd57331521L8-R11)
[[6]](diffhunk://#diff-338d14c67dabffaf6f68fbf09b16e8d67bead2b9df340e46601b2fbd57331521L56-R64)

These changes make the minimap codebase more modular and robust, and lay
the groundwork for supporting additional node layout strategies in the
future.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5547-Layoutstore-Minimap-calculation-26e6d73d3650813e9457c051dff41ca1)
by [Unito](https://www.unito.io)
2025-09-19 13:52:57 -07:00
Benjamin Lu
893409dfc8 Add playwright tests for links and slots in vue nodes mode (#5668)
Tests added
- Should show a link dragging out from a slot when dragging on a slot
- Should create a link when dropping on a compatible slot
- Should not create a link when dropping on an incompatible slot(s)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5668-Add-playwright-tests-for-links-and-slots-in-vue-nodes-mode-2736d73d36508188a47dceee5d1a11e5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-19 13:51:47 -07:00
Christian Byrne
df2fda6077 [refactor] Replace manual semantic version utilities/functions with semver package (#5653)
## Summary
- Replace custom `compareVersions()` with `semver.compare()`
- Replace custom `isSemVer()` with `semver.valid()`  
- Remove deprecated version comparison functions from `formatUtil.ts`
- Update all version comparison logic across components and stores
- Fix tests to use semver mocking instead of formatUtil mocking

## Benefits
- **Industry standard**: Uses well-maintained, battle-tested `semver`
package
- **Better reliability**: Handles edge cases more robustly than custom
implementation
- **Consistent behavior**: All version comparisons now use the same
underlying logic
- **Type safety**: Better TypeScript support with proper semver types


Fixes #4787

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5653-refactor-Replace-manual-semantic-version-utilities-functions-with-semver-package-2736d73d365081fb8498ee11cbcc10e2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-19 12:27:49 -07:00
Christian Byrne
4f5bbe0605 [refactor] Remove legacy manager UI support and tag from header (#5665)
## Summary

Removed the informational "Use Legacy UI" tag from the ManagerHeader
component while preserving all underlying legacy manager functionality.

## Changes

- **What**: Removed Tag component displaying legacy UI information from
ManagerHeader
- **Breaking**: None - all legacy manager functionality remains intact
- **Dependencies**: None

## Review Focus

Visual cleanup only - the `--enable-manager-legacy-ui` CLI flag and all
related functionality continues to work normally. Only the informational
UI tag has been removed from the header.
2025-09-19 02:07:51 -07:00
Christian Byrne
a975e50f1b [feat] Add tooltip support for Vue nodes (#5577)
## Summary

Added tooltip support for Vue node components using PrimeVue's v-tooltip
directive with proper data integration and container scoping.


https://github.com/user-attachments/assets/d1af31e6-ef6a-4df8-8de4-5098aa4490a1

## Changes

- **What**: Implemented tooltip functionality for Vue node headers,
input/output slots, and widgets using [PrimeVue
v-tooltip](https://primevue.org/tooltip/) directive
- **Dependencies**: Leverages existing PrimeVue tooltip system, no new
dependencies

## Review Focus

Container scoping implementation via provide/inject pattern for tooltip
positioning, proper TypeScript interfaces eliminating `as any` casts,
and integration with existing settings store for tooltip delays and
enable/disable functionality.

```mermaid
graph TD
    A[LGraphNode Container] --> B[provide tooltipContainer]
    B --> C[NodeHeader inject]
    B --> D[InputSlot inject]
    B --> E[OutputSlot inject]
    B --> F[NodeWidgets inject]

    G[useNodeTooltips composable] --> H[NodeDefStore lookup]
    G --> I[Settings integration]
    G --> J[i18n fallback]

    C --> G
    D --> G
    E --> G
    F --> G

    style A fill:#f9f9f9,stroke:#333,color:#000
    style G fill:#e8f4fd,stroke:#0066cc,color:#000
```

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-09-19 01:07:50 -07:00
Christian Byrne
a17c74fa0c fix: add optional chaining to nodeDef access in NodeTooltip (#5663)
## Summary

Extension of https://github.com/Comfy-Org/ComfyUI_frontend/pull/5659:
Added optional chaining to NodeTooltip component to prevent TypeError
when `nodeDef` is undefined for unknown node types.

## Changes

- **What**: Added [optional chaining
operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)
(`?.`) to safely access `nodeDef` properties in NodeTooltip component

## Review Focus

Error handling for node types not found in nodeDefStore and tooltip
display behavior for unrecognized nodes.

Fixes CLOUD-FRONTEND-STAGING-3N

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-19 01:04:12 -07:00
Christian Byrne
5e625a5002 [test] add Vue FormSelectButton widget component tests (#5576)
## Summary

Added comprehensive component tests for FormSelectButton widget with 497
test cases covering all interaction patterns and edge cases.

## Changes

- **What**: Created test suite for
[FormSelectButton.vue](https://vuejs.org/guide/scaling-up/testing.html)
component with full coverage of string/number/object options, PrimeVue
compatibility, disabled states, and visual styling
- **Dependencies**: No new dependencies (uses existing vitest,
@vue/test-utils)

## Review Focus

Test completeness covering edge cases like unicode characters, duplicate
values, and objects with missing properties. Verify test helper
functions correctly simulate user interactions.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5576-Add-Vue-FormSelectButton-widget-component-tests-26f6d73d36508171ae08ee74d0605db2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
2025-09-19 00:12:25 -07:00
Christian Byrne
002fac0232 [refactor] Migrate manager code to DDD structure (#5662)
## Summary

Reorganized custom nodes manager functionality from scattered technical
layers into a cohesive domain-focused module following [domain-driven
design](https://en.wikipedia.org/wiki/Domain-driven_design) principles.

## Changes

- **What**: Migrated all manager code from technical layers
(`src/components/`, `src/stores/`, etc.) to unified domain structure at
`src/workbench/extensions/manager/`
- **Breaking**: Import paths changed for all manager-related modules
(40+ files updated)

## Review Focus

Verify all import path updates are correct and no circular dependencies
introduced. Check that [Vue 3 composition
API](https://vuejs.org/guide/reusability/composables.html) patterns
remain consistent across relocated composables.


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5662-refactor-Migrate-manager-code-to-DDD-structure-2736d73d3650812c87faf6ed0fffb196)
by [Unito](https://www.unito.io)
2025-09-19 00:03:05 -07:00
Christian Byrne
7e115543fa fix: prevent TypeError when nodeDef is undefined in NodeTooltip (#5659)
## Summary

Fix TypeError in NodeTooltip component when `nodeDef` is undefined. This
occurs when hovering over nodes whose type is not found in the
nodeDefStore.

## Changes

- Add optional chaining (`?.`) to `nodeDef.description` access on line
71
- Follows the same defensive pattern used in previous fixes for similar
issues

## Context

This addresses Sentry issue
[CLOUD-FRONTEND-STAGING-1B](https://comfy-org.sentry.io/issues/6829258525/)
which shows 19 occurrences affecting 14 users.

The fix follows the same pattern as previous commits:
-
[290bf52fc](290bf52fc5)
- Fixed similar issue on line 112
-
[e8997a765](e8997a7653)
- Fixed multiple similar issues


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5659-fix-prevent-TypeError-when-nodeDef-is-undefined-in-NodeTooltip-2736d73d3650816e8be3f44889198b58)
by [Unito](https://www.unito.io)
2025-09-18 23:53:58 -07:00
Christian Byrne
80d75bb164 fix TypeError: nodes is not iterable when loading graph (#5660)
## Summary
- Fixes Sentry issue CLOUD-FRONTEND-STAGING-29 (TypeError: nodes is not
iterable)
- Adds defensive guard to check if nodes is valid array before iteration
- Gracefully handles malformed workflow data by skipping node processing

## Root Cause
The `collectMissingNodesAndModels` function in `src/scripts/app.ts:1135`
was attempting to iterate over `nodes` without checking if it was a
valid iterable, causing crashes when workflow data was malformed or
missing the nodes property.

## Fix
Added null/undefined/array validation before the for-loop:
```typescript
if (\!nodes || \!Array.isArray(nodes)) {
  console.warn('Workflow nodes data is missing or invalid, skipping node processing', { nodes, path })
  return
}
```

Fixes CLOUD-FRONTEND-STAGING-29

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5660-fix-TypeError-nodes-is-not-iterable-when-loading-graph-2736d73d365081cfb828d27e59a4811c)
by [Unito](https://www.unito.io)
2025-09-18 23:27:42 -07:00
snomiao
d59885839a fix: correct Claude PR review to use BASE_SHA for accurate diff comparison (#5654)
## Summary
- Fixes the Claude automated PR review comparing against wrong commits
- Updates the comprehensive-pr-review.md command to use `$BASE_SHA`
instead of `origin/$BASE_BRANCH`
- Resolves issue where Claude was reviewing unrelated changes from other
PRs

## Problem
As identified in #5651 (comment
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5651#issuecomment-3310416767),
the Claude automated review was incorrectly analyzing changes that
weren't part of the PR being reviewed. The review was mentioning Turkish
language removal, linkRenderer changes, and other modifications that
weren't in the actual PR diff.

## Root Cause Analysis

### The Issue Explained (from Discord discussion)
When Christian Byrne noticed Claude was referencing things from previous
reviews on other PRs, we investigated and found:

1. **The backport branch was created from origin/main BEFORE Turkish
language support was merged**
   - Branch state: `main.A`
   - Backport changes committed: `main.A.Backport`

2. **Turkish language support was then merged into origin/main**
   - Main branch updated to: `main.A.Turkish`

3. **Claude review workflow checked out `main.A.Backport` and ran git
diff against `origin/main`**
   - This compared: `main.A.Backport <> main.A.Turkish`
   - The diff showed: `+++Backport` changes and `---Turkish` removal
   - Because the common parent of both branches was `main.A`

### Why This Happens
When using `origin/$BASE_BRANCH`, git resolves to the latest commit on
that branch. The diff includes:
1. The PR's actual changes (+++Backport)
2. The reverse of all commits merged to main since the PR was created
(---Turkish)

This causes Claude to review changes that appear as "removals" of code
from other merged PRs, leading to confusing comments about unrelated
code.

## Solution
Changed the git diff commands to use `$BASE_SHA` directly, which GitHub
Actions provides as the exact commit SHA that represents the merge base.
This ensures Claude only reviews the actual changes introduced by the
PR.

### Before (incorrect):
```bash
git diff --name-only "origin/$BASE_BRANCH"  # Compares against latest main
git diff "origin/$BASE_BRANCH"
git diff --name-status "origin/$BASE_BRANCH"
```

### After (correct):
```bash
git diff --name-only "$BASE_SHA"  # Compares against merge base
git diff "$BASE_SHA"
git diff --name-status "$BASE_SHA"
```

## Technical Details

### GitHub Actions Environment Variables
- `BASE_SHA`: The commit SHA of the merge base (where PR branched from
main)
- `BASE_BRANCH`: Not provided by GitHub Actions (this was the bug)
- Using `origin/$BASE_BRANCH` was falling back to comparing against the
latest main commit

### Alternative Approaches Considered
1. **Approach 1**: Rebase/update branch before running Claude review
   - Downside: Changes the PR's commits, not always desirable
2. **Approach 2**: Use BASE_SHA to diff against the merge base 
   - This is what GitHub's PR diff view does
   - Shows only the changes introduced by the PR

## Testing
The BASE_SHA environment variable is already correctly set in the
claude-pr-review.yml workflow (line 88), so this change will work
immediately once merged.

## Impact
- Claude reviews will now be accurate and only analyze the actual PR
changes
- No false positives about "removed" code from other PRs
- More reliable automated PR review process
- Developers won't be confused by comments about code they didn't change

## Verification
You can verify this fix by:
1. Creating a PR from an older branch
2. Merging another PR to main
3. Triggering Claude review with the label
4. Claude should only review the PR's changes, not show removals from
the newly merged commits

## Credits
Thanks to @Christian-Byrne for reporting the issue and @snomiao for the
root cause analysis.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 22:09:20 -07:00
snomiao
cbb0f765b8 feat: enable verbatimModuleSyntax in TypeScript config (#5533)
## Summary
- Enable `verbatimModuleSyntax` compiler option in TypeScript
configuration
- Update all type imports to use explicit `import type` syntax
- This change will Improve tree-shaking and bundler compatibility

## Motivation
The `verbatimModuleSyntax` option ensures that type-only imports are
explicitly marked with the `type` keyword. This:
- Makes import/export intentions clearer
- Improves tree-shaking by helping bundlers identify what can be safely
removed
- Ensures better compatibility with modern bundlers
- Follows TypeScript best practices for module syntax

## Changes
- Added `"verbatimModuleSyntax": true` to `tsconfig.json`
- Updated another 48+ files to use explicit `import type` syntax for
type-only imports
- No functional changes, only import/export syntax improvements

## Test Plan
- [x] TypeScript compilation passes
- [x] Build completes successfully  
- [x] Tests pass
- [ ] No runtime behavior changes

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5533-feat-enable-verbatimModuleSyntax-in-TypeScript-config-26d6d73d36508190b424ef9b379b5130)
by [Unito](https://www.unito.io)
2025-09-18 21:05:56 -07:00
Christian Byrne
726a2fbbc9 feat: add manual dispatch to backport workflow (#5651)
Enables manual backport triggering for scenarios where labels are added
after PR merge.

Adds workflow_dispatch trigger to the backport workflow with support
for:
- Specifying PR number to backport post-merge
- Force rerun option to override duplicate detection  
- Proper handling of multi-version backport scenarios

Solves the issue where adding version labels (e.g., 1.27) after a PR is
already merged and backported (e.g., to 1.26) would not trigger
additional backports.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5651-feat-add-manual-dispatch-to-backport-workflow-2736d73d365081b6ba00c7a43c9ba06b)
by [Unito](https://www.unito.io)
2025-09-18 21:01:07 -07:00
snomiao
553b5aa02b feat: Add Turkish language support (#5438)
## Summary
- Added complete Turkish language translation for ComfyUI Frontend
- Integrated Turkish locale into the i18n system
- Added Turkish as a selectable language option in settings

## Implementation Details
- Added Turkish translation files provided by @naxci1:
  - `src/locales/tr/main.json` - Main UI translations
  - `src/locales/tr/commands.json` - Command translations
  - `src/locales/tr/nodeDefs.json` - Node definitions translations
  - `src/locales/tr/settings.json` - Settings translations
- Updated `src/i18n.ts` to import and register Turkish locale
- Added Turkish option to language selector in
`src/constants/coreSettings.ts`

## Test Plan
- [ ] Verify Turkish translations load correctly
- [ ] Test language switching to/from Turkish
- [ ] Check all UI elements display properly in Turkish
- [ ] Verify node descriptions and tooltips in Turkish
- [ ] Test command palette in Turkish

Fixes #5437

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5438-feat-Add-Turkish-language-support-2686d73d36508184bbf2dc1e0cd15350)
by [Unito](https://www.unito.io)
2025-09-18 19:43:53 -07:00
Benjamin Lu
2ff0d951ed Slot functionality for vue nodes (#5628)
Allows for simple slot functionality in vue nodes mode.

Has:
- Drag new link from slot
- Connect new link from dropping on slot

Now:
- Tests

After:
- Drop on reroute
- Correct link color on connect
- Drop on node
- Hover effects

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5628-Slot-functionality-for-vue-nodes-2716d73d365081c59a3cef7c8a5e539e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 19:35:15 -07:00
Christian Byrne
1f88925144 fix: don't immediately close missing nodes dialog if manager is disabled (#5647)
If manager is disabled, it assumed all missing nodes are installed and
immediately closes the missing nodes warning when loading a workflow
with missing nodes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5647-fix-don-t-immediately-close-missing-nodes-dialog-if-manager-is-disabled-2736d73d36508199a50bca2026528ab6)
by [Unito](https://www.unito.io)
2025-09-18 17:54:37 -07:00
AustinMroz
250433a91a Fix SaveAs (#5643)
Implementing subgraph blueprints (#5139) included changes to saving to
ensure that SaveAs generates a new workflow of the correct type. However
this code failed to utilize the pre-prepared state when performing the
actual save. This produced a couple of problems with both failing to
detach the workflow and failing to apply the correct state

This error is only encountered when using Save As from a non temporary
workflow (one loaded from the workflows sidebar tab).

As this state calculation code is only used in this code path, it has
been moved into the saveAs function of the workflowStore.

Resolves #5592

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5643-Fix-SaveAs-2726d73d3650818faa7af449d1f13c26)
by [Unito](https://www.unito.io)
2025-09-18 16:56:49 -07:00
268 changed files with 17496 additions and 3126 deletions

View File

@@ -67,9 +67,9 @@ This is critical for better file inspection:
Use git locally for much faster analysis:
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
### Step 1.5: Create Analysis Cache

2
.gitattributes vendored
View File

@@ -13,4 +13,4 @@
# Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true
src/types/generatedManagerTypes.ts linguist-generated=true
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true

View File

@@ -4,10 +4,25 @@ on:
pull_request_target:
types: [closed, labeled]
branches: [main]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to backport'
required: true
type: string
force_rerun:
description: 'Force rerun even if backports exist'
required: false
type: boolean
default: false
jobs:
backport:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
if: >
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -15,6 +30,35 @@ jobs:
issues: write
steps:
- name: Validate inputs for manual triggers
if: github.event_name == 'workflow_dispatch'
run: |
# Validate PR number format
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number format. Must be a positive integer."
exit 1
fi
# Validate PR exists and is merged
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
exit 1
fi
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
if [ "$MERGED" != "true" ]; then
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
exit 1
fi
# Validate PR has needs-backport label
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -29,7 +73,7 @@ jobs:
id: check-existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
# Check for existing backport PRs for this PR number
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
@@ -39,6 +83,13 @@ jobs:
exit 0
fi
# For manual triggers with force_rerun, proceed anyway
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "::warning::Force rerun requested - existing backports will be updated"
exit 0
fi
echo "Found existing backport PRs:"
echo "$EXISTING_BACKPORTS"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -50,8 +101,17 @@ jobs:
run: |
# Extract version labels (e.g., "1.24", "1.22")
VERSIONS=""
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
for label in $(echo "$LABELS" | jq -r '.[].name'); do
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# For manual triggers, get labels from the PR
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
# For automatic triggers, extract from PR event
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi
for label in $LABELS; do
# Match version labels like "1.24" (major.minor only)
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Validate the branch exists before adding to list
@@ -75,12 +135,20 @@ jobs:
if: steps.check-existing.outputs.skip != 'true'
id: backport
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
FAILED=""
SUCCESS=""
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_TITLE="${{ github.event.pull_request.title }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}"
@@ -133,10 +201,18 @@ jobs:
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: |
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
else
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r version branch <<< "${backport}"
@@ -165,9 +241,16 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
PR_NUMBER="${{ inputs.pr_number }}"
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r version reason conflicts <<< "${failure}"

View File

@@ -32,11 +32,10 @@ jobs:
with:
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI_devtools
path: ComfyUI/custom_nodes/ComfyUI_devtools
- name: Copy ComfyUI_devtools from frontend repo
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Checkout custom node repository
uses: actions/checkout@v4
with:

View File

@@ -27,12 +27,10 @@ jobs:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
- name: Copy ComfyUI_devtools from frontend repo
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Install pnpm
uses: pnpm/action-setup@v4

1
.gitignore vendored
View File

@@ -44,6 +44,7 @@ components.d.ts
tests-ui/data/*
tests-ui/ComfyUI_examples
tests-ui/workflows/examples
coverage/
# Browser tests
/test-results/

View File

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

View File

@@ -1,17 +1,61 @@
# Admins
* @Comfy-Org/comfy_frontend_devs
# Desktop/Electron
/src/types/desktop/ @webfiltered
/src/constants/desktopDialogs.ts @webfiltered
/src/constants/desktopMaintenanceTasks.ts @webfiltered
/src/stores/electronDownloadStore.ts @webfiltered
/src/extensions/core/electronAdapter.ts @webfiltered
/src/views/DesktopDialogView.vue @webfiltered
/src/components/install/ @webfiltered
/src/components/maintenance/ @webfiltered
/vite.electron.config.mts @webfiltered
# Maintainers
*.md @Comfy-Org/comfy_maintainer
/tests-ui/ @Comfy-Org/comfy_maintainer
/browser_tests/ @Comfy-Org/comfy_maintainer
/.env_example @Comfy-Org/comfy_maintainer
# Common UI Components
/src/components/chip/ @viva-jinyi
/src/components/card/ @viva-jinyi
/src/components/button/ @viva-jinyi
/src/components/input/ @viva-jinyi
# Translations (AIGODLIKE team + shinshin86)
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
# Topbar
/src/components/topbar/ @pythongosssss
# Load 3D extension
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Thumbnail
/src/renderer/core/thumbnail/ @pythongosssss
# Mask Editor extension
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs
# Legacy UI
/scripts/ui/ @pythongosssss
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
/src/services/nodeHelpService.ts @benceruleanlu
# Selection toolbox
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88
# Assets
/src/platform/assets/ @arjansingh
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
# 3D
/src/extensions/core/load3d.ts @jtydhr88
/src/components/load3d/ @jtydhr88
# Manager
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer

View File

@@ -16,9 +16,14 @@ Without this flag, parallel tests will conflict and fail randomly.
### ComfyUI devtools
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
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 adds additional API endpoints and nodes to ComfyUI for browser testing._
For local development, copy the devtools files to your ComfyUI installation:
```bash
cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
```
### Node.js & Playwright Prerequisites
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:

View File

@@ -0,0 +1 @@
{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4}

View File

@@ -0,0 +1,104 @@
import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces'
import type { ComfyPage } from '../fixtures/ComfyPage'
interface FitToViewOptions {
selectionOnly?: boolean
zoom?: number
padding?: number
}
/**
* Instantly fits the canvas view to graph content without waiting for UI animation.
*
* Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented.
*/
export async function fitToViewInstant(
comfyPage: ComfyPage,
options: FitToViewOptions = {}
) {
const { selectionOnly = false, zoom = 0.75, padding = 10 } = options
const rectangles = await comfyPage.page.evaluate<
ReadOnlyRect[] | null,
{ selectionOnly: boolean }
>(
({ selectionOnly }) => {
const app = window['app']
if (!app?.canvas) return null
const canvas = app.canvas
const items = (() => {
if (selectionOnly && canvas.selectedItems?.size) {
return Array.from(canvas.selectedItems)
}
try {
return Array.from(canvas.positionableItems ?? [])
} catch {
return []
}
})()
if (!items.length) return null
const rects: ReadOnlyRect[] = []
for (const item of items) {
const rect = item?.boundingRect
if (!rect) continue
const x = Number(rect[0])
const y = Number(rect[1])
const width = Number(rect[2])
const height = Number(rect[3])
rects.push([x, y, width, height] as const)
}
return rects.length ? rects : null
},
{ selectionOnly }
)
if (!rectangles || rectangles.length === 0) return
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (const [x, y, width, height] of rectangles) {
minX = Math.min(minX, Number(x))
minY = Math.min(minY, Number(y))
maxX = Math.max(maxX, Number(x) + Number(width))
maxY = Math.max(maxY, Number(y) + Number(height))
}
const hasFiniteBounds =
Number.isFinite(minX) &&
Number.isFinite(minY) &&
Number.isFinite(maxX) &&
Number.isFinite(maxY)
if (!hasFiniteBounds) return
const bounds: ReadOnlyRect = [
minX - padding,
minY - padding,
maxX - minX + 2 * padding,
maxY - minY + 2 * padding
]
await comfyPage.page.evaluate(
({ bounds, zoom }) => {
const app = window['app']
if (!app?.canvas) return
const canvas = app.canvas
canvas.ds.fitToBounds(bounds, { zoom })
canvas.setDirty(true, true)
},
{ bounds, zoom }
)
await comfyPage.nextFrame()
}

View File

@@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
test.describe('NodeHeader', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -0,0 +1,221 @@
import type { Locator } from '@playwright/test'
import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../helpers/fitToView'
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
const box = await locator.boundingBox()
if (!box) throw new Error('Slot bounding box not available')
return {
x: box.x + box.width / 2,
y: box.y + box.height / 2
}
}
test.describe('Vue Node Link Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})
test('should show a link dragging out from a slot when dragging on a slot', async ({
comfyPage,
comfyMouse
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const outputSlot = await samplerNode.getOutput(0)
await outputSlot.removeLinks()
await comfyPage.nextFrame()
const slotKey = getSlotKey(String(samplerNode.id), 0, false)
const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`)
await expect(slotLocator).toBeVisible()
const start = await getCenter(slotLocator)
const canvasBox = await comfyPage.canvas.boundingBox()
if (!canvasBox) throw new Error('Canvas bounding box not available')
// Arbitrary value
const dragTarget = {
x: start.x + 180,
y: start.y - 140
}
await comfyMouse.move(start)
await comfyMouse.drag(dragTarget)
await comfyPage.nextFrame()
try {
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-dragging-link.png'
)
} finally {
await comfyMouse.drop()
}
})
test('should create a link when dropping on a compatible slot', async ({
comfyPage
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
expect(vaeNodes.length).toBeGreaterThan(0)
const vaeNode = vaeNodes[0]
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true)
const outputSlot = comfyPage.page.locator(
`[data-slot-key="${outputSlotKey}"]`
)
const inputSlot = comfyPage.page.locator(
`[data-slot-key="${inputSlotKey}"]`
)
await expect(outputSlot).toBeVisible()
await expect(inputSlot).toBeVisible()
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(1)
expect(await vaeInput.getLinkCount()).toBe(1)
const linkDetails = await comfyPage.page.evaluate((sourceId) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return null
const source = graph.getNodeById(sourceId)
if (!source) return null
const linkId = source.outputs[0]?.links?.[0]
if (linkId == null) return null
const link = graph.links[linkId]
if (!link) return null
return {
originId: link.origin_id,
originSlot: link.origin_slot,
targetId: link.target_id,
targetSlot: link.target_slot
}
}, samplerNode.id)
expect(linkDetails).not.toBeNull()
expect(linkDetails).toMatchObject({
originId: samplerNode.id,
originSlot: 0,
targetId: vaeNode.id,
targetSlot: 0
})
})
test('should not create a link when slot types are incompatible', async ({
comfyPage
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
expect(clipNodes.length).toBeGreaterThan(0)
const clipNode = clipNodes[0]
const samplerOutput = await samplerNode.getOutput(0)
const clipInput = await clipNode.getInput(0)
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
const inputSlotKey = getSlotKey(String(clipNode.id), 0, true)
const outputSlot = comfyPage.page.locator(
`[data-slot-key="${outputSlotKey}"]`
)
const inputSlot = comfyPage.page.locator(
`[data-slot-key="${inputSlotKey}"]`
)
await expect(outputSlot).toBeVisible()
await expect(inputSlot).toBeVisible()
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0)
expect(await clipInput.getLinkCount()).toBe(0)
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return 0
const source = graph.getNodeById(sourceId)
if (!source) return 0
return source.outputs[0]?.links?.length ?? 0
}, samplerNode.id)
expect(graphLinkCount).toBe(0)
})
test('should not create a link when dropping onto a slot on the same node', async ({
comfyPage
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const samplerOutput = await samplerNode.getOutput(0)
const samplerInput = await samplerNode.getInput(3)
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true)
const outputSlot = comfyPage.page.locator(
`[data-slot-key="${outputSlotKey}"]`
)
const inputSlot = comfyPage.page.locator(
`[data-slot-key="${inputSlotKey}"]`
)
await expect(outputSlot).toBeVisible()
await expect(inputSlot).toBeVisible()
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0)
expect(await samplerInput.getLinkCount()).toBe(0)
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return 0
const source = graph.getNodeById(sourceId)
if (!source) return 0
return source.outputs[0]?.links?.length ?? 0
}, samplerNode.id)
expect(graphLinkCount).toBe(0)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,44 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Vue Nodes - LOD', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
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 buttonsInNodes = vueNodesContainer.getByRole('button')
await expect(textboxesInNodes.first()).toBeVisible()
await expect(buttonsInNodes.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(buttonsInNodes.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(buttonsInNodes.first()).toBeVisible()
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,47 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Selection', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
const modifiers = [
{ key: 'Control', name: 'ctrl' },
{ key: 'Shift', name: 'shift' }
] as const
for (const { key: modifier, name } of modifiers) {
test(`should allow selecting multiple nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Empty Latent Image').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
await comfyPage.page.getByText('KSampler').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3)
})
test(`should allow de-selecting nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Load Checkpoint').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
}
})

View File

@@ -0,0 +1,49 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const BYPASS_HOTKEY = 'Control+b'
const BYPASS_CLASS = /before:bg-bypass\/60/
test.describe('Vue Node Bypass', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
await checkpointNode.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
const ksamplerNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'KSampler'
})
await checkpointNode.getByText('Load Checkpoint').click()
await ksamplerNode.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await expect(ksamplerNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS)
})
})

View File

@@ -3,7 +3,6 @@
"compilerOptions": {
/* Test files should not be compiled */
"noEmit": true,
// "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true

View File

@@ -1,5 +1,5 @@
import path from 'path'
import { Plugin } from 'vite'
import type { Plugin } from 'vite'
interface ShimResult {
code: string

View File

@@ -1,7 +1,7 @@
import glob from 'fast-glob'
import fs from 'fs-extra'
import { dirname, join } from 'node:path'
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
interface ImportMapSource {
name: string

17
build/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* Build scripts configuration */
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true
},
"include": [
"**/*.ts"
]
}

View File

@@ -33,7 +33,13 @@ export default defineConfig([
},
parserOptions: {
parser: tseslint.parser,
projectService: true,
projectService: {
allowDefaultProject: [
'vite.config.mts',
'vite.electron.config.mts',
'vite.types.config.mts'
]
},
tsConfigRootDir: import.meta.dirname,
ecmaVersion: 2020,
sourceType: 'module',
@@ -77,12 +83,25 @@ export default defineConfig([
'@typescript-eslint/prefer-as-const': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'always'
}
],
'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off',
// Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'],
'vue/multi-word-component-names': 'off', // TODO: fix
'vue/no-template-shadow': 'off', // TODO: fix
/* Toggle on to do additional until we can clean up existing violations.
'vue/no-unused-emit-declarations': 'error',
'vue/no-unused-properties': 'error',
'vue/no-unused-refs': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/no-useless-v-bind': 'error',
// */
'vue/one-component-per-file': 'off', // TODO: fix
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
// Restrict deprecated PrimeVue components

View File

@@ -8,8 +8,8 @@
<link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
<!-- Fullscreen mode on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Fullscreen mode on mobile browsers -->
<meta name="mobile-web-app-capable" content="yes">
<!-- Status bar style (eg. black or transparent) -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">

View File

@@ -22,7 +22,7 @@ const config: KnipConfig = {
],
ignore: [
// Auto generated manager types
'src/types/generatedManagerTypes.ts',
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'src/types/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts',

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.28.0",
"version": "1.28.1",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -27,6 +27,8 @@
"preview": "nx preview",
"lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix",
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
"lint:no-cache": "eslint src",
"lint:fix:no-cache": "eslint src --fix",
"knip": "knip --cache",
@@ -94,6 +96,7 @@
"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",
"zip-dir": "^2.0.0",

3
pnpm-lock.yaml generated
View File

@@ -339,6 +339,9 @@ importers:
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
vue-component-type-helpers:
specifier: ^3.0.7
version: 3.0.7
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))

View File

@@ -9,9 +9,18 @@ import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
interface WidgetInfo {
name?: string
label?: string
}
interface WidgetLabels {
[key: string]: Record<string, { name: string }>
}
test('collect-i18n-node-defs', async ({ comfyPage }) => {
// Mock view route
comfyPage.page.route('**/view**', async (route) => {
await comfyPage.page.route('**/view**', async (route) => {
await route.fulfill({
body: JSON.stringify({})
})
@@ -20,6 +29,7 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
const nodeDefs: ComfyNodeDefImpl[] = (
Object.values(
await comfyPage.page.evaluate(async () => {
// @ts-expect-error - app is dynamically added to window
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
@@ -52,7 +62,7 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
)
async function extractWidgetLabels() {
const nodeLabels = {}
const nodeLabels: WidgetLabels = {}
for (const nodeDef of nodeDefs) {
const inputNames = Object.values(nodeDef.inputs).map(
@@ -65,12 +75,15 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
const widgetsMappings = await comfyPage.page.evaluate(
(args) => {
const [nodeName, displayName, inputNames] = args
// @ts-expect-error - LiteGraph is dynamically added to window
const node = window['LiteGraph'].createNode(nodeName, displayName)
if (!node.widgets?.length) return {}
return Object.fromEntries(
node.widgets
.filter((w) => w?.name && !inputNames.includes(w.name))
.map((w) => [w.name, w.label])
.filter(
(w: WidgetInfo) => w?.name && !inputNames.includes(w.name)
)
.map((w: WidgetInfo) => [w.name, w.label])
)
},
[nodeDef.name, nodeDef.display_name, inputNames]

View File

@@ -72,7 +72,7 @@ function capture(srcLocaleDir: string, tempBaseDir: string) {
const relativePath = file.replace(srcLocaleDir, '')
const targetPath = join(tempBaseDir, relativePath)
ensureDir(dirname(targetPath))
writeFileSync(targetPath, readFileSync(file))
writeFileSync(targetPath, readFileSync(file, 'utf8'))
}
console.log('Captured current locale files to temp/base/')
}

14
scripts/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* Script files configuration */
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true
},
"include": [
"**/*.ts"
]
}

View File

@@ -929,48 +929,6 @@ audio.comfy-audio.empty-audio-widget {
}
/* End of [Desktop] Electron window specific styles */
/* Vue Node LOD (Level of Detail) System */
/* These classes control rendering detail based on zoom level */
/* Minimal LOD (zoom <= 0.4) - Title only for performance */
.lg-node--lod-minimal {
min-height: 32px;
transition: min-height 0.2s ease;
/* Performance optimizations */
text-shadow: none;
backdrop-filter: none;
}
.lg-node--lod-minimal .lg-node-body {
display: none !important;
}
/* Reduced LOD (0.4 < zoom <= 0.8) - Essential widgets, simplified styling */
.lg-node--lod-reduced {
transition: opacity 0.1s ease;
/* Performance optimizations */
text-shadow: none;
}
.lg-node--lod-reduced .lg-widget-label,
.lg-node--lod-reduced .lg-slot-label {
display: none;
}
.lg-node--lod-reduced .lg-slot {
opacity: 0.6;
font-size: 0.75rem;
}
.lg-node--lod-reduced .lg-widget {
margin: 2px 0;
font-size: 0.875rem;
}
/* Full LOD (zoom > 0.8) - Complete detail rendering */
.lg-node--lod-full {
/* Uses default styling - no overrides needed */
}
.lg-node {
/* Disable text selection on all nodes */
@@ -996,23 +954,52 @@ audio.comfy-audio.empty-audio-widget {
will-change: transform;
}
/* Global performance optimizations for LOD */
.lg-node--lod-minimal,
.lg-node--lod-reduced {
/* Remove ALL expensive paint effects */
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
text-shadow: none !important;
-webkit-mask-image: none !important;
mask-image: none !important;
clip-path: none !important;
/* 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;
-webkit-mask-image: none;
mask-image: none;
clip-path: none;
background-image: none;
text-rendering: optimizeSpeed;
border-radius: 0;
contain: layout style;
transition: none;
}
/* Reduce paint complexity for minimal LOD */
.lg-node--lod-minimal {
/* Skip complex borders */
border-radius: 0 !important;
/* Use solid colors only */
background-image: none !important;
.isLOD .lg-node > * {
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 */

98
src/base/common/async.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Cross-browser async utilities for scheduling tasks during browser idle time
* with proper fallbacks for browsers that don't support requestIdleCallback.
*
* Implementation based on:
* https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts
*/
interface IdleDeadline {
didTimeout: boolean
timeRemaining(): number
}
interface IDisposable {
dispose(): void
}
/**
* Internal implementation function that handles the actual scheduling logic.
* Uses feature detection to determine whether to use native requestIdleCallback
* or fall back to setTimeout-based implementation.
*/
let _runWhenIdle: (
targetWindow: any,
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
/**
* Execute the callback during the next browser idle period.
* Falls back to setTimeout-based scheduling in browsers without native support.
*/
export let runWhenGlobalIdle: (
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
// Self-invoking function to set up the idle callback implementation
;(function () {
const safeGlobal: any = globalThis
if (
typeof safeGlobal.requestIdleCallback !== 'function' ||
typeof safeGlobal.cancelIdleCallback !== 'function'
) {
// Fallback implementation for browsers without native support (e.g., Safari)
_runWhenIdle = (_targetWindow, runner, _timeout?) => {
setTimeout(() => {
if (disposed) {
return
}
// Simulate IdleDeadline - give 15ms window (one frame at ~64fps)
const end = Date.now() + 15
const deadline: IdleDeadline = {
didTimeout: true,
timeRemaining() {
return Math.max(0, end - Date.now())
}
}
runner(Object.freeze(deadline))
})
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
}
}
}
} else {
// Native requestIdleCallback implementation
_runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => {
const handle: number = targetWindow.requestIdleCallback(
runner,
typeof timeout === 'number' ? { timeout } : undefined
)
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
targetWindow.cancelIdleCallback(handle)
}
}
}
}
runWhenGlobalIdle = (runner, timeout) =>
_runWhenIdle(globalThis, runner, timeout)
})()

View File

@@ -59,14 +59,13 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useManagerState } from '@/composables/useManagerState'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useDialogStore } from '@/stores/dialogStore'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
import PackInstallButton from './manager/button/PackInstallButton.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
@@ -138,7 +137,7 @@ const allMissingNodesInstalled = computed(() => {
})
// Watch for completion and close dialog
watch(allMissingNodesInstalled, async (allInstalled) => {
if (allInstalled) {
if (allInstalled && showInstallAllButton.value) {
// Use nextTick to ensure state updates are complete
await nextTick()

View File

@@ -43,11 +43,11 @@
<script setup lang="ts">
import Message from 'primevue/message'
import { compare } from 'semver'
import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { compareVersions } from '@/utils/formatUtil'
const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]>
@@ -68,7 +68,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first)
return compareVersions(b, a) // Reversed for descending order
return compare(b, a) // Reversed for descending order
})
})

View File

@@ -1,82 +0,0 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Tag from 'primevue/tag'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import ManagerHeader from './ManagerHeader.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})
describe('ManagerHeader', () => {
const createWrapper = () => {
return mount(ManagerHeader, {
global: {
plugins: [createPinia(), PrimeVue, i18n],
directives: {
tooltip: Tooltip
},
components: {
Tag
}
}
})
}
it('renders the component title', () => {
const wrapper = createWrapper()
expect(wrapper.find('h2').text()).toBe(
enMessages.manager.discoverCommunityContent
)
})
it('displays the legacy manager UI tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
})
it('applies info severity to the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('p-tag-info')
})
it('displays info icon in the tag', () => {
const wrapper = createWrapper()
const icon = wrapper.find('.pi-info-circle')
expect(icon.exists()).toBe(true)
})
it('has cursor-help class on the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('cursor-help')
})
it('has proper structure with flex container', () => {
const wrapper = createWrapper()
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
expect(flexContainer.exists()).toBe(true)
const tag = flexContainer.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
})
})

View File

@@ -1,25 +0,0 @@
<template>
<div class="w-full">
<div class="flex items-center">
<h2 class="text-lg font-normal text-left">
{{ $t('manager.discoverCommunityContent') }}
</h2>
<div class="flex justify-end ml-auto pr-4 pl-2">
<Tag
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
severity="info"
icon="pi pi-info-circle"
:value="$t('manager.legacyManagerUI')"
class="cursor-help ml-2"
:pt="{
root: { class: 'text-xs' }
}"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Tag from 'primevue/tag'
</script>

View File

@@ -33,7 +33,7 @@
<!-- TransformPane for Vue node rendering -->
<TransformPane
v-if="isVueNodesEnabled && comfyApp.canvas && comfyAppReady"
v-if="shouldRenderVueNodes && comfyApp.canvas && comfyAppReady"
:canvas="comfyApp.canvas"
@transform-update="handleTransformUpdate"
@wheel.capture="canvasInteractions.forwardEventToCanvas"
@@ -43,8 +43,6 @@
v-for="nodeData in allNodes"
:key="nodeData.id"
:node-data="nodeData"
:position="nodePositions.get(nodeData.id)"
:size="nodeSizes.get(nodeData.id)"
:readonly="false"
:error="
executionStore.lastExecutionError?.node_id === nodeData.id
@@ -53,9 +51,6 @@
"
:zoom-level="canvasStore.canvas?.ds?.scale || 1"
:data-node-id="nodeData.id"
@node-click="handleNodeSelect"
@update:collapsed="handleNodeCollapse"
@update:title="handleNodeTitleUpdate"
/>
</TransformPane>
@@ -76,9 +71,9 @@
import { useEventListener, whenever } from '@vueuse/core'
import {
computed,
nextTick,
onMounted,
onUnmounted,
provide,
ref,
shallowRef,
watch,
@@ -116,13 +111,11 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useExecutionStateProvider } from '@/renderer/extensions/vueNodes/execution/useExecutionStateProvider'
import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
@@ -169,44 +162,33 @@ const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
// Feature flags
const { shouldRenderVueNodes } = useVueFeatureFlags()
const isVueNodesEnabled = computed(() => shouldRenderVueNodes.value)
// Vue node system
const vueNodeLifecycle = useVueNodeLifecycle(isVueNodesEnabled)
const viewportCulling = useViewportCulling(
isVueNodesEnabled,
vueNodeLifecycle.vueNodeData,
vueNodeLifecycle.nodeDataTrigger,
vueNodeLifecycle.nodeManager
)
const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
const vueNodeLifecycle = useVueNodeLifecycle()
const viewportCulling = useViewportCulling()
const nodePositions = vueNodeLifecycle.nodePositions
const nodeSizes = vueNodeLifecycle.nodeSizes
const allNodes = viewportCulling.allNodes
const handleTransformUpdate = () => {
viewportCulling.handleTransformUpdate()
// TODO: Fix paste position sync in separate PR
vueNodeLifecycle.detectChangesInRAF.value()
const handleVueNodeLifecycleReset = async () => {
if (shouldRenderVueNodes.value) {
vueNodeLifecycle.disposeNodeManagerAndSyncs()
await nextTick()
vueNodeLifecycle.initializeNodeManager()
}
}
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate
// Provide selection state to all Vue nodes
const selectedNodeIds = computed(
() =>
new Set(
canvasStore.selectedItems
.filter((item) => item.id !== undefined)
.map((item) => String(item.id))
)
watch(() => canvasStore.currentGraph, handleVueNodeLifecycleReset)
watch(
() => canvasStore.isInSubgraph,
async (newValue, oldValue) => {
if (oldValue && !newValue) {
useWorkflowStore().updateActiveGraph()
}
await handleVueNodeLifecycleReset()
}
)
provide(SelectedNodeIdsKey, selectedNodeIds)
// Provide execution state to all Vue nodes
useExecutionStateProvider()
const allNodes = viewportCulling.allNodes
const handleTransformUpdate = viewportCulling.handleTransformUpdate
watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
@@ -404,6 +386,7 @@ onMounted(async () => {
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
attachSlotLinkPreviewRenderer(comfyApp.canvas)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false

View File

@@ -33,9 +33,11 @@ const tooltipText = ref('')
const left = ref<string>()
const top = ref<string>()
const hideTooltip = () => (tooltipText.value = '')
function hideTooltip() {
return (tooltipText.value = '')
}
const showTooltip = async (tooltip: string | null | undefined) => {
async function showTooltip(tooltip: string | null | undefined) {
if (!tooltip) return
left.value = comfyApp.canvas.mouse[0] + 'px'
@@ -56,9 +58,9 @@ const showTooltip = async (tooltip: string | null | undefined) => {
}
}
const onIdle = () => {
function onIdle() {
const { canvas } = comfyApp
const node = canvas.node_over
const node = canvas?.node_over
if (!node) return
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
@@ -68,7 +70,7 @@ const onIdle = () => {
ctor.title_mode !== LiteGraph.NO_TITLE &&
canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title
) {
return showTooltip(nodeDef.description)
return showTooltip(nodeDef?.description)
}
if (node.flags?.collapsed) return
@@ -83,7 +85,7 @@ const onIdle = () => {
const inputName = node.inputs[inputSlot].name
const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(inputName)}.tooltip`,
nodeDef.inputs[inputName]?.tooltip ?? ''
nodeDef?.inputs[inputName]?.tooltip ?? ''
)
return showTooltip(translatedTooltip)
}
@@ -97,7 +99,7 @@ const onIdle = () => {
if (outputSlot !== -1) {
const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.outputs.${outputSlot}.tooltip`,
nodeDef.outputs[outputSlot]?.tooltip ?? ''
nodeDef?.outputs[outputSlot]?.tooltip ?? ''
)
return showTooltip(translatedTooltip)
}
@@ -107,7 +109,7 @@ const onIdle = () => {
if (widget && !isDOMWidget(widget)) {
const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(widget.name)}.tooltip`,
nodeDef.inputs[widget.name]?.tooltip ?? ''
nodeDef?.inputs[widget.name]?.tooltip ?? ''
)
// Widget tooltip can be set dynamically, current translation collection does not support this.
return showTooltip(widget.tooltip ?? translatedTooltip)

View File

@@ -11,7 +11,7 @@
:style="`backgroundColor: ${containerStyles.backgroundColor};`"
:pt="{
header: 'hidden',
content: 'px-1 py-1 h-10 px-1 flex flex-row gap-1'
content: 'p-1 h-10 flex flex-row gap-1'
}"
@wheel="canvasInteractions.handleWheel"
>

View File

@@ -142,14 +142,14 @@ import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useManagerState } from '@/composables/useManagerState'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useCommandStore } from '@/stores/commandStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { formatVersionAnchor } from '@/utils/formatUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
// Types
interface MenuItem {

View File

@@ -64,31 +64,29 @@ const litegraphService = useLitegraphService()
const { visible, newSearchBoxEnabled } = storeToRefs(searchBoxStore)
const dismissable = ref(true)
const getNewNodeLocation = (): Point => {
function getNewNodeLocation(): Point {
return triggerEvent
? [triggerEvent.canvasX, triggerEvent.canvasY]
: litegraphService.getCanvasCenter()
}
const nodeFilters = ref<FuseFilterWithValue<ComfyNodeDefImpl, string>[]>([])
const addFilter = (filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) => {
function addFilter(filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) {
nodeFilters.value.push(filter)
}
const removeFilter = (
filter: FuseFilterWithValue<ComfyNodeDefImpl, string>
) => {
function removeFilter(filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) {
nodeFilters.value = nodeFilters.value.filter(
(f) => toRaw(f) !== toRaw(filter)
)
}
const clearFilters = () => {
function clearFilters() {
nodeFilters.value = []
}
const closeDialog = () => {
function closeDialog() {
visible.value = false
}
const canvasStore = useCanvasStore()
const addNode = (nodeDef: ComfyNodeDefImpl) => {
function addNode(nodeDef: ComfyNodeDefImpl) {
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: getNewNodeLocation()
})
@@ -106,7 +104,7 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
window.requestAnimationFrame(closeDialog)
}
const showSearchBox = (e: CanvasPointerEvent | null) => {
function showSearchBox(e: CanvasPointerEvent | null) {
if (newSearchBoxEnabled.value) {
if (e?.pointerType === 'touch') {
setTimeout(() => {
@@ -120,11 +118,12 @@ const showSearchBox = (e: CanvasPointerEvent | null) => {
}
}
const getFirstLink = () =>
canvasStore.getCanvas().linkConnector.renderLinks.at(0)
function getFirstLink() {
return canvasStore.getCanvas().linkConnector.renderLinks.at(0)
}
const nodeDefStore = useNodeDefStore()
const showNewSearchBox = (e: CanvasPointerEvent | null) => {
function showNewSearchBox(e: CanvasPointerEvent | null) {
const firstLink = getFirstLink()
if (firstLink) {
const filter =
@@ -149,7 +148,7 @@ const showNewSearchBox = (e: CanvasPointerEvent | null) => {
}, 300)
}
const showContextMenu = (e: CanvasPointerEvent) => {
function showContextMenu(e: CanvasPointerEvent) {
const firstLink = getFirstLink()
if (!firstLink) return
@@ -226,7 +225,7 @@ watchEffect(() => {
)
})
const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
function canvasEventHandler(e: LiteGraphCanvasEvent) {
if (e.detail.subType === 'empty-double-click') {
showSearchBox(e.detail.originalEvent)
} else if (e.detail.subType === 'group-double-click') {
@@ -249,8 +248,10 @@ const linkReleaseActionShift = computed(() =>
)
// Prevent normal LinkConnector reset (called by CanvasPointer.finally)
const preventDefault = (e: Event) => e.preventDefault()
const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
function preventDefault(e: Event) {
return e.preventDefault()
}
function cancelNextReset(e: CustomEvent<CanvasPointerEvent>) {
e.preventDefault()
const canvas = canvasStore.getCanvas()
@@ -260,7 +261,7 @@ const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
})
}
const handleDroppedOnCanvas = (e: CustomEvent<CanvasPointerEvent>) => {
function handleDroppedOnCanvas(e: CustomEvent<CanvasPointerEvent>) {
disconnectOnReset = true
const action = e.detail.shiftKey
? linkReleaseActionShift.value
@@ -281,7 +282,7 @@ const handleDroppedOnCanvas = (e: CustomEvent<CanvasPointerEvent>) => {
}
// Resets litegraph state
const reset = () => {
function reset() {
listenerController?.abort()
listenerController = null
triggerEvent = null

View File

@@ -82,7 +82,6 @@ import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { useManagerState } from '@/composables/useManagerState'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useColorPaletteService } from '@/services/colorPaletteService'
@@ -90,10 +89,11 @@ import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService()

View File

@@ -34,14 +34,8 @@ export const useCurrentUser = () => {
return null
})
const onUserResolved = (callback: (user: AuthUserInfo) => void) => {
if (resolvedUserInfo.value) {
callback(resolvedUserInfo.value)
}
const stop = whenever(resolvedUserInfo, callback)
return () => stop()
}
const onUserResolved = (callback: (user: AuthUserInfo) => void) =>
whenever(resolvedUserInfo, callback, { immediate: true })
const userDisplayName = computed(() => {
if (isApiKeyLogin.value) {

View File

@@ -2,42 +2,15 @@
* Vue node lifecycle management for LiteGraph integration
* Provides event-driven reactivity with performance optimizations
*/
import { nextTick, reactive } from 'vue'
import { reactive } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
import { type Bounds, QuadTree } from '@/renderer/core/spatial/QuadTree'
import type { WidgetValue } from '@/types/simplifiedWidget'
import type { SpatialIndexDebugInfo } from '@/types/spatialIndex'
import type { LGraph, LGraphNode } from '../../lib/litegraph/src/litegraph'
export interface NodeState {
visible: boolean
dirty: boolean
lastUpdate: number
culled: boolean
}
interface NodeMetadata {
lastRenderTime: number
cachedBounds: DOMRect | null
lodLevel: 'high' | 'medium' | 'low'
spatialIndex?: QuadTree<string>
}
interface PerformanceMetrics {
fps: number
frameTime: number
updateTime: number
nodeCount: number
culledCount: number
callbackUpdateCount: number
rafUpdateCount: number
adaptiveQuality: boolean
}
export interface SafeWidgetData {
name: string
type: string
@@ -63,109 +36,26 @@ export interface VueNodeData {
}
}
interface SpatialMetrics {
queryTime: number
nodesInIndex: number
}
interface GraphNodeManager {
export interface GraphNodeManager {
// Reactive state - safe data extracted from LiteGraph nodes
vueNodeData: ReadonlyMap<string, VueNodeData>
nodeState: ReadonlyMap<string, NodeState>
nodePositions: ReadonlyMap<string, { x: number; y: number }>
nodeSizes: ReadonlyMap<string, { width: number; height: number }>
// Access to original LiteGraph nodes (non-reactive)
getNode(id: string): LGraphNode | undefined
// Lifecycle methods
setupEventListeners(): () => void
cleanup(): void
// Update methods
scheduleUpdate(
nodeId?: string,
priority?: 'critical' | 'normal' | 'low'
): void
forceSync(): void
detectChangesInRAF(): void
// Spatial queries
getVisibleNodeIds(viewportBounds: Bounds): Set<string>
// Performance
performanceMetrics: PerformanceMetrics
spatialMetrics: SpatialMetrics
// Debug
getSpatialIndexDebugInfo(): SpatialIndexDebugInfo | null
}
export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Get layout mutations composable
const { moveNode, resizeNode, createNode, deleteNode, setSource } =
useLayoutMutations()
const { createNode, deleteNode, setSource } = useLayoutMutations()
// Safe reactive data extracted from LiteGraph nodes
const vueNodeData = reactive(new Map<string, VueNodeData>())
const nodeState = reactive(new Map<string, NodeState>())
const nodePositions = reactive(new Map<string, { x: number; y: number }>())
const nodeSizes = reactive(
new Map<string, { width: number; height: number }>()
)
// Non-reactive storage for original LiteGraph nodes
const nodeRefs = new Map<string, LGraphNode>()
// WeakMap for heavy data that auto-GCs when nodes are removed
const nodeMetadata = new WeakMap<LGraphNode, NodeMetadata>()
// Performance tracking
const performanceMetrics = reactive<PerformanceMetrics>({
fps: 0,
frameTime: 0,
updateTime: 0,
nodeCount: 0,
culledCount: 0,
callbackUpdateCount: 0,
rafUpdateCount: 0,
adaptiveQuality: false
})
// Spatial indexing using QuadTree
const spatialIndex = new QuadTree<string>(
{ x: -10000, y: -10000, width: 20000, height: 20000 },
{ maxDepth: 6, maxItemsPerNode: 4 }
)
let lastSpatialQueryTime = 0
// Spatial metrics
const spatialMetrics = reactive<SpatialMetrics>({
queryTime: 0,
nodesInIndex: 0
})
// Update batching
const pendingUpdates = new Set<string>()
const criticalUpdates = new Set<string>()
const lowPriorityUpdates = new Set<string>()
let updateScheduled = false
let batchTimeoutId: number | null = null
// Change detection state
const lastNodesSnapshot = new Map<
string,
{ pos: [number, number]; size: [number, number] }
>()
const attachMetadata = (node: LGraphNode) => {
nodeMetadata.set(node, {
lastRenderTime: performance.now(),
cachedBounds: null,
lodLevel: 'high',
spatialIndex: undefined
})
}
// Extract safe data from LiteGraph node for Vue consumption
const extractVueNodeData = (node: LGraphNode): VueNodeData => {
// Determine subgraph ID - null for root graph, string for subgraphs
@@ -286,7 +176,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
...currentData,
widgets: updatedWidgets
})
performanceMetrics.callbackUpdateCount++
} catch (error) {
// Ignore widget update errors to prevent cascade failures
}
@@ -356,71 +245,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
})
}
// Uncomment when needed for future features
// const getNodeMetadata = (node: LGraphNode): NodeMetadata => {
// let metadata = nodeMetadata.get(node)
// if (!metadata) {
// attachMetadata(node)
// metadata = nodeMetadata.get(node)!
// }
// return metadata
// }
const scheduleUpdate = (
nodeId?: string,
priority: 'critical' | 'normal' | 'low' = 'normal'
) => {
if (nodeId) {
const state = nodeState.get(nodeId)
if (state) state.dirty = true
// Priority queuing
if (priority === 'critical') {
criticalUpdates.add(nodeId)
flush() // Immediate flush for critical updates
return
} else if (priority === 'low') {
lowPriorityUpdates.add(nodeId)
} else {
pendingUpdates.add(nodeId)
}
}
if (!updateScheduled) {
updateScheduled = true
// Adaptive batching strategy
if (pendingUpdates.size > 10) {
// Many updates - batch in nextTick
void nextTick(() => flush())
} else {
// Few updates - small delay for more batching
batchTimeoutId = window.setTimeout(() => flush(), 4)
}
}
}
const flush = () => {
const startTime = performance.now()
if (batchTimeoutId !== null) {
clearTimeout(batchTimeoutId)
batchTimeoutId = null
}
// Clear all pending updates
criticalUpdates.clear()
pendingUpdates.clear()
lowPriorityUpdates.clear()
updateScheduled = false
// Sync with graph state
syncWithGraph()
const endTime = performance.now()
performanceMetrics.updateTime = endTime - startTime
}
const syncWithGraph = () => {
if (!graph?._nodes) return
@@ -431,11 +255,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
if (!currentNodes.has(id)) {
nodeRefs.delete(id)
vueNodeData.delete(id)
nodeState.delete(id)
nodePositions.delete(id)
nodeSizes.delete(id)
lastNodesSnapshot.delete(id)
spatialIndex.remove(id)
}
}
@@ -451,163 +270,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Extract and store safe data for Vue
vueNodeData.set(id, extractVueNodeData(node))
if (!nodeState.has(id)) {
nodeState.set(id, {
visible: true,
dirty: false,
lastUpdate: performance.now(),
culled: false
})
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
attachMetadata(node)
// Add to spatial index
const bounds: Bounds = {
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1]
}
spatialIndex.insert(id, bounds, id)
}
})
// Update performance metrics
performanceMetrics.nodeCount = vueNodeData.size
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
(s) => s.culled
).length
}
// Most performant: Direct position sync without re-setting entire node
// Query visible nodes using QuadTree spatial index
const getVisibleNodeIds = (viewportBounds: Bounds): Set<string> => {
const startTime = performance.now()
// Use QuadTree for fast spatial query
const results: string[] = spatialIndex.query(viewportBounds)
const visibleIds = new Set(results)
lastSpatialQueryTime = performance.now() - startTime
spatialMetrics.queryTime = lastSpatialQueryTime
return visibleIds
}
/**
* Detects position changes for a single node and updates reactive state
*/
const detectPositionChanges = (node: LGraphNode, id: string): boolean => {
const currentPos = nodePositions.get(id)
if (
!currentPos ||
currentPos.x !== node.pos[0] ||
currentPos.y !== node.pos[1]
) {
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
// Push position change to layout store
// Source is already set to 'canvas' in detectChangesInRAF
void moveNode(id, { x: node.pos[0], y: node.pos[1] })
return true
}
return false
}
/**
* Detects size changes for a single node and updates reactive state
*/
const detectSizeChanges = (node: LGraphNode, id: string): boolean => {
const currentSize = nodeSizes.get(id)
if (
!currentSize ||
currentSize.width !== node.size[0] ||
currentSize.height !== node.size[1]
) {
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
// Push size change to layout store
// Source is already set to 'canvas' in detectChangesInRAF
void resizeNode(id, {
width: node.size[0],
height: node.size[1]
})
return true
}
return false
}
/**
* Updates spatial index for a node if bounds changed
*/
const updateSpatialIndex = (node: LGraphNode, id: string): void => {
const bounds: Bounds = {
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1]
}
spatialIndex.update(id, bounds)
}
/**
* Updates performance metrics after change detection
*/
const updatePerformanceMetrics = (
startTime: number,
positionUpdates: number,
sizeUpdates: number
): void => {
const endTime = performance.now()
performanceMetrics.updateTime = endTime - startTime
performanceMetrics.nodeCount = vueNodeData.size
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
(state) => state.culled
).length
spatialMetrics.nodesInIndex = spatialIndex.size
if (positionUpdates > 0 || sizeUpdates > 0) {
performanceMetrics.rafUpdateCount++
}
}
/**
* Main RAF change detection function
*/
const detectChangesInRAF = () => {
const startTime = performance.now()
if (!graph?._nodes) return
let positionUpdates = 0
let sizeUpdates = 0
// Set source for all canvas-driven updates
setSource(LayoutSource.Canvas)
// Process each node for changes
for (const node of graph._nodes) {
const id = String(node.id)
const posChanged = detectPositionChanges(node, id)
const sizeChanged = detectSizeChanges(node, id)
if (posChanged) positionUpdates++
if (sizeChanged) sizeUpdates++
// Update spatial index if geometry changed
if (posChanged || sizeChanged) {
updateSpatialIndex(node, id)
}
}
updatePerformanceMetrics(startTime, positionUpdates, sizeUpdates)
}
/**
@@ -629,32 +292,11 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Extract initial data for Vue (may be incomplete during graph configure)
vueNodeData.set(id, extractVueNodeData(node))
// Set up reactive tracking state
nodeState.set(id, {
visible: true,
dirty: false,
lastUpdate: performance.now(),
culled: false
})
const initializeVueNodeLayout = () => {
// Extract actual positions after configure() has potentially updated them
const nodePosition = { x: node.pos[0], y: node.pos[1] }
const nodeSize = { width: node.size[0], height: node.size[1] }
nodePositions.set(id, nodePosition)
nodeSizes.set(id, nodeSize)
attachMetadata(node)
// Add to spatial index for viewport culling with final positions
const nodeBounds: Bounds = {
x: nodePosition.x,
y: nodePosition.y,
width: nodeSize.width,
height: nodeSize.height
}
spatialIndex.insert(id, nodeBounds, id)
// Add node to layout store with final positions
setSource(LayoutSource.Canvas)
void createNode(id, {
@@ -698,9 +340,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
) => {
const id = String(node.id)
// Remove from spatial index
spatialIndex.remove(id)
// Remove node from layout store
setSource(LayoutSource.Canvas)
void deleteNode(id)
@@ -708,10 +347,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Clean up all tracking references
nodeRefs.delete(id)
vueNodeData.delete(id)
nodeState.delete(id)
nodePositions.delete(id)
nodeSizes.delete(id)
lastNodesSnapshot.delete(id)
// Call original callback if provided
if (originalCallback) {
@@ -733,23 +368,9 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
graph.onNodeRemoved = originalOnNodeRemoved || undefined
graph.onTrigger = originalOnTrigger || undefined
// Clear pending updates
if (batchTimeoutId !== null) {
clearTimeout(batchTimeoutId)
batchTimeoutId = null
}
// Clear all state maps
nodeRefs.clear()
vueNodeData.clear()
nodeState.clear()
nodePositions.clear()
nodeSizes.clear()
lastNodesSnapshot.clear()
pendingUpdates.clear()
criticalUpdates.clear()
lowPriorityUpdates.clear()
spatialIndex.clear()
}
}
@@ -845,18 +466,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
return {
vueNodeData,
nodeState,
nodePositions,
nodeSizes,
getNode,
setupEventListeners,
cleanup,
scheduleUpdate,
forceSync: syncWithGraph,
detectChangesInRAF,
getVisibleNodeIds,
performanceMetrics,
spatialMetrics,
getSpatialIndexDebugInfo: () => spatialIndex.getDebugInfo()
cleanup
}
}

View File

@@ -6,27 +6,18 @@
* 2. Set display none on element to avoid cascade resolution overhead
* 3. Only run when transform changes (event driven)
*/
import { type Ref, computed } from 'vue'
import { useThrottleFn } from '@vueuse/core'
import { computed } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app as comfyApp } from '@/scripts/app'
interface NodeManager {
getNode: (id: string) => any
}
export function useViewportCulling(
isVueNodesEnabled: Ref<boolean>,
vueNodeData: Ref<ReadonlyMap<string, VueNodeData>>,
nodeDataTrigger: Ref<number>,
nodeManager: Ref<NodeManager | null>
) {
export function useViewportCulling() {
const canvasStore = useCanvasStore()
const { vueNodeData, nodeManager } = useVueNodeLifecycle()
const allNodes = computed(() => {
if (!isVueNodesEnabled.value) return []
void nodeDataTrigger.value // Force re-evaluation when nodeManager initializes
return Array.from(vueNodeData.value.values())
})
@@ -34,7 +25,7 @@ export function useViewportCulling(
* Update visibility of all nodes based on viewport
* Queries DOM directly - no cache maintenance needed
*/
const updateVisibility = () => {
function updateVisibility() {
if (!nodeManager.value || !canvasStore.canvas || !comfyApp.canvas) return
const canvas = canvasStore.canvas
@@ -76,31 +67,17 @@ export function useViewportCulling(
}
}
const updateVisibilityDebounced = useThrottleFn(updateVisibility, 20)
// RAF throttling for smooth updates during continuous panning
let rafId: number | null = null
/**
* Handle transform update - called by TransformPane event
* Uses RAF to batch updates for smooth performance
*/
const handleTransformUpdate = () => {
if (!isVueNodesEnabled.value) return
// Cancel previous RAF if still pending
if (rafId !== null) {
cancelAnimationFrame(rafId)
}
// Schedule update in next animation frame
rafId = requestAnimationFrame(() => {
updateVisibility()
rafId = null
function handleTransformUpdate() {
requestAnimationFrame(async () => {
await updateVisibilityDebounced()
})
}
return {
allNodes,
handleTransformUpdate,
updateVisibility
handleTransformUpdate
}
}

View File

@@ -1,20 +1,12 @@
/**
* Vue Node Lifecycle Management Composable
*
* Handles the complete lifecycle of Vue node rendering system including:
* - Node manager initialization and cleanup
* - Layout store synchronization
* - Slot and link sync management
* - Reactive state management for node data, positions, and sizes
* - Memory management and proper cleanup
*/
import { type Ref, computed, readonly, ref, shallowRef, watch } from 'vue'
import { createSharedComposable } from '@vueuse/core'
import { readonly, ref, shallowRef, watch } from 'vue'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type {
NodeState,
GraphNodeManager,
VueNodeData
} from '@/composables/graph/useGraphNodeManager'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -24,13 +16,12 @@ import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
import { app as comfyApp } from '@/scripts/app'
export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
function useVueNodeLifecycleIndividual() {
const canvasStore = useCanvasStore()
const layoutMutations = useLayoutMutations()
const { shouldRenderVueNodes } = useVueFeatureFlags()
const nodeManager = shallowRef<ReturnType<typeof useGraphNodeManager> | null>(
null
)
const nodeManager = shallowRef<GraphNodeManager | null>(null)
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
// Sync management
@@ -40,39 +31,25 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
// Vue node data state
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
const nodeState = ref<ReadonlyMap<string, NodeState>>(new Map())
const nodePositions = ref<ReadonlyMap<string, { x: number; y: number }>>(
new Map()
)
const nodeSizes = ref<ReadonlyMap<string, { width: number; height: number }>>(
new Map()
)
// Change detection function
const detectChangesInRAF = ref<() => void>(() => {})
// Trigger for forcing computed re-evaluation
const nodeDataTrigger = ref(0)
const isNodeManagerReady = computed(() => nodeManager.value !== null)
const initializeNodeManager = () => {
if (!comfyApp.graph || nodeManager.value) return
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
const activeGraph = comfyApp.canvas?.graph || comfyApp.graph
if (!activeGraph || nodeManager.value) return
// Initialize the core node manager
const manager = useGraphNodeManager(comfyApp.graph)
const manager = useGraphNodeManager(activeGraph)
nodeManager.value = manager
cleanupNodeManager.value = manager.cleanup
// Use the manager's data maps
vueNodeData.value = manager.vueNodeData
nodeState.value = manager.nodeState
nodePositions.value = manager.nodePositions
nodeSizes.value = manager.nodeSizes
detectChangesInRAF.value = manager.detectChangesInRAF
// Initialize layout system with existing nodes
const nodes = comfyApp.graph._nodes.map((node: LGraphNode) => ({
// Initialize layout system with existing nodes from active graph
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
id: node.id.toString(),
pos: [node.pos[0], node.pos[1]] as [number, number],
size: [node.size[0], node.size[1]] as [number, number]
@@ -80,7 +57,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
layoutStore.initializeFromLiteGraph(nodes)
// Seed reroutes into the Layout Store so hit-testing uses the new path
for (const reroute of comfyApp.graph.reroutes.values()) {
for (const reroute of activeGraph.reroutes.values()) {
const [x, y] = reroute.pos
const parent = reroute.parentId ?? undefined
const linkIds = Array.from(reroute.linkIds)
@@ -88,7 +65,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
}
// Seed existing links into the Layout Store (topology only)
for (const link of comfyApp.graph._links.values()) {
for (const link of activeGraph._links.values()) {
layoutMutations.createLink(
link.id,
link.origin_id,
@@ -132,17 +109,13 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
// Reset reactive maps to clean state
vueNodeData.value = new Map()
nodeState.value = new Map()
nodePositions.value = new Map()
nodeSizes.value = new Map()
// Reset change detection function
detectChangesInRAF.value = () => {}
}
// Watch for Vue nodes enabled state changes
watch(
() => isVueNodesEnabled.value && Boolean(comfyApp.graph),
() =>
shouldRenderVueNodes.value &&
Boolean(comfyApp.canvas?.graph || comfyApp.graph),
(enabled) => {
if (enabled) {
initializeNodeManager()
@@ -155,7 +128,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
// Consolidated watch for slot layout sync management
watch(
[() => canvasStore.canvas, () => isVueNodesEnabled.value],
[() => canvasStore.canvas, () => shouldRenderVueNodes.value],
([canvas, vueMode], [, oldVueMode]) => {
const modeChanged = vueMode !== oldVueMode
@@ -187,7 +160,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
// Handle case where Vue nodes are enabled but graph starts empty
const setupEmptyGraphListener = () => {
if (
isVueNodesEnabled.value &&
shouldRenderVueNodes.value &&
comfyApp.graph &&
!nodeManager.value &&
comfyApp.graph._nodes.length === 0
@@ -198,7 +171,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
comfyApp.graph.onNodeAdded = originalOnNodeAdded
// Initialize node manager if needed
if (isVueNodesEnabled.value && !nodeManager.value) {
if (shouldRenderVueNodes.value && !nodeManager.value) {
initializeNodeManager()
}
@@ -229,13 +202,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
return {
vueNodeData,
nodeState,
nodePositions,
nodeSizes,
nodeDataTrigger: readonly(nodeDataTrigger),
nodeManager: readonly(nodeManager),
detectChangesInRAF: readonly(detectChangesInRAF),
isNodeManagerReady,
// Lifecycle methods
initializeNodeManager,
@@ -244,3 +211,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
cleanup
}
}
export const useVueNodeLifecycle = createSharedComposable(
useVueNodeLifecycleIndividual
)

View File

@@ -1548,6 +1548,71 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
},
ByteDanceImageReferenceNode: {
displayPrice: byteDanceVideoPricingCalculator
},
WanTextToVideoApi: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'size'
) as IComboWidget
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
const seconds = parseFloat(String(durationWidget.value))
const resolutionStr = String(resolutionWidget.value).toLowerCase()
const resKey = resolutionStr.includes('1080')
? '1080p'
: resolutionStr.includes('720')
? '720p'
: resolutionStr.includes('480')
? '480p'
: resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? ''
const pricePerSecond: Record<string, number> = {
'480p': 0.05,
'720p': 0.1,
'1080p': 0.15
}
const pps = pricePerSecond[resKey]
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
const cost = (pps * seconds).toFixed(2)
return `$${cost}/Run`
}
},
WanImageToVideoApi: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'resolution'
) as IComboWidget
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
const seconds = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget.value).trim().toLowerCase()
const pricePerSecond: Record<string, number> = {
'480p': 0.05,
'720p': 0.1,
'1080p': 0.15
}
const pps = pricePerSecond[resolution]
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
const cost = (pps * seconds).toFixed(2)
return `$${cost}/Run`
}
},
WanTextToImageApi: {
displayPrice: '$0.03/Run'
}
}
@@ -1647,7 +1712,9 @@ export const useNodePricing = () => {
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution']
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
WanTextToVideoApi: ['duration', 'size'],
WanImageToVideoApi: ['duration', 'resolution']
}
return widgetMap[nodeType] || []
}

View File

@@ -2,9 +2,9 @@ import { whenever } from '@vueuse/core'
import { computed, onUnmounted, ref } from 'vue'
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
const comfyManagerStore = useComfyManagerStore()

View File

@@ -5,10 +5,10 @@ import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { components } from '@/types/comfyRegistryTypes'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Composable to find missing NodePacks from workflow

View File

@@ -2,7 +2,7 @@ import { get, useAsyncState } from '@vueuse/core'
import type { Ref } from 'vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
/**
* Handles fetching node packs from the registry given a list of node pack IDs

View File

@@ -1,8 +1,8 @@
import { compare, valid } from 'semver'
import { computed } from 'vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
export const usePackUpdateStatus = (
nodePack: components['schemas']['Node']
@@ -16,14 +16,14 @@ export const usePackUpdateStatus = (
const latestVersion = computed(() => nodePack.latest_version?.version)
const isNightlyPack = computed(
() => !!installedVersion.value && !isSemVer(installedVersion.value)
() => !!installedVersion.value && !valid(installedVersion.value)
)
const isUpdateAvailable = computed(() => {
if (!isInstalled.value || isNightlyPack.value || !latestVersion.value) {
return false
}
return compareVersions(latestVersion.value, installedVersion.value) > 0
return compare(latestVersion.value, installedVersion.value) > 0
})
return {

View File

@@ -1,7 +1,7 @@
import { type Ref, computed } from 'vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type NodePack = components['schemas']['Node']

View File

@@ -1,9 +1,9 @@
import { compare, valid } from 'semver'
import { computed, onMounted } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Composable to find NodePacks that have updates available
@@ -25,13 +25,13 @@ export const useUpdateAvailableNodes = () => {
)
const latestVersion = pack.latest_version?.version
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion)
const isNightlyPack = !!installedVersion && !valid(installedVersion)
if (isNightlyPack || !latestVersion) {
return false
}
return compareVersions(latestVersion, installedVersion) > 0
return compare(latestVersion, installedVersion) > 0
}
// Same filtering logic as ManagerDialogContent.vue

View File

@@ -7,9 +7,9 @@ import { app } from '@/scripts/app'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type WorkflowPack = {
id:

View File

@@ -5,9 +5,7 @@ import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import config from '@/config'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { SystemStats } from '@/types'
@@ -28,6 +26,8 @@ import {
satisfiesVersion,
utilCheckVersionCompatibility
} from '@/utils/versionUtil'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Composable for conflict detection system.
@@ -641,7 +641,9 @@ export function useConflictDetection() {
async function initializeConflictDetection(): Promise<void> {
try {
// Check if manager is new Manager before proceeding
const { useManagerState } = await import('@/composables/useManagerState')
const { useManagerState } = await import(
'@/workbench/extensions/manager/composables/useManagerState'
)
const managerState = useManagerState()
if (!managerState.isNewManagerUI.value) {

View File

@@ -1,6 +1,5 @@
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import {
DEFAULT_DARK_COLOR_PALETTE,
@@ -41,12 +40,16 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import {
getAllNonIoNodesInSubgraph,
getExecutionIdsForSelectedNodes
} from '@/utils/graphTraversalUtil'
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
import {
ManagerUIState,
useManagerState
} from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const moveSelectedNodesVersionAdded = '1.22.2'

View File

@@ -2,9 +2,9 @@ import { type ComputedRef, computed, unref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/**
* Extracting import failed conflicts from conflict list

View File

@@ -5,9 +5,9 @@ import { computed, ref, watch } from 'vue'
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'
import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
import type { SearchAttribute } from '@/types/algoliaTypes'
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { QuerySuggestion, SearchMode } from '@/types/searchServiceTypes'
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type RegistryNodePack = components['schemas']['Node']

View File

@@ -3,7 +3,7 @@ import { onUnmounted, ref } from 'vue'
import type { LogsWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import type { components } from '@/types/generatedManagerTypes'
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
const LOGS_MESSAGE_TYPE = 'logs'
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'

View File

@@ -2,16 +2,17 @@
* Vue-related feature flags composable
* Manages local settings-driven flags and LiteGraph integration
*/
import { createSharedComposable } from '@vueuse/core'
import { computed, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { LiteGraph } from '../lib/litegraph/src/litegraph'
export const useVueFeatureFlags = () => {
function useVueFeatureFlagsIndividual() {
const settingStore = useSettingStore()
const isVueNodesEnabled = computed(() => {
const shouldRenderVueNodes = computed(() => {
try {
return settingStore.get('Comfy.VueNodes.Enabled') ?? false
} catch {
@@ -19,20 +20,20 @@ export const useVueFeatureFlags = () => {
}
})
// Whether Vue nodes should render
const shouldRenderVueNodes = computed(() => isVueNodesEnabled.value)
// Sync the Vue nodes flag with LiteGraph global settings
const syncVueNodesFlag = () => {
LiteGraph.vueNodesMode = isVueNodesEnabled.value
}
// Watch for changes and update LiteGraph immediately
watch(isVueNodesEnabled, syncVueNodesFlag, { immediate: true })
watch(
shouldRenderVueNodes,
() => {
LiteGraph.vueNodesMode = shouldRenderVueNodes.value
},
{ immediate: true }
)
return {
isVueNodesEnabled,
shouldRenderVueNodes,
syncVueNodesFlag
shouldRenderVueNodes
}
}
export const useVueFeatureFlags = createSharedComposable(
useVueFeatureFlagsIndividual
)

View File

@@ -122,14 +122,14 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
key: '.'
},
commandId: 'Comfy.Canvas.FitView',
targetElementId: 'graph-canvas'
targetElementId: 'graph-canvas-container'
},
{
combo: {
key: 'p'
},
commandId: 'Comfy.Canvas.ToggleSelected.Pin',
targetElementId: 'graph-canvas'
targetElementId: 'graph-canvas-container'
},
{
combo: {
@@ -137,7 +137,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
targetElementId: 'graph-canvas'
targetElementId: 'graph-canvas-container'
},
{
combo: {
@@ -145,7 +145,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
targetElementId: 'graph-canvas'
targetElementId: 'graph-canvas-container'
},
{
combo: {
@@ -153,7 +153,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
targetElementId: 'graph-canvas'
targetElementId: 'graph-canvas-container'
},
{
combo: {

View File

@@ -11,16 +11,16 @@ import {
} from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import {
ComfyLink,
ComfyNode,
ComfyWorkflowJSON
type ComfyLink,
type ComfyNode,
type ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { useDialogService } from '@/services/dialogService'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { ComfyExtension } from '@/types/comfy'
import { type ComfyExtension } from '@/types/comfy'
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
import { GROUP } from '@/utils/executableGroupNodeDto'
import { deserialiseAndCreate, serialise } from '@/utils/vintageClipboard'

View File

@@ -9,7 +9,7 @@ import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { t } from '@/i18n'
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { api } from '@/scripts/api'
import { ComfyApp, app } from '@/scripts/app'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'

View File

@@ -1,9 +1,9 @@
import * as THREE from 'three'
import {
AnimationItem,
AnimationManagerInterface,
EventManagerInterface
type AnimationItem,
type AnimationManagerInterface,
type EventManagerInterface
} from '@/extensions/core/load3d/interfaces'
export class AnimationManager implements AnimationManagerInterface {

View File

@@ -2,11 +2,11 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
CameraManagerInterface,
CameraState,
CameraType,
EventManagerInterface,
NodeStorageInterface
type CameraManagerInterface,
type CameraState,
type CameraType,
type EventManagerInterface,
type NodeStorageInterface
} from './interfaces'
export class CameraManager implements CameraManagerInterface {

View File

@@ -2,9 +2,9 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
ControlsManagerInterface,
EventManagerInterface,
NodeStorageInterface
type ControlsManagerInterface,
type EventManagerInterface,
type NodeStorageInterface
} from './interfaces'
export class ControlsManager implements ControlsManagerInterface {

View File

@@ -1,4 +1,4 @@
import { EventCallback, EventManagerInterface } from './interfaces'
import { type EventCallback, type EventManagerInterface } from './interfaces'
export class EventManager implements EventManagerInterface {
private listeners: { [key: string]: EventCallback[] } = {}

View File

@@ -1,6 +1,9 @@
import * as THREE from 'three'
import { EventManagerInterface, LightingManagerInterface } from './interfaces'
import {
type EventManagerInterface,
type LightingManagerInterface
} from './interfaces'
export class LightingManager implements LightingManagerInterface {
lights: THREE.Light[] = []

View File

@@ -1,7 +1,7 @@
import * as THREE from 'three'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { CameraManager } from './CameraManager'
import { ControlsManager } from './ControlsManager'
@@ -16,11 +16,11 @@ import { SceneManager } from './SceneManager'
import { SceneModelManager } from './SceneModelManager'
import { ViewHelperManager } from './ViewHelperManager'
import {
CameraState,
CaptureResult,
Load3DOptions,
MaterialMode,
UpDirection
type CameraState,
type CaptureResult,
type Load3DOptions,
type MaterialMode,
type UpDirection
} from './interfaces'
class Load3d {

View File

@@ -4,7 +4,7 @@ import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { AnimationManager } from './AnimationManager'
import Load3d from './Load3d'
import { Load3DOptions } from './interfaces'
import { type Load3DOptions } from './interfaces'
class Load3dAnimation extends Load3d {
private animationManager: AnimationManager

View File

@@ -9,9 +9,9 @@ import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import {
EventManagerInterface,
LoaderManagerInterface,
ModelManagerInterface
type EventManagerInterface,
type LoaderManagerInterface,
type ModelManagerInterface
} from './interfaces'
export class LoaderManager implements LoaderManagerInterface {

View File

@@ -1,6 +1,6 @@
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { NodeStorageInterface } from './interfaces'
import { type NodeStorageInterface } from './interfaces'
export class NodeStorage implements NodeStorageInterface {
private node: LGraphNode

View File

@@ -1,7 +1,10 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
import {
type EventManagerInterface,
type PreviewManagerInterface
} from './interfaces'
export class PreviewManager implements PreviewManagerInterface {
previewCamera: THREE.Camera

View File

@@ -1,6 +1,6 @@
import * as THREE from 'three'
import { EventManagerInterface } from './interfaces'
import { type EventManagerInterface } from './interfaces'
export class RecordingManager {
private mediaRecorder: MediaRecorder | null = null

View File

@@ -2,7 +2,10 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import Load3dUtils from './Load3dUtils'
import { EventManagerInterface, SceneManagerInterface } from './interfaces'
import {
type EventManagerInterface,
type SceneManagerInterface
} from './interfaces'
export class SceneManager implements SceneManagerInterface {
scene: THREE.Scene

View File

@@ -2,7 +2,7 @@ import * as THREE from 'three'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
@@ -11,11 +11,11 @@ import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShad
import { ConditionalLineMaterial } from './conditional-lines/Lines2/ConditionalLineMaterial'
import { ConditionalLineSegmentsGeometry } from './conditional-lines/Lines2/ConditionalLineSegmentsGeometry'
import {
EventManagerInterface,
Load3DOptions,
MaterialMode,
ModelManagerInterface,
UpDirection
type EventManagerInterface,
type Load3DOptions,
type MaterialMode,
type ModelManagerInterface,
type UpDirection
} from './interfaces'
export class SceneModelManager implements ModelManagerInterface {

View File

@@ -2,7 +2,10 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { NodeStorageInterface, ViewHelperManagerInterface } from './interfaces'
import {
type NodeStorageInterface,
type ViewHelperManagerInterface
} from './interfaces'
export class ViewHelperManager implements ViewHelperManagerInterface {
viewHelper: ViewHelper = {} as ViewHelper

View File

@@ -2,13 +2,13 @@ import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
export type Load3DNodeType = 'Load3D' | 'Preview3D'

View File

@@ -4,7 +4,7 @@ https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
*/
import { app } from '@/scripts/app'
import { DOMWidget } from '@/scripts/domWidget'
import { type DOMWidget } from '@/scripts/domWidget'
import { ComfyWidgets } from '@/scripts/widgets'
import { useExtensionService } from '@/services/extensionService'

View File

@@ -2,7 +2,7 @@ import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'

View File

@@ -15,7 +15,7 @@ import type { ResultItemType } from '@/schemas/apiSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { DOMWidget } from '@/scripts/domWidget'
import { useAudioService } from '@/services/audioService'
import { NodeLocatorId } from '@/types'
import { type NodeLocatorId } from '@/types'
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
import { api } from '../../scripts/api'

View File

@@ -1,6 +1,6 @@
import {
ComfyNodeDef,
InputSpec,
type ComfyNodeDef,
type InputSpec,
isComboInputSpecV1
} from '@/schemas/nodeDefSchema'

View File

@@ -28,6 +28,10 @@ import ruCommands from './locales/ru/commands.json' with { type: 'json' }
import ru from './locales/ru/main.json' with { type: 'json' }
import ruNodes from './locales/ru/nodeDefs.json' with { type: 'json' }
import ruSettings from './locales/ru/settings.json' with { type: 'json' }
import trCommands from './locales/tr/commands.json' with { type: 'json' }
import tr from './locales/tr/main.json' with { type: 'json' }
import trNodes from './locales/tr/nodeDefs.json' with { type: 'json' }
import trSettings from './locales/tr/settings.json' with { type: 'json' }
import zhTWCommands from './locales/zh-TW/commands.json' with { type: 'json' }
import zhTW from './locales/zh-TW/main.json' with { type: 'json' }
import zhTWNodes from './locales/zh-TW/nodeDefs.json' with { type: 'json' }
@@ -55,7 +59,8 @@ const messages = {
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings)
ar: buildLocale(ar, arNodes, arCommands, arSettings),
tr: buildLocale(tr, trNodes, trCommands, trSettings)
}
export const i18n = createI18n({
@@ -71,7 +76,7 @@ export const i18n = createI18n({
})
/** Convenience shorthand: i18n.global */
export const { t, te } = i18n.global
export const { t, te, d } = i18n.global
/**
* Safe translation function that returns the fallback message if the key is not found.

View File

@@ -87,6 +87,7 @@ import type { PickNevers } from './types/utility'
import type { IBaseWidget } from './types/widgets'
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
import { findFirstNode, getAllNestedItems } from './utils/collections'
import { resolveConnectingLinkColor } from './utils/linkColors'
import type { UUID } from './utils/uuid'
import { BaseWidget } from './widgets/BaseWidget'
import { toConcreteWidget } from './widgets/widgetMap'
@@ -4031,6 +4032,18 @@ export class LGraphCanvas
// TODO: Report failures, i.e. `failedNodes`
const newPositions = created.map((node) => ({
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size?.[0] ?? 100,
height: node.size?.[1] ?? 200
}
}))
layoutStore.batchUpdateNodeBounds(newPositions)
this.selectItems(created)
graph.afterChange()
@@ -4716,29 +4729,20 @@ export class LGraphCanvas
const connShape = fromSlot.shape
const connType = fromSlot.type
const colour =
connType === LiteGraph.EVENT
? LiteGraph.EVENT_LINK_COLOR
: LiteGraph.CONNECTING_LINK_COLOR
const colour = resolveConnectingLinkColor(connType)
// the connection being dragged by the mouse
if (this.linkRenderer) {
this.linkRenderer.renderLinkDirect(
this.linkRenderer.renderDraggingLink(
ctx,
pos,
highlightPos,
null,
false,
null,
colour,
fromDirection,
dragDirection,
{
...this.buildLinkRenderContext(),
linkMarkerShape: LinkMarkerShape.None
},
{
disabled: false
}
)
}

View File

@@ -205,7 +205,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
linkSegment: LinkSegment
): Reroute[] {
if (!linkSegment.parentId) return []
if (linkSegment.parentId === undefined) return []
return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? []
}
@@ -229,7 +229,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
linkSegment: LinkSegment,
rerouteId: RerouteId
): Reroute | null | undefined {
if (!linkSegment.parentId) return
if (linkSegment.parentId === undefined) return
return network.reroutes
.get(linkSegment.parentId)
?.findNextReroute(rerouteId)
@@ -498,7 +498,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
target_slot: this.target_slot,
type: this.type
}
if (this.parentId) copy.parentId = this.parentId
if (this.parentId !== undefined) copy.parentId = this.parentId
return copy
}
}

View File

@@ -82,6 +82,7 @@ export interface Positionable extends Parent<Positionable>, HasBoundingRect {
* @default 0,0
*/
readonly pos: Point
readonly size?: Size
/** true if this object is part of the selection, otherwise false. */
selected?: boolean

View File

@@ -48,7 +48,6 @@ export interface LinkReleaseContextExtended {
links: ConnectingLink[]
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface LiteGraphCanvasEvent extends CustomEvent<CanvasEventDetail> {}
export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
@@ -140,7 +139,7 @@ export { BaseWidget } from './widgets/BaseWidget'
export { LegacyWidget } from './widgets/LegacyWidget'
export { isComboWidget } from './widgets/widgetMap'
export { isComboWidget, isAssetWidget } from './widgets/widgetMap'
// Additional test-specific exports
export { LGraphButton } from './LGraphButton'
export { MovingOutputLink } from './canvas/MovingOutputLink'

View File

@@ -0,0 +1,13 @@
import type { CanvasColour, ISlotType } from '../interfaces'
import { LiteGraph } from '../litegraph'
/**
* Resolve the colour used while rendering or previewing a connection of a given slot type.
*/
export function resolveConnectingLinkColor(
type: ISlotType | undefined
): CanvasColour {
return type === LiteGraph.EVENT
? LiteGraph.EVENT_LINK_COLOR
: LiteGraph.CONNECTING_LINK_COLOR
}

View File

@@ -13,6 +13,22 @@ export class AssetWidget
this.value = widget.value?.toString() ?? ''
}
override set value(value: IAssetWidget['value']) {
const oldValue = this.value
super.value = value
// Force canvas redraw when value changes to show update immediately
if (oldValue !== value && this.node.graph?.list_of_graphcanvas) {
for (const canvas of this.node.graph.list_of_graphcanvas) {
canvas.setDirty(true)
}
}
}
override get value(): IAssetWidget['value'] {
return super.value
}
override get _displayValue(): string {
return String(this.value) //FIXME: Resolve asset name
}

View File

@@ -1,5 +1,6 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type {
IAssetWidget,
IBaseWidget,
IComboWidget,
IWidget,
@@ -132,4 +133,9 @@ export function isComboWidget(widget: IBaseWidget): widget is IComboWidget {
return widget.type === 'combo'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IAssetWidget}. */
export function isAssetWidget(widget: IBaseWidget): widget is IAssetWidget {
return widget.type === 'asset'
}
// #endregion Type Guards

View File

@@ -24,7 +24,7 @@ Add your language code to the `outputLocales` array:
```javascript
module.exports = defineConfig({
// ... existing config
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'], // Add your language here
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'tr'], // Add your language here
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

View File

@@ -11,6 +11,7 @@ Our project supports multiple languages using `vue-i18n`. This allows users arou
- ko (한국어)
- fr (Français)
- es (Español)
- tr (Türkçe)
## How to Add a New Language

View File

@@ -1873,6 +1873,13 @@
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Search assets...",
"allModels": "All Models",
"unknown": "Unknown"
"unknown": "Unknown",
"fileFormats": "File formats",
"baseModels": "Base models",
"sortBy": "Sort by",
"sortAZ": "A-Z",
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular"
}
}

View File

@@ -0,0 +1,312 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Güncellemeleri Kontrol Et"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Özel Düğümler Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "Girişler Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "Kayıtlar Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "extra_model_paths.yaml dosyasını aç"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "Modeller Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": ıktılar Klasörünü Aç"
},
"Comfy-Desktop_OpenDevTools": {
"label": "Geliştirici Araçlarını Aç"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Masaüstü Kullanıcı Kılavuzu"
},
"Comfy-Desktop_Quit": {
"label": ık"
},
"Comfy-Desktop_Reinstall": {
"label": "Yeniden Yükle"
},
"Comfy-Desktop_Restart": {
"label": "Yeniden Başlat"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
},
"Comfy_BrowseTemplates": {
"label": "Şablonlara Gözat"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Seçili Öğeleri Sil"
},
"Comfy_Canvas_FitView": {
"label": "Görünümü seçili düğümlere sığdır"
},
"Comfy_Canvas_Lock": {
"label": "Tuvali Kilitle"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Seçili Düğümleri Aşağı Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Left": {
"label": "Seçili Düğümleri Sola Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Right": {
"label": "Seçili Düğümleri Sağa Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Up": {
"label": "Seçili Düğümleri Yukarı Taşı"
},
"Comfy_Canvas_ResetView": {
"label": "Görünümü Sıfırla"
},
"Comfy_Canvas_Resize": {
"label": "Seçili Düğümleri Yeniden Boyutlandır"
},
"Comfy_Canvas_ToggleLinkVisibility": {
"label": "Tuval Bağlantı Görünürlüğünü Aç/Kapat"
},
"Comfy_Canvas_ToggleLock": {
"label": "Tuval Kilidini Aç/Kapat"
},
"Comfy_Canvas_ToggleMinimap": {
"label": "Mini Haritayı Aç/Kapat"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Seçili Öğeleri Sabitle/Sabitlemeyi Kaldır"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "Seçili Düğümleri Atla/Geri Al"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "Seçili Düğümleri Daralt/Genişlet"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "Seçili Düğümleri Sessize Al/Sesi Aç"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Seçili Düğümleri Sabitle/Sabitlemeyi Kaldır"
},
"Comfy_Canvas_Unlock": {
"label": "Tuvalin Kilidini Aç"
},
"Comfy_Canvas_ZoomIn": {
"label": "Yakınlaştır"
},
"Comfy_Canvas_ZoomOut": {
"label": "Uzaklaştır"
},
"Comfy_ClearPendingTasks": {
"label": "Bekleyen Görevleri Temizle"
},
"Comfy_ClearWorkflow": {
"label": "İş Akışını Temizle"
},
"Comfy_ContactSupport": {
"label": "Destekle İletişime Geç"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Model Seçiciyi Göster (Geliştirici)"
},
"Comfy_DuplicateWorkflow": {
"label": "Mevcut İş Akışını Çoğalt"
},
"Comfy_ExportWorkflow": {
"label": "İş Akışını Dışa Aktar"
},
"Comfy_ExportWorkflowAPI": {
"label": "İş Akışını Dışa Aktar (API Formatı)"
},
"Comfy_Feedback": {
"label": "Geri Bildirim Ver"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Seçimi Alt Grafiğe Dönüştür"
},
"Comfy_Graph_ExitSubgraph": {
"label": "Alt Grafikten Çık"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Grubu İçeriğe Sığdır"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "Seçili Düğümleri Gruplandır"
},
"Comfy_Graph_UnpackSubgraph": {
"label": "Seçili Alt Grafiği Aç"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "Seçili düğümleri grup düğümüne dönüştür"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "Grup düğümlerini yönet"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "Seçili grup düğümlerinin grubunu çöz"
},
"Comfy_Help_AboutComfyUI": {
"label": "ComfyUI Hakkında'yı Aç"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "Comfy-Org Discord'unu Aç"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUI Belgelerini Aç"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "ComfyUI Forumunu Aç"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUI Sorunlarını Aç"
},
"Comfy_Interrupt": {
"label": "Kes"
},
"Comfy_LoadDefaultWorkflow": {
"label": "Varsayılan İş Akışını Yükle"
},
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Özel Düğüm Yöneticisi"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Özel Düğümler (Eski)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Yönetici Menüsü (Eski)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Eksik Özel Düğümleri Yükle"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Özel Düğüm Güncellemelerini Kontrol Et"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Özel Düğüm Yöneticisi İlerleme Çubuğunu Aç/Kapat"
},
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Maske Düzenleyicide Fırça Boyutunu Azalt"
},
"Comfy_MaskEditor_BrushSize_Increase": {
"label": "Maske Düzenleyicide Fırça Boyutunu Artır"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Seçili Düğüm için Maske Düzenleyiciyi Aç"
},
"Comfy_Memory_UnloadModels": {
"label": "Modelleri Boşalt"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Modelleri ve Yürütme Önbelleğini Boşalt"
},
"Comfy_NewBlankWorkflow": {
"label": "Yeni Boş İş Akışı"
},
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenManagerDialog": {
"label": "Yönetici"
},
"Comfy_OpenWorkflow": {
"label": "İş Akışını Aç"
},
"Comfy_PublishSubgraph": {
"label": "Alt Grafiği Yayınla"
},
"Comfy_QueuePrompt": {
"label": "İstemi Kuyruğa Al"
},
"Comfy_QueuePromptFront": {
"label": "İstemi Kuyruğa Al (Ön)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Seçili Çıktı Düğümlerini Kuyruğa Al"
},
"Comfy_Redo": {
"label": "Yinele"
},
"Comfy_RefreshNodeDefinitions": {
"label": "Düğüm Tanımlarını Yenile"
},
"Comfy_SaveWorkflow": {
"label": "İş Akışını Kaydet"
},
"Comfy_SaveWorkflowAs": {
"label": "İş Akışını Farklı Kaydet"
},
"Comfy_ShowSettingsDialog": {
"label": "Ayarlar İletişim Kutusunu Göster"
},
"Comfy_ToggleCanvasInfo": {
"label": "Tuval Performansı"
},
"Comfy_ToggleHelpCenter": {
"label": "Yardım Merkezi"
},
"Comfy_ToggleTheme": {
"label": "Temayı Değiştir (Karanlık/Açık)"
},
"Comfy_Undo": {
"label": "Geri Al"
},
"Comfy_User_OpenSignInDialog": {
"label": "Giriş Yapma İletişim Kutusunu Aç"
},
"Comfy_User_SignOut": {
"label": ıkış Yap"
},
"Workspace_CloseWorkflow": {
"label": "Mevcut İş Akışını Kapat"
},
"Workspace_NextOpenedWorkflow": {
"label": "Sonraki Açılan İş Akışı"
},
"Workspace_PreviousOpenedWorkflow": {
"label": "Önceki Açılan İş Akışı"
},
"Workspace_SearchBox_Toggle": {
"label": "Arama Kutusunu Aç/Kapat"
},
"Workspace_ToggleBottomPanel": {
"label": "Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "Tuş Atamaları İletişim Kutusunu Göster"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Terminal Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Kayıtlar Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Temel Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
"label": "Görünüm Kontrolleri Alt Panelini Aç/Kapat"
},
"Workspace_ToggleFocusMode": {
"label": "Odak Modunu Aç/Kapat"
},
"Workspace_ToggleSidebarTab_model-library": {
"label": "Model Kütüphanesi Kenar Çubuğunu Aç/Kapat",
"tooltip": "Model Kütüphanesi"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Düğüm Kütüphanesi Kenar Çubuğunu Aç/Kapat",
"tooltip": "Düğüm Kütüphanesi"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Kuyruk Kenar Çubuğunu Aç/Kapat",
"tooltip": "Kuyruk"
},
"Workspace_ToggleSidebarTab_workflows": {
"label": "İş Akışları Kenar Çubuğunu Aç/Kapat",
"tooltip": "İş Akışları"
}
}

1800
src/locales/tr/main.json Normal file

File diff suppressed because it is too large Load Diff

8653
src/locales/tr/nodeDefs.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,424 @@
{
"Comfy-Desktop_AutoUpdate": {
"name": "Güncellemeleri otomatik olarak kontrol et"
},
"Comfy-Desktop_SendStatistics": {
"name": "Anonim kullanım metrikleri gönder"
},
"Comfy-Desktop_UV_PypiInstallMirror": {
"name": "Pypi Yükleme Yansısı",
"tooltip": "Varsayılan pip yükleme yansısı"
},
"Comfy-Desktop_UV_PythonInstallMirror": {
"name": "Python Yükleme Yansısı",
"tooltip": "Yönetilen Python kurulumları Astral python-build-standalone projesinden indirilir. Bu değişken, Python kurulumları için farklı bir kaynak kullanmak üzere bir yansıma URL'sine ayarlanabilir. Sağlanan URL, örneğin https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz'deki https://github.com/astral-sh/python-build-standalone/releases/download'ın yerini alacaktır. Dağıtımlar, file:// URL şeması kullanılarak yerel bir dizinden okunabilir."
},
"Comfy-Desktop_UV_TorchInstallMirror": {
"name": "Torch Yükleme Yansısı",
"tooltip": "Pytorch için Pip yükleme yansısı"
},
"Comfy-Desktop_WindowStyle": {
"name": "Pencere Stili",
"tooltip": "Özel: Sistem başlık çubuğunu ComfyUI'nin Üst menüsüyle değiştirin",
"options": {
"default": "varsayılan",
"custom": "özel"
}
},
"Comfy_Canvas_BackgroundImage": {
"name": "Tuval arka plan resmi",
"tooltip": "Tuval arka planı için resim URL'si. Çıktılar panelindeki bir resme sağ tıklayıp \"Arka Plan Olarak Ayarla\"yı seçerek kullanabilir veya yükleme düğmesini kullanarak kendi resminizi yükleyebilirsiniz."
},
"Comfy_Canvas_NavigationMode": {
"name": "Tuval Gezinme Modu",
"options": {
"Standard (New)": "Standart (Yeni)",
"Drag Navigation": "Sürükleyerek Gezinme"
}
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Seçim araç kutusunu göster"
},
"Comfy_ConfirmClear": {
"name": "İş akışını temizlerken onay iste"
},
"Comfy_DevMode": {
"name": "Geliştirici modu seçeneklerini etkinleştir (API kaydetme, vb.)"
},
"Comfy_DisableFloatRounding": {
"name": "Varsayılan ondalık sayı widget yuvarlamasını devre dışı bırak.",
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir) Arka uçtaki düğüm tarafından yuvarlama ayarlandığında yuvarlama devre dışı bırakılamaz."
},
"Comfy_DisableSliders": {
"name": "Düğüm widget kaydırıcılarını devre dışı bırak"
},
"Comfy_DOMClippingEnabled": {
"name": "DOM öğesi kırpmayı etkinleştir (etkinleştirmek performansı düşürebilir)"
},
"Comfy_EditAttention_Delta": {
"name": "Ctrl+yukarı/aşağı hassasiyeti"
},
"Comfy_EnableTooltips": {
"name": "Araç İpuçlarını Etkinleştir"
},
"Comfy_EnableWorkflowViewRestore": {
"name": "İş akışlarında tuval konumunu ve yakınlaştırma seviyesini kaydet ve geri yükle"
},
"Comfy_FloatRoundingPrecision": {
"name": "Ondalık sayı widget yuvarlama ondalık basamakları [0 = otomatik].",
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir)"
},
"Comfy_Graph_CanvasInfo": {
"name": "Sol alt köşede tuval bilgilerini göster (fps, vb.)"
},
"Comfy_Graph_CanvasMenu": {
"name": "Grafik tuval menüsünü göster"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "Hızlı yakınlaştırma kısayolunu etkinleştir (Ctrl + Shift + Sürükle)"
},
"Comfy_Graph_LinkMarkers": {
"name": "Bağlantı orta nokta işaretçileri",
"options": {
"None": "Yok",
"Circle": "Daire",
"Arrow": "Ok"
}
},
"Comfy_Graph_ZoomSpeed": {
"name": "Tuval yakınlaştırma hızı"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Düzenlemek için grup başlığına çift tıkla"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "Seçili düğümleri gruplandırma dolgusu"
},
"Comfy_LinkRelease_Action": {
"name": "Bağlantı bırakıldığında eylem (Değiştirici yok)",
"options": {
"context menu": "bağlam menüsü",
"search box": "arama kutusu",
"no action": "eylem yok"
}
},
"Comfy_LinkRelease_ActionShift": {
"name": "Bağlantı bırakıldığında eylem (Shift)",
"options": {
"context menu": "bağlam menüsü",
"search box": "arama kutusu",
"no action": "eylem yok"
}
},
"Comfy_LinkRenderMode": {
"name": "Bağlantı Oluşturma Modu",
"options": {
"Straight": "Düz",
"Linear": "Doğrusal",
"Spline": "Eğri",
"Hidden": "Gizli"
}
},
"Comfy_Load3D_3DViewerEnable": {
"name": "3D Görüntüleyiciyi Etkinleştir (Beta)",
"tooltip": "Seçili düğümler için 3D Görüntüleyiciyi (Beta) etkinleştirir. Bu özellik, 3D modelleri doğrudan tam boyutlu 3D görüntüleyici içinde görselleştirmenize ve etkileşimde bulunmanıza olanak tanır."
},
"Comfy_Load3D_BackgroundColor": {
"name": "Başlangıç Arka Plan Rengi",
"tooltip": "3D sahnenin varsayılan arka plan rengini kontrol eder. Bu ayar, yeni bir 3D widget oluşturulduğunda arka plan görünümünü belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
},
"Comfy_Load3D_CameraType": {
"name": "Başlangıç Kamera Tipi",
"tooltip": "Yeni bir 3D widget oluşturulduğunda kameranın varsayılan olarak perspektif mi yoksa ortografik mi olacağını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir.",
"options": {
"perspective": "perspektif",
"orthographic": "ortografik"
}
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Işık Ayarlama Artışı",
"tooltip": "3D sahnelerde ışık yoğunluğunu ayarlarken artış boyutunu kontrol eder. Daha küçük bir adım değeri, aydınlatma ayarlamaları üzerinde daha ince kontrol sağlarken, daha büyük bir değer ayarlama başına daha belirgin değişikliklere neden olur."
},
"Comfy_Load3D_LightIntensity": {
"name": "Başlangıç Işık Yoğunluğu",
"tooltip": "3D sahnedeki aydınlatmanın varsayılan parlaklık seviyesini ayarlar. Bu değer, yeni bir 3D widget oluşturulduğunda ışıkların nesneleri ne kadar yoğun aydınlatacağını belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Maksimum Işık Yoğunluğu",
"tooltip": "3D sahneler için izin verilen maksimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek üst parlaklık sınırını tanımlar."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Minimum Işık Yoğunluğu",
"tooltip": "3D sahneler için izin verilen minimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek alt parlaklık sınırını tanımlar."
},
"Comfy_Load3D_ShowGrid": {
"name": "Başlangıç Izgara Görünürlüğü",
"tooltip": "Yeni bir 3D widget oluşturulduğunda ızgaranın varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Load3D_ShowPreview": {
"name": "Başlangıç Önizleme Görünürlüğü",
"tooltip": "Yeni bir 3D widget oluşturulduğunda önizleme ekranının varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Locale": {
"name": "Dil"
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "Fırça ayar hızı çarpanı",
"tooltip": "Ayarlama sırasında fırça boyutunun ve sertliğinin ne kadar hızlı değiştiğini kontrol eder. Daha yüksek değerler daha hızlı değişiklikler anlamına gelir."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "Fırça ayarını baskın eksene kilitle",
"tooltip": "Etkinleştirildiğinde, fırça ayarları yalnızca daha fazla hareket ettiğiniz yöne bağlı olarak boyutu VEYA sertliği etkileyecektir"
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "Yeni maske düzenleyiciyi kullan",
"tooltip": "Yeni maske düzenleyici arayüzüne geç"
},
"Comfy_ModelLibrary_AutoLoadAll": {
"name": "Tüm model klasörlerini otomatik olarak yükle",
"tooltip": "Doğruysa, model kütüphanesini açar açmaz tüm klasörler yüklenecektir (bu, yüklenirken gecikmelere neden olabilir). Yanlışsa, kök düzeyindeki model klasörleri yalnızca üzerlerine tıkladığınızda yüklenecektir."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "Model kütüphanesi ağaç görünümünde hangi adın görüntüleneceği",
"tooltip": "Model listesinde ham dosya adının (dizin veya \".safetensors\" uzantısı olmadan) basitleştirilmiş bir görünümünü oluşturmak için \"dosyaadı\"nı seçin. Yapılandırılabilir model meta veri başlığını görüntülemek için \"başlık\"ı seçin.",
"options": {
"filename": "dosyaadı",
"title": "başlık"
}
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Görüntü önizlemesinin altında genişlik × yüksekliği göster"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Bağlantıyı otomatik olarak düğüm yuvasına yapıştır",
"tooltip": "Bir bağlantıyı bir düğümün üzerine sürüklerken, bağlantı otomatik olarak düğüm üzerindeki uygun bir giriş yuvasına yapışır"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "Düğümleri silerken tüm bağlantıları koru",
"tooltip": "Bir düğümü silerken, tüm giriş ve çıkış bağlantılarını yeniden bağlamaya çalışın (silinen düğümü atlayarak)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "Düzenlemek için düğüm başlığına çift tıkla"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "Orta tıklama yeni bir Yeniden Yönlendirme düğümü oluşturur"
},
"Comfy_Node_Opacity": {
"name": "Düğüm opaklığı"
},
"Comfy_Node_ShowDeprecated": {
"name": "Aramada kullanımdan kaldırılmış düğümleri göster",
"tooltip": "Kullanımdan kaldırılmış düğümler arayüzde varsayılan olarak gizlidir, ancak bunları kullanan mevcut iş akışlarında işlevsel kalır."
},
"Comfy_Node_ShowExperimental": {
"name": "Aramada deneysel düğümleri göster",
"tooltip": "Deneysel düğümler arayüzde bu şekilde işaretlenmiştir ve gelecekteki sürümlerde önemli değişikliklere veya kaldırılmaya tabi olabilir. Üretim iş akışlarında dikkatli kullanın"
},
"Comfy_Node_SnapHighlightsNode": {
"name": "Yapıştırma düğümü vurgular",
"tooltip": "Uygun giriş yuvasına sahip bir düğümün üzerine bir bağlantı sürüklerken, düğümü vurgulayın"
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Düğüm ID rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "Düğüm yaşam döngüsü rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "Düğüm kaynak rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster",
"Hide built-in": "Yerleşik olanı gizle"
}
},
"Comfy_NodeBadge_ShowApiPricing": {
"name": "API düğüm fiyatlandırma rozetini göster"
},
"Comfy_NodeSearchBoxImpl": {
"name": "Düğüm arama kutusu uygulaması",
"options": {
"default": "varsayılan",
"litegraph (legacy)": "litegraph (eski)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "Düğüm önizlemesi",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "Arama sonuçlarında düğüm kategorisini göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "Arama sonuçlarında düğüm kimliği adını göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "Arama sonuçlarında düğüm sıklığını göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSuggestions_number": {
"name": "Düğüm öneri sayısı",
"tooltip": "Yalnızca litegraph arama kutusu/bağlam menüsü için"
},
"Comfy_Notification_ShowVersionUpdates": {
"name": "Sürüm güncellemelerini göster",
"tooltip": "Yeni modeller ve önemli yeni özellikler için güncellemeleri göster."
},
"Comfy_Pointer_ClickBufferTime": {
"name": "İşaretçi tıklama kayma gecikmesi",
"tooltip": "Bir işaretçi düğmesine bastıktan sonra, bu, işaretçi hareketinin göz ardı edilebileceği maksimum süredir (milisaniye cinsinden).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
},
"Comfy_Pointer_ClickDrift": {
"name": "İşaretçi tıklama kayması (maksimum mesafe)",
"tooltip": "İşaretçi bir düğmeyi basılı tutarken bu mesafeden daha fazla hareket ederse, bu sürükleme olarak kabul edilir (tıklama yerine).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
},
"Comfy_Pointer_DoubleClickTime": {
"name": "Çift tıklama aralığı (maksimum)",
"tooltip": "Çift tıklamanın iki tıklaması arasındaki milisaniye cinsinden maksimum süre. Bu değeri artırmak, çift tıklamaların bazen kaydedilmemesi durumunda yardımcı olabilir."
},
"Comfy_PreviewFormat": {
"name": "Önizleme görüntü formatı",
"tooltip": "Görüntü widget'ında bir önizleme görüntülerken, onu hafif bir görüntüye dönüştürün, örn. webp, jpeg, webp;50, vb."
},
"Comfy_PromptFilename": {
"name": "İş akışını kaydederken dosya adı iste"
},
"Comfy_Queue_MaxHistoryItems": {
"name": "Kuyruk geçmişi boyutu",
"tooltip": "Kuyruk geçmişinde gösterilen maksimum görev sayısı."
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Toplu iş sayısı sınırı",
"tooltip": "Tek bir düğme tıklamasıyla kuyruğa eklenen maksimum görev sayısı"
},
"Comfy_Sidebar_Location": {
"name": "Kenar çubuğu konumu",
"options": {
"left": "sol",
"right": "sağ"
}
},
"Comfy_Sidebar_Size": {
"name": "Kenar çubuğu boyutu",
"options": {
"normal": "normal",
"small": "küçük"
}
},
"Comfy_Sidebar_UnifiedWidth": {
"name": "Birleşik kenar çubuğu genişliği"
},
"Comfy_SnapToGrid_GridSize": {
"name": "Izgaraya yapıştırma boyutu",
"tooltip": "Shift tuşunu basılı tutarken düğümleri sürükleyip yeniden boyutlandırırken ızgaraya hizalanacaklar, bu o ızgaranın boyutunu kontrol eder."
},
"Comfy_TextareaWidget_FontSize": {
"name": "Metin alanı widget yazı tipi boyutu"
},
"Comfy_TextareaWidget_Spellcheck": {
"name": "Metin alanı widget yazım denetimi"
},
"Comfy_TreeExplorer_ItemPadding": {
"name": "Ağaç gezgini öğe dolgusu"
},
"Comfy_UseNewMenu": {
"name": "Yeni menüyü kullan",
"tooltip": "Menü çubuğu konumu. Mobil cihazlarda menü her zaman üstte gösterilir.",
"options": {
"Disabled": "Devre dışı",
"Top": "Üst",
"Bottom": "Alt"
}
},
"Comfy_Validation_Workflows": {
"name": "İş akışlarını doğrula"
},
"Comfy_VueNodes_Enabled": {
"name": "Vue düğüm oluşturmayı etkinleştir",
"tooltip": "Düğümleri tuval öğeleri yerine Vue bileşenleri olarak oluşturun. Deneysel özellik."
},
"Comfy_VueNodes_Widgets": {
"name": "Vue widget'larını etkinleştir",
"tooltip": "Widget'ları Vue düğümleri içinde Vue bileşenleri olarak oluşturun."
},
"Comfy_WidgetControlMode": {
"name": "Widget kontrol modu",
"tooltip": "Widget değerlerinin ne zaman güncelleneceğini (rastgele/artırma/azaltma), istem kuyruğa alınmadan önce veya sonra kontrol eder.",
"options": {
"before": "önce",
"after": "sonra"
}
},
"Comfy_Window_UnloadConfirmation": {
"name": "Pencereyi kapatırken onay göster"
},
"Comfy_Workflow_AutoSave": {
"name": "Otomatik Kaydet",
"options": {
"off": "kapalı",
"after delay": "gecikmeden sonra"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Otomatik Kaydetme Gecikmesi (ms)",
"tooltip": "Yalnızca Otomatik Kaydetme \"gecikmeden sonra\" olarak ayarlandığında geçerlidir."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "İş akışlarını silerken onay göster"
},
"Comfy_Workflow_Persist": {
"name": "İş akışı durumunu koru ve sayfayı (yeniden) yüklediğinde geri yükle"
},
"Comfy_Workflow_ShowMissingModelsWarning": {
"name": "Eksik model uyarısını göster"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "Eksik düğüm uyarısını göster"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "İş akışını kaydederken düğüm kimliklerini sırala"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Açılan iş akışları konumu",
"options": {
"Sidebar": "Kenar Çubuğu",
"Topbar": "Üst Çubuk",
"Topbar (2nd-row)": "Üst Çubuk (2. sıra)"
}
},
"LiteGraph_Canvas_MinFontSizeForLOD": {
"name": "Yakınlaştırma Düğümü Ayrıntı Seviyesi - yazı tipi boyutu eşiği",
"tooltip": "Düğümlerin ne zaman düşük kaliteli LOD oluşturmaya geçeceğini kontrol eder. Ne zaman geçiş yapılacağını belirlemek için piksel cinsinden yazı tipi boyutunu kullanır. Devre dışı bırakmak için 0'a ayarlayın. 1-24 arasındaki değerler LOD için minimum yazı tipi boyutu eşiğini ayarlar - daha yüksek değerler (24 piksel) = uzaklaştırırken düğümleri daha erken basitleştirilmiş oluşturmaya geçirin, daha düşük değerler (1 piksel) = tam düğüm kalitesini daha uzun süre koruyun."
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Maksimum FPS",
"tooltip": "Tuvalin saniyede oluşturmasına izin verilen maksimum kare sayısı. Akıcılık pahasına GPU kullanımını sınırlar. 0 ise, ekran yenileme hızı kullanılır. Varsayılan: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Yakınlaştırıldığında düğüm birleşik widget menülerini (listeleri) ölçeklendir"
},
"LiteGraph_Node_DefaultPadding": {
"name": "Yeni düğümleri her zaman küçült",
"tooltip": "Oluşturulduğunda düğümleri mümkün olan en küçük boyuta yeniden boyutlandırın. Devre dışı bırakıldığında, yeni eklenen bir düğüm widget değerlerini göstermek için biraz genişletilecektir."
},
"LiteGraph_Node_TooltipDelay": {
"name": "Araç İpucu Gecikmesi"
},
"LiteGraph_Reroute_SplineOffset": {
"name": "Yeniden yönlendirme eğri ofseti",
"tooltip": "Yeniden yönlendirme merkez noktasından bezier kontrol noktası ofseti"
},
"pysssss_SnapToGrid": {
"name": "Her zaman ızgaraya yapıştır"
}
}

View File

@@ -2,11 +2,6 @@ import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import * as Sentry from '@sentry/vue'
import { initializeApp } from 'firebase/app'
import {
browserLocalPersistence,
browserSessionPersistence,
indexedDBLocalPersistence
} from 'firebase/auth'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -14,7 +9,7 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import { createApp } from 'vue'
import { VueFire, VueFireAuthWithDependencies } from 'vuefire'
import { VueFire, VueFireAuth } from 'vuefire'
import { FIREBASE_CONFIG } from '@/config/firebase'
import '@/lib/litegraph/public/css/litegraph.css'
@@ -71,18 +66,6 @@ app
.use(i18n)
.use(VueFire, {
firebaseApp,
modules: [
// Configure Firebase Auth persistence: localStorage first, IndexedDB last.
// Localstorage is preferred to IndexedDB for mobile Safari compatibility.
VueFireAuthWithDependencies({
dependencies: {
persistence: [
browserLocalPersistence,
browserSessionPersistence,
indexedDBLocalPersistence
]
}
})
]
modules: [VueFireAuth()]
})
.mount('#vue-app')

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