From c9d74777ba0c61d8c12e9ef81362ccfb9d00f73a Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Sat, 17 Jan 2026 19:13:05 -0800 Subject: [PATCH 001/375] Migrate parentIds when converting to subgraph (#5708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parentId property on links and reroutes was not handled at all in the "Convert to Subgraph" code. This needs to be addressed in 4 cases - A new external input link must have parentId set to the first non-migrated reroute - A new external output link must have the parentId of it's eldest remaining child set to undefined - A new internal input link must have the parentId of it's eldest remaining child set to undefined - A new internal output link must have the parentId set to the first migrated reroute This is handled in two parts by adding logic where the boundry links is created - The change involves mutation of inputs (which isn't great) but the function here was already mutating inputs into an invalid state - @DrJKL Do you see a quick way to better fix both these cases? Looks like litegraph tests aren't enabled and cursory glance shows multiple need to be updated to reflect recent changes. I'll still try to add some tests anyways. EDIT: Tests are non functional. Seems the subgraph conversion call requires the rest of the frontend is running and has event listeners to register the subgraph node def. More work than anticipated, best revisited later Resolves #5669 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5708-Migrate-parentIds-when-converting-to-subgraph-2746d73d365081f78acff4454092c74a) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown --- src/lib/litegraph/src/LGraph.ts | 46 +++++++++++++++---- .../litegraph/src/subgraph/subgraphUtils.ts | 30 ++++++++++-- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 04f2a4a4fe..486831e136 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -1571,8 +1571,21 @@ export class LGraph // Inputs, outputs, and links const links = internalLinks.map((x) => x.asSerialisable()) - const inputs = mapSubgraphInputsAndLinks(resolvedInputLinks, links) - const outputs = mapSubgraphOutputsAndLinks(resolvedOutputLinks, links) + + const internalReroutes = new Map([...reroutes].map((r) => [r.id, r])) + const externalReroutes = new Map( + [...this.reroutes].filter(([id]) => !internalReroutes.has(id)) + ) + const inputs = mapSubgraphInputsAndLinks( + resolvedInputLinks, + links, + internalReroutes + ) + const outputs = mapSubgraphOutputsAndLinks( + resolvedOutputLinks, + links, + externalReroutes + ) // Prepare subgraph data const data = { @@ -1714,10 +1727,10 @@ export class LGraph // Reconnect output links in parent graph i = 0 for (const [, connections] of outputsGroupedByOutput.entries()) { - // Special handling: Subgraph output node i++ for (const connection of connections) { const { input, inputNode, link, subgraphOutput } = connection + // Special handling: Subgraph output node if (link.target_id === SUBGRAPH_OUTPUT_ID) { link.origin_id = subgraphNode.id link.origin_slot = i - 1 @@ -2013,33 +2026,50 @@ export class LGraph while (parentId) { instance.parentId = parentId instance = this.reroutes.get(parentId) - if (!instance) throw new Error('Broken Id link when unpacking') + if (!instance) { + console.error('Broken Id link when unpacking') + break + } if (instance.linkIds.has(linkInstance.id)) throw new Error('Infinite parentId loop') instance.linkIds.add(linkInstance.id) parentId = instance.parentId } } + if (!instance) continue parentId = newLink.iparent while (parentId) { const migratedId = rerouteIdMap.get(parentId) - if (!migratedId) throw new Error('Broken Id link when unpacking') + if (!migratedId) { + console.error('Broken Id link when unpacking') + break + } instance.parentId = migratedId instance = this.reroutes.get(migratedId) - if (!instance) throw new Error('Broken Id link when unpacking') + if (!instance) { + console.error('Broken Id link when unpacking') + break + } if (instance.linkIds.has(linkInstance.id)) throw new Error('Infinite parentId loop') instance.linkIds.add(linkInstance.id) const oldReroute = subgraphNode.subgraph.reroutes.get(parentId) - if (!oldReroute) throw new Error('Broken Id link when unpacking') + if (!oldReroute) { + console.error('Broken Id link when unpacking') + break + } parentId = oldReroute.parentId } + if (!instance) break if (!newLink.externalFirst) { parentId = newLink.eparent while (parentId) { instance.parentId = parentId instance = this.reroutes.get(parentId) - if (!instance) throw new Error('Broken Id link when unpacking') + if (!instance) { + console.error('Broken Id link when unpacking') + break + } if (instance.linkIds.has(linkInstance.id)) throw new Error('Infinite parentId loop') instance.linkIds.add(linkInstance.id) diff --git a/src/lib/litegraph/src/subgraph/subgraphUtils.ts b/src/lib/litegraph/src/subgraph/subgraphUtils.ts index 38f74efaad..518869503d 100644 --- a/src/lib/litegraph/src/subgraph/subgraphUtils.ts +++ b/src/lib/litegraph/src/subgraph/subgraphUtils.ts @@ -4,6 +4,7 @@ import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { LLink } from '@/lib/litegraph/src/LLink' import type { ResolvedConnection } from '@/lib/litegraph/src/LLink' import { Reroute } from '@/lib/litegraph/src/Reroute' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID @@ -259,10 +260,29 @@ export function groupResolvedByOutput( return groupedByOutput } +function mapReroutes( + link: SerialisableLLink, + reroutes: Map +) { + let child: SerialisableLLink | Reroute = link + let nextReroute = + child.parentId === undefined ? undefined : reroutes.get(child.parentId) + + while (child.parentId !== undefined && nextReroute) { + child = nextReroute + nextReroute = + child.parentId === undefined ? undefined : reroutes.get(child.parentId) + } + + const lastId = child.parentId + child.parentId = undefined + return lastId +} export function mapSubgraphInputsAndLinks( resolvedInputLinks: ResolvedConnection[], - links: SerialisableLLink[] + links: SerialisableLLink[], + reroutes: Map ): SubgraphIO[] { // Group matching links const groupedByOutput = groupResolvedByOutput(resolvedInputLinks) @@ -279,8 +299,10 @@ export function mapSubgraphInputsAndLinks( if (!input) continue const linkData = link.asSerialisable() + link.parentId = mapReroutes(link, reroutes) linkData.origin_id = SUBGRAPH_INPUT_ID linkData.origin_slot = inputs.length + links.push(linkData) inputLinks.push(linkData) } @@ -340,7 +362,8 @@ export function mapSubgraphInputsAndLinks( */ export function mapSubgraphOutputsAndLinks( resolvedOutputLinks: ResolvedConnection[], - links: SerialisableLLink[] + links: SerialisableLLink[], + reroutes: Map ): SubgraphIO[] { // Group matching links const groupedByOutput = groupResolvedByOutput(resolvedOutputLinks) @@ -355,10 +378,11 @@ export function mapSubgraphOutputsAndLinks( const { link, output } = resolved if (!output) continue - // Link const linkData = link.asSerialisable() + linkData.parentId = mapReroutes(link, reroutes) linkData.target_id = SUBGRAPH_OUTPUT_ID linkData.target_slot = outputs.length + links.push(linkData) outputLinks.push(linkData) } From 82c3cd3cd29feafe097709209aab0ecce82b70b0 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Sat, 17 Jan 2026 22:32:32 -0500 Subject: [PATCH 002/375] add thumbnail for 3d generation (#8129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary add thrumbnail for 3d genations, feature requested by @PabloWiedemann ## Screenshots https://github.com/user-attachments/assets/4fb9b88b-dd7b-4a69-a70c-e850472d3498 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8129-add-thumbnail-for-3d-generation-2eb6d73d365081f2a30bc698a4fde6e0) by [Unito](https://www.unito.io) --- src/composables/useLoad3d.ts | 16 +++++ src/extensions/core/load3d/Load3d.ts | 54 +++++++++++++++ src/extensions/core/load3d/Load3dUtils.ts | 65 +++++++++++++++++++ src/extensions/core/saveMesh.ts | 12 ++++ src/platform/assets/components/Media3DTop.vue | 29 ++++++++- 5 files changed, 173 insertions(+), 3 deletions(-) diff --git a/src/composables/useLoad3d.ts b/src/composables/useLoad3d.ts index c8312051ae..b07daba2e8 100644 --- a/src/composables/useLoad3d.ts +++ b/src/composables/useLoad3d.ts @@ -511,6 +511,22 @@ export const useLoad3d = (nodeOrRef: MaybeRef) => { hasSkeleton.value = load3d?.hasSkeleton() ?? false // Reset skeleton visibility when loading new model modelConfig.value.showSkeleton = false + + if (load3d) { + const node = nodeRef.value + + const modelWidget = node?.widgets?.find( + (w) => w.name === 'model_file' || w.name === 'image' + ) + const value = modelWidget?.value + if (typeof value === 'string') { + void Load3dUtils.generateThumbnailIfNeeded( + load3d, + value, + isPreview.value ? 'output' : 'input' + ) + } + } }, skeletonVisibilityChange: (value: boolean) => { modelConfig.value.showSkeleton = value diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts index 657a1796b9..60690e4f82 100644 --- a/src/extensions/core/load3d/Load3d.ts +++ b/src/extensions/core/load3d/Load3d.ts @@ -754,6 +754,60 @@ class Load3d { this.forceRender() } + public async captureThumbnail( + width: number = 256, + height: number = 256 + ): Promise { + if (!this.modelManager.currentModel) { + throw new Error('No model loaded for thumbnail capture') + } + + const savedState = this.cameraManager.getCameraState() + const savedCameraType = this.cameraManager.getCurrentCameraType() + const savedGridVisible = this.sceneManager.gridHelper.visible + + try { + this.sceneManager.gridHelper.visible = false + + if (savedCameraType !== 'perspective') { + this.cameraManager.toggleCamera('perspective') + } + + const box = new THREE.Box3().setFromObject(this.modelManager.currentModel) + const size = box.getSize(new THREE.Vector3()) + const center = box.getCenter(new THREE.Vector3()) + + const maxDim = Math.max(size.x, size.y, size.z) + const distance = maxDim * 1.5 + + const cameraPosition = new THREE.Vector3( + center.x - distance * 0.8, + center.y + distance * 0.4, + center.z + distance * 0.3 + ) + + this.cameraManager.perspectiveCamera.position.copy(cameraPosition) + this.cameraManager.perspectiveCamera.lookAt(center) + this.cameraManager.perspectiveCamera.updateProjectionMatrix() + + if (this.controlsManager.controls) { + this.controlsManager.controls.target.copy(center) + this.controlsManager.controls.update() + } + + const result = await this.sceneManager.captureScene(width, height) + return result.scene + } finally { + this.sceneManager.gridHelper.visible = savedGridVisible + + if (savedCameraType !== 'perspective') { + this.cameraManager.toggleCamera(savedCameraType) + } + this.cameraManager.setCameraState(savedState) + this.controlsManager.controls?.update() + } + } + public remove(): void { if (this.contextMenuAbortController) { this.contextMenuAbortController.abort() diff --git a/src/extensions/core/load3d/Load3dUtils.ts b/src/extensions/core/load3d/Load3dUtils.ts index 13095ac96b..ba7c36e557 100644 --- a/src/extensions/core/load3d/Load3dUtils.ts +++ b/src/extensions/core/load3d/Load3dUtils.ts @@ -1,9 +1,34 @@ +import type Load3d from '@/extensions/core/load3d/Load3d' import { t } from '@/i18n' import { useToastStore } from '@/platform/updates/common/toastStore' import { api } from '@/scripts/api' import { app } from '@/scripts/app' class Load3dUtils { + static async generateThumbnailIfNeeded( + load3d: Load3d, + modelPath: string, + folderType: 'input' | 'output' + ): Promise { + const [subfolder, filename] = this.splitFilePath(modelPath) + const thumbnailFilename = this.getThumbnailFilename(filename) + + const exists = await this.fileExists( + subfolder, + thumbnailFilename, + folderType + ) + if (exists) return + + const imageData = await load3d.captureThumbnail(256, 256) + await this.uploadThumbnail( + imageData, + subfolder, + thumbnailFilename, + folderType + ) + } + static async uploadTempImage( imageData: string, prefix: string, @@ -122,6 +147,46 @@ class Load3dUtils { await Promise.all(uploadPromises) } + + static getThumbnailFilename(modelFilename: string): string { + return `${modelFilename}.png` + } + + static async fileExists( + subfolder: string, + filename: string, + type: string = 'input' + ): Promise { + try { + const url = api.apiURL(this.getResourceURL(subfolder, filename, type)) + const response = await fetch(url, { method: 'HEAD' }) + return response.ok + } catch { + return false + } + } + + static async uploadThumbnail( + imageData: string, + subfolder: string, + filename: string, + type: string = 'input' + ): Promise { + const blob = await fetch(imageData).then((r) => r.blob()) + const file = new File([blob], filename, { type: 'image/png' }) + + const body = new FormData() + body.append('image', file) + body.append('subfolder', subfolder) + body.append('type', type) + + const resp = await api.fetchApi('/upload/image', { + method: 'POST', + body + }) + + return resp.status === 200 + } } export default Load3dUtils diff --git a/src/extensions/core/saveMesh.ts b/src/extensions/core/saveMesh.ts index ae94a86093..9471204677 100644 --- a/src/extensions/core/saveMesh.ts +++ b/src/extensions/core/saveMesh.ts @@ -4,6 +4,7 @@ import Load3D from '@/components/load3d/Load3D.vue' import { useLoad3d } from '@/composables/useLoad3d' import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper' import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration' +import Load3dUtils from '@/extensions/core/load3d/Load3dUtils' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema' @@ -94,6 +95,17 @@ useExtensionService().registerExtension({ const config = new Load3DConfiguration(load3d, node.properties) const loadFolder = fileInfo.type as 'input' | 'output' + + const onModelLoaded = () => { + load3d.removeEventListener('modelLoadingEnd', onModelLoaded) + void Load3dUtils.generateThumbnailIfNeeded( + load3d, + filePath, + loadFolder + ) + } + load3d.addEventListener('modelLoadingEnd', onModelLoaded) + config.configureForSaveMesh(loadFolder, filePath) } }) diff --git a/src/platform/assets/components/Media3DTop.vue b/src/platform/assets/components/Media3DTop.vue index a4cc141db1..b5b7f0c609 100644 --- a/src/platform/assets/components/Media3DTop.vue +++ b/src/platform/assets/components/Media3DTop.vue @@ -1,12 +1,35 @@ + + From 54db655a230b3371bf5320617d725611c9615e53 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 17 Jan 2026 19:43:24 -0800 Subject: [PATCH 003/375] feat: make subgraphs blueprints appear higher in node library sidebar (#8140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Changes insertion order so subgraph blueprints are inserted first and therefore appear highest in node library sidebar (when using default 'original' ordering). image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8140-feat-make-subgraphs-blueprints-appear-higher-in-node-library-sidebar-2ec6d73d3650816f8164f0991b81c116) by [Unito](https://www.unito.io) --- src/stores/nodeDefStore.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index 8498659419..27a46ad219 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -299,9 +299,10 @@ export const useNodeDefStore = defineStore('nodeDef', () => { const nodeDefs = computed(() => { const subgraphStore = useSubgraphStore() + // Blueprints first for discoverability in the node library sidebar return [ - ...Object.values(nodeDefsByName.value), - ...subgraphStore.subgraphBlueprints + ...subgraphStore.subgraphBlueprints, + ...Object.values(nodeDefsByName.value) ] }) const nodeDataTypes = computed(() => { From 7fcef2ba8944a67914e0313705794076f27db70b Mon Sep 17 00:00:00 2001 From: Comfy Org PR Bot Date: Sun, 18 Jan 2026 12:55:35 +0900 Subject: [PATCH 004/375] 1.38.5 (#8138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Patch version increment to 1.38.5 **Base branch:** `main` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8138-1-38-5-2ec6d73d365081b6bf57fd29cb56998c) by [Unito](https://www.unito.io) --------- Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com> Co-authored-by: github-actions Co-authored-by: Alexander Brown --- package.json | 2 +- src/locales/ar/main.json | 11 +++++++++++ src/locales/es/main.json | 11 +++++++++++ src/locales/fa/main.json | 11 +++++++++++ src/locales/fr/main.json | 11 +++++++++++ src/locales/ja/main.json | 11 +++++++++++ src/locales/ko/main.json | 11 +++++++++++ src/locales/pt-BR/main.json | 11 +++++++++++ src/locales/ru/main.json | 11 +++++++++++ src/locales/tr/main.json | 11 +++++++++++ src/locales/zh-TW/main.json | 11 +++++++++++ src/locales/zh/main.json | 11 +++++++++++ 12 files changed, 122 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 60b37f1dac..78ac9e373e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.38.4", + "version": "1.38.5", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", diff --git a/src/locales/ar/main.json b/src/locales/ar/main.json index e3d3693495..122883cd9c 100644 --- a/src/locales/ar/main.json +++ b/src/locales/ar/main.json @@ -238,6 +238,12 @@ "title": "إنشاء حساب" } }, + "boundingBox": { + "height": "الارتفاع", + "width": "العرض", + "x": "س", + "y": "ص" + }, "breadcrumbsMenu": { "clearWorkflow": "مسح سير العمل", "deleteBlueprint": "حذف المخطط", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "لا توجد صور للمقارنة" }, + "imageCrop": { + "cropPreviewAlt": "معاينة الاقتصاص", + "loading": "جارٍ التحميل...", + "noInputImage": "لا توجد صورة إدخال متصلة" + }, "importFailed": { "copyError": "خطأ في النسخ", "title": "فشل الاستيراد" diff --git a/src/locales/es/main.json b/src/locales/es/main.json index cd59b5bf8c..cbbb1770b7 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -238,6 +238,12 @@ "title": "Crea una cuenta" } }, + "boundingBox": { + "height": "Alto", + "width": "Ancho", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "Limpiar flujo de trabajo", "deleteBlueprint": "Eliminar Plano", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "No hay imágenes para comparar" }, + "imageCrop": { + "cropPreviewAlt": "Vista previa del recorte", + "loading": "Cargando...", + "noInputImage": "No hay imagen de entrada conectada" + }, "importFailed": { "copyError": "Error al copiar", "title": "Error de importación" diff --git a/src/locales/fa/main.json b/src/locales/fa/main.json index d4c673259a..9d48620d57 100644 --- a/src/locales/fa/main.json +++ b/src/locales/fa/main.json @@ -238,6 +238,12 @@ "title": "ایجاد حساب کاربری" } }, + "boundingBox": { + "height": "ارتفاع", + "width": "عرض", + "x": "ایکس", + "y": "وای" + }, "breadcrumbsMenu": { "clearWorkflow": "پاک‌سازی workflow", "deleteBlueprint": "حذف blueprint", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "تصویری برای مقایسه وجود ندارد" }, + "imageCrop": { + "cropPreviewAlt": "پیش‌نمایش برش", + "loading": "در حال بارگذاری...", + "noInputImage": "هیچ تصویر ورودی متصل نیست" + }, "importFailed": { "copyError": "خطا در کپی", "title": "وارد کردن ناموفق بود" diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index 014f2ea0e7..04db65b9e6 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -238,6 +238,12 @@ "title": "Créer un compte" } }, + "boundingBox": { + "height": "Hauteur", + "width": "Largeur", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "Effacer le workflow", "deleteBlueprint": "Supprimer le plan", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "Aucune image à comparer" }, + "imageCrop": { + "cropPreviewAlt": "Aperçu du recadrage", + "loading": "Chargement...", + "noInputImage": "Aucune image d'entrée connectée" + }, "importFailed": { "copyError": "Erreur de copie", "title": "Échec de l’importation" diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index 7791475f58..ef3a14b51a 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -238,6 +238,12 @@ "title": "アカウントを作成する" } }, + "boundingBox": { + "height": "高さ", + "width": "幅", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "ワークフローをクリア", "deleteBlueprint": "ブループリントを削除", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "比較する画像がありません" }, + "imageCrop": { + "cropPreviewAlt": "切り抜きプレビュー", + "loading": "読み込み中...", + "noInputImage": "入力画像が接続されていません" + }, "importFailed": { "copyError": "コピーエラー", "title": "インポート失敗" diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 60b2f97113..939f3ae532 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -238,6 +238,12 @@ "title": "계정 생성" } }, + "boundingBox": { + "height": "높이", + "width": "너비", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "워크플로 내용 지우기", "deleteBlueprint": "블루프린트 삭제", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "비교할 이미지가 없습니다" }, + "imageCrop": { + "cropPreviewAlt": "자르기 미리보기", + "loading": "로딩 중...", + "noInputImage": "입력 이미지가 연결되지 않았습니다" + }, "importFailed": { "copyError": "복사 오류", "title": "가져오기 실패" diff --git a/src/locales/pt-BR/main.json b/src/locales/pt-BR/main.json index 160f525f57..37ef795926 100644 --- a/src/locales/pt-BR/main.json +++ b/src/locales/pt-BR/main.json @@ -238,6 +238,12 @@ "title": "Criar uma conta" } }, + "boundingBox": { + "height": "Altura", + "width": "Largura", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "Limpar Fluxo de Trabalho", "deleteBlueprint": "Excluir Blueprint", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "Nenhuma imagem para comparar" }, + "imageCrop": { + "cropPreviewAlt": "Pré-visualização do recorte", + "loading": "Carregando...", + "noInputImage": "Nenhuma imagem de entrada conectada" + }, "importFailed": { "copyError": "Erro ao Copiar", "title": "Falha na Importação" diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 60155d3340..cf8a166232 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -238,6 +238,12 @@ "title": "Создать аккаунт" } }, + "boundingBox": { + "height": "Высота", + "width": "Ширина", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "Очистить рабочий процесс", "deleteBlueprint": "Удалить схему", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "Нет изображений для сравнения" }, + "imageCrop": { + "cropPreviewAlt": "Предпросмотр обрезки", + "loading": "Загрузка...", + "noInputImage": "Входное изображение не подключено" + }, "importFailed": { "copyError": "Ошибка копирования", "title": "Ошибка импорта" diff --git a/src/locales/tr/main.json b/src/locales/tr/main.json index 870cee220f..d4aa99bd0c 100644 --- a/src/locales/tr/main.json +++ b/src/locales/tr/main.json @@ -238,6 +238,12 @@ "title": "Hesap oluşturun" } }, + "boundingBox": { + "height": "Yükseklik", + "width": "Genişlik", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "İş Akışını Temizle", "deleteBlueprint": "Taslağı Sil", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "Karşılaştırılacak görsel yok" }, + "imageCrop": { + "cropPreviewAlt": "Kırpma önizlemesi", + "loading": "Yükleniyor...", + "noInputImage": "Bağlı giriş görseli yok" + }, "importFailed": { "copyError": "Kopyalama Hatası", "title": "İçe Aktarma Başarısız" diff --git a/src/locales/zh-TW/main.json b/src/locales/zh-TW/main.json index 0a148681ae..84bcf62b64 100644 --- a/src/locales/zh-TW/main.json +++ b/src/locales/zh-TW/main.json @@ -238,6 +238,12 @@ "title": "建立帳戶" } }, + "boundingBox": { + "height": "高度", + "width": "寬度", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "清除工作流程", "deleteBlueprint": "刪除藍圖", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "沒有可比較的圖像" }, + "imageCrop": { + "cropPreviewAlt": "裁切預覽", + "loading": "載入中...", + "noInputImage": "未連接輸入影像" + }, "importFailed": { "copyError": "複製錯誤", "title": "匯入失敗" diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index 1108b17020..3551fbb922 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -238,6 +238,12 @@ "title": "创建一个账户" } }, + "boundingBox": { + "height": "高度", + "width": "宽度", + "x": "X", + "y": "Y" + }, "breadcrumbsMenu": { "clearWorkflow": "清除工作流", "deleteBlueprint": "删除蓝图", @@ -982,6 +988,11 @@ "imageCompare": { "noImages": "没有可以对比的图像" }, + "imageCrop": { + "cropPreviewAlt": "裁剪预览", + "loading": "加载中...", + "noInputImage": "未连接输入图像" + }, "importFailed": { "copyError": "复制错误", "title": "导入失败" From 284bdce61b85f4e64e36e9975ef68ebcb8f81a25 Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Sat, 17 Jan 2026 20:28:48 -0800 Subject: [PATCH 005/375] Add a slider indicator for number widgets in vue mode. (#8122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes it's difficult to gauge the valid range of values for a widget. Litegraph includes a "slider" widget which displays the distance from the min and max values as a colored bar. However, this implementation is rather strongly disliked because it prevents entering an exact number. Vue mode makes it simple to add just the indicator onto our existing widget. In addition to requiring both min and max be set, not every widget would want this functionality. It's not useful information for seed, but also has potential to cause confusion on widgets like CFG, that allow inputting numbers up to 100 even though values beyond ~15 are rarely desirable. As a proposed heuristic, the ratio of "step" to distance between min and max is currently used, but this could fairly easily be changed to an opt-in only system. image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8122-Add-a-slider-indicator-for-number-widgets-in-vue-mode-2eb6d73d365081218fc8e86f37001958) by [Unito](https://www.unito.io) --- .../components/WidgetInputNumberInput.vue | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue index 1add11f4a7..d14dd2167a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue @@ -41,7 +41,8 @@ const modelValue = defineModel({ default: 0 }) const formattedValue = computed(() => { const unformattedValue = dragValue.value ?? modelValue.value - if (!isFinite(unformattedValue)) return `${unformattedValue}` + if ((unformattedValue as unknown) === '' || !isFinite(unformattedValue)) + return `${unformattedValue}` return n(unformattedValue, { useGrouping: useGrouping.value, @@ -175,6 +176,20 @@ const buttonTooltip = computed(() => { } return null }) + +const sliderWidth = computed(() => { + const { max, min, step } = filteredProps.value + if ( + min === undefined || + max === undefined || + step === undefined || + (max - min) / step >= 100 + ) + return 0 + const value = dragValue.value ?? modelValue.value + const ratio = (value - min) / (max - min) + return (ratio * 100).toFixed(0) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue index f571d6dade..e4e1220bf6 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue @@ -2,6 +2,7 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue' import type { Component } from 'vue' +import Popover from '@/components/ui/Popover.vue' import Button from '@/components/ui/button/Button.vue' import type { SimplifiedControlWidget, @@ -19,8 +20,6 @@ const props = defineProps<{ const modelValue = defineModel() -const popover = ref() - const controlModel = ref(props.widget.controlWidget.value) const controlButtonIcon = computed(() => { @@ -37,24 +36,24 @@ const controlButtonIcon = computed(() => { }) watch(controlModel, props.widget.controlWidget.update) - -const togglePopover = (event: Event) => { - popover.value.toggle(event) -} - From a8b4928accb21b198f8c5bac82eceac9a5f4a39b Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Mon, 19 Jan 2026 20:57:08 -0800 Subject: [PATCH 017/375] feat(canvas): show 'Show Advanced' button on nodes with advanced widgets (#8148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing 'Show Advanced' button (previously subgraph-only) to also appear on regular nodes that have widgets marked with `options.advanced = true`. ## Changes - Updates `showAdvancedInputsButton` computed to check for advanced widgets on regular nodes - Updates `handleShowAdvancedInputs` to set `node.showAdvanced = true` and trigger canvas redraw for regular nodes ## Related - Backend PR that adds `advanced` flag: comfyanonymous/ComfyUI#11939 - Canvas hide PR: feat/advanced-widgets-canvas-hide (this PR provides the toggle for that) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8148-feat-canvas-show-Show-Advanced-button-on-nodes-with-advanced-widgets-2ec6d73d36508155a8adfa0a8ec84d46) by [Unito](https://www.unito.io) --- .../vueNodes/components/LGraphNode.vue | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index b990c957a1..1d96cda830 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -482,18 +482,30 @@ const lgraphNode = computed(() => { const showAdvancedInputsButton = computed(() => { const node = lgraphNode.value - if (!node || !(node instanceof SubgraphNode)) return false + if (!node) return false - // Check if there are hidden inputs (widgets not promoted) - const interiorNodes = node.subgraph.nodes - const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? []) + // For subgraph nodes: check for unpromoted widgets + if (node instanceof SubgraphNode) { + const interiorNodes = node.subgraph.nodes + const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? []) + return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted) + } - return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted) + // For regular nodes: show button if there are advanced widgets and they're currently hidden + const hasAdvancedWidgets = nodeData.widgets?.some((w) => w.options?.advanced) + return hasAdvancedWidgets && !node.showAdvanced }) function handleShowAdvancedInputs() { - const rightSidePanelStore = useRightSidePanelStore() - rightSidePanelStore.focusSection('advanced-inputs') + const node = lgraphNode.value + if (!node) return + + if (node instanceof SubgraphNode) { + const rightSidePanelStore = useRightSidePanelStore() + rightSidePanelStore.focusSection('advanced-inputs') + } else { + node.showAdvanced = true + } } const nodeMedia = computed(() => { From b5f91977c842e447d4c962431f131788289bf1b7 Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Tue, 20 Jan 2026 14:48:44 +0900 Subject: [PATCH 018/375] [bugfix] Add spacing between action buttons in node library sidebar (#8172) --- src/components/common/TreeExplorerTreeNode.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/TreeExplorerTreeNode.vue b/src/components/common/TreeExplorerTreeNode.vue index cea8ba451f..450e8d9d99 100644 --- a/src/components/common/TreeExplorerTreeNode.vue +++ b/src/components/common/TreeExplorerTreeNode.vue @@ -28,7 +28,7 @@ />
From 916c1248e3948d76babb60b5ea0b4dc57d03e989 Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Wed, 21 Jan 2026 06:10:31 +0900 Subject: [PATCH 019/375] [bugfix] Fix search bar height alignment in MediaAssetFilterBar (#8171) --- .../assets/components/MediaAssetFilterBar.vue | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/src/platform/assets/components/MediaAssetFilterBar.vue b/src/platform/assets/components/MediaAssetFilterBar.vue index 6a0fcdb939..0442f5f565 100644 --- a/src/platform/assets/components/MediaAssetFilterBar.vue +++ b/src/platform/assets/components/MediaAssetFilterBar.vue @@ -1,40 +1,26 @@ From 79d3b2c291c4bfe1170fb100ca0fc4b3b1bfe8f3 Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Tue, 20 Jan 2026 13:31:56 -0800 Subject: [PATCH 020/375] Fix properties context menu (#8188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tiny fix for a regression introduced in #7817 that prevented changing a node's properties through the litegraph context menu. image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8188-Fix-properties-context-menu-2ee6d73d365081ba8844dd3c8d74432d) by [Unito](https://www.unito.io) --- src/lib/litegraph/src/LGraphCanvas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 30d7a3fd69..40893174f8 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher }) function inner_clicked( - this: ContextMenu, + this: ContextMenuDivElement, v?: string | IContextMenuValue ) { if (!node || typeof v === 'string' || !v?.value) return - const rect = this.root.getBoundingClientRect() + const rect = this.getBoundingClientRect() canvas.showEditPropertyValue(node, v.value, { position: [rect.left, rect.top] }) From 5df793b721c02ef93e7278b4d7bbbcf26ccc11fc Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 20 Jan 2026 13:35:54 -0800 Subject: [PATCH 021/375] feat: add feature usage tracker for nightly surveys (#8175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces `useFeatureUsageTracker` composable that tracks how many times a user has used a specific feature, along with first and last usage timestamps. Data persists to localStorage using `@vueuse/core`'s `useStorage`. This composable provides the foundation for triggering surveys after a configurable number of feature uses. Includes comprehensive unit tests. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8175-feat-add-feature-usage-tracker-for-nightly-surveys-2ee6d73d36508118859ece6fcf17561d) by [Unito](https://www.unito.io) --- .../surveys/useFeatureUsageTracker.test.ts | 131 ++++++++++++++++++ .../surveys/useFeatureUsageTracker.ts | 46 ++++++ 2 files changed, 177 insertions(+) create mode 100644 src/platform/surveys/useFeatureUsageTracker.test.ts create mode 100644 src/platform/surveys/useFeatureUsageTracker.ts diff --git a/src/platform/surveys/useFeatureUsageTracker.test.ts b/src/platform/surveys/useFeatureUsageTracker.test.ts new file mode 100644 index 0000000000..5313e9eafd --- /dev/null +++ b/src/platform/surveys/useFeatureUsageTracker.test.ts @@ -0,0 +1,131 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const STORAGE_KEY = 'Comfy.FeatureUsage' + +describe('useFeatureUsageTracker', () => { + beforeEach(() => { + localStorage.clear() + vi.resetModules() + }) + + afterEach(() => { + localStorage.clear() + }) + + it('initializes with zero count for new feature', async () => { + const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + const { useCount } = useFeatureUsageTracker('test-feature') + + expect(useCount.value).toBe(0) + }) + + it('increments count on trackUsage', async () => { + const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + const { useCount, trackUsage } = useFeatureUsageTracker('test-feature') + + expect(useCount.value).toBe(0) + + trackUsage() + expect(useCount.value).toBe(1) + + trackUsage() + expect(useCount.value).toBe(2) + }) + + it('sets firstUsed only on first use', async () => { + vi.useFakeTimers() + const firstTs = 1000000 + vi.setSystemTime(firstTs) + try { + const { useFeatureUsageTracker } = + await import('./useFeatureUsageTracker') + const { usage, trackUsage } = useFeatureUsageTracker('test-feature') + + trackUsage() + expect(usage.value?.firstUsed).toBe(firstTs) + + vi.setSystemTime(firstTs + 5000) + trackUsage() + expect(usage.value?.firstUsed).toBe(firstTs) + } finally { + vi.useRealTimers() + } + }) + + it('updates lastUsed on each use', async () => { + vi.useFakeTimers() + try { + const { useFeatureUsageTracker } = + await import('./useFeatureUsageTracker') + const { usage, trackUsage } = useFeatureUsageTracker('test-feature') + + trackUsage() + const firstLastUsed = usage.value?.lastUsed ?? 0 + + vi.advanceTimersByTime(10) + trackUsage() + + expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed) + } finally { + vi.useRealTimers() + } + }) + + it('reset clears feature data', async () => { + const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + const { useCount, trackUsage, reset } = + useFeatureUsageTracker('test-feature') + + trackUsage() + trackUsage() + expect(useCount.value).toBe(2) + + reset() + expect(useCount.value).toBe(0) + }) + + it('tracks multiple features independently', async () => { + const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + const featureA = useFeatureUsageTracker('feature-a') + const featureB = useFeatureUsageTracker('feature-b') + + featureA.trackUsage() + featureA.trackUsage() + featureB.trackUsage() + + expect(featureA.useCount.value).toBe(2) + expect(featureB.useCount.value).toBe(1) + }) + + it('persists to localStorage', async () => { + vi.useFakeTimers() + try { + const { useFeatureUsageTracker } = + await import('./useFeatureUsageTracker') + const { trackUsage } = useFeatureUsageTracker('persisted-feature') + + trackUsage() + await vi.runAllTimersAsync() + + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') + expect(stored['persisted-feature']?.useCount).toBe(1) + } finally { + vi.useRealTimers() + } + }) + + it('loads existing data from localStorage', async () => { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + 'existing-feature': { useCount: 5, firstUsed: 1000, lastUsed: 2000 } + }) + ) + + vi.resetModules() + const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + const { useCount } = useFeatureUsageTracker('existing-feature') + + expect(useCount.value).toBe(5) + }) +}) diff --git a/src/platform/surveys/useFeatureUsageTracker.ts b/src/platform/surveys/useFeatureUsageTracker.ts new file mode 100644 index 0000000000..b8d825db48 --- /dev/null +++ b/src/platform/surveys/useFeatureUsageTracker.ts @@ -0,0 +1,46 @@ +import { useStorage } from '@vueuse/core' +import { computed } from 'vue' + +interface FeatureUsage { + useCount: number + firstUsed: number + lastUsed: number +} + +type FeatureUsageRecord = Record + +const STORAGE_KEY = 'Comfy.FeatureUsage' + +/** + * Tracks feature usage for survey eligibility. + * Persists to localStorage. + */ +export function useFeatureUsageTracker(featureId: string) { + const usageData = useStorage(STORAGE_KEY, {}) + + const usage = computed(() => usageData.value[featureId]) + + const useCount = computed(() => usage.value?.useCount ?? 0) + + function trackUsage() { + const now = Date.now() + const existing = usageData.value[featureId] + + usageData.value[featureId] = { + useCount: (existing?.useCount ?? 0) + 1, + firstUsed: existing?.firstUsed ?? now, + lastUsed: now + } + } + + function reset() { + delete usageData.value[featureId] + } + + return { + usage, + useCount, + trackUsage, + reset + } +} From e8b45204f2922fe46d2b25c0f0b0894f13e926a8 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 20 Jan 2026 14:22:25 -0800 Subject: [PATCH 022/375] feat(panel): add collapsible Advanced Inputs section for widgets marked advanced (#8146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a collapsible 'Advanced Inputs' section to the right-side panel that displays widgets marked with `options.advanced = true`. image ## Changes - Filters normal widgets to exclude advanced ones - Adds new `advancedWidgetsSectionDataList` computed for advanced widgets - Renders a collapsible section (collapsed by default) for advanced widgets ## Related - Backend PR that adds `advanced` flag: comfyanonymous/ComfyUI#11939 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8146-feat-panel-add-collapsible-Advanced-Inputs-section-for-widgets-marked-advanced-2ec6d73d36508120af1af27110a6fb96) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action Co-authored-by: Rizumu Ayaka --- .../parameters/TabNormalInputs.vue | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/components/rightSidePanel/parameters/TabNormalInputs.vue b/src/components/rightSidePanel/parameters/TabNormalInputs.vue index 30689f1a41..add1288763 100644 --- a/src/components/rightSidePanel/parameters/TabNormalInputs.vue +++ b/src/components/rightSidePanel/parameters/TabNormalInputs.vue @@ -25,13 +25,31 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => { return nodes.map((node) => { const { widgets = [] } = node const shownWidgets = widgets - .filter((w) => !(w.options?.canvasOnly || w.options?.hidden)) + .filter( + (w) => + !(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced) + ) .map((widget) => ({ node, widget })) return { widgets: shownWidgets, node } }) }) +const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => { + return nodes + .map((node) => { + const { widgets = [] } = node + const advancedWidgets = widgets + .filter( + (w) => + !(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced + ) + .map((widget) => ({ node, widget })) + return { widgets: advancedWidgets, node } + }) + .filter(({ widgets }) => widgets.length > 0) +}) + const isMultipleNodesSelected = computed( () => widgetsSectionDataList.value.length > 1 ) @@ -56,6 +74,12 @@ const label = computed(() => { : t('rightSidePanel.inputsNone') : undefined // SectionWidgets display node titles by default }) + +const advancedLabel = computed(() => { + return !mustShowNodeTitle && !isMultipleNodesSelected.value + ? t('rightSidePanel.advancedInputs') + : undefined // SectionWidgets display node titles by default +}) From f5a784e5619dda3e81ca1771650a3edf1e1520c3 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 20 Jan 2026 15:52:40 -0800 Subject: [PATCH 023/375] fix: add plurilization to node pack count in custom node manager dialog (#8191) --- src/locales/en/main.json | 1 + .../manager/packCard/PackCard.test.ts | 25 ++++++++++++++++--- .../components/manager/packCard/PackCard.vue | 9 ++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index fa557ee96b..e87121414b 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -158,6 +158,7 @@ "choose_file_to_upload": "choose file to upload", "capture": "capture", "nodes": "Nodes", + "nodesCount": "{count} nodes | {count} node | {count} nodes", "community": "Community", "all": "All", "versionMismatchWarning": "Version Compatibility Warning", diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts b/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts index 3e51167390..b3a92e5a33 100644 --- a/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts @@ -10,15 +10,22 @@ import type { RegistryPack } from '@/workbench/extensions/manager/types/comfyManagerTypes' +const translateMock = vi.hoisted(() => + vi.fn((key: string, choice?: number) => + typeof choice === 'number' ? `${key}-${choice}` : key + ) +) +const dateMock = vi.hoisted(() => vi.fn(() => '2024. 1. 1.')) + // Mock dependencies vi.mock('vue-i18n', () => ({ useI18n: vi.fn(() => ({ - d: vi.fn(() => '2024. 1. 1.'), - t: vi.fn((key: string) => key) + d: dateMock, + t: translateMock })), createI18n: vi.fn(() => ({ global: { - t: vi.fn((key: string) => key), + t: translateMock, te: vi.fn(() => true) } })) @@ -187,6 +194,18 @@ describe('PackCard', () => { // Should still render without errors expect(wrapper.exists()).toBe(true) }) + + it('should use localized singular/plural nodes label', () => { + const packWithNodes = { + ...mockNodePack, + comfy_nodes: ['node-a'] + } as MergedNodePack + + const wrapper = createWrapper({ nodePack: packWithNodes }) + + expect(wrapper.text()).toContain('g.nodesCount-1') + expect(translateMock).toHaveBeenCalledWith('g.nodesCount', 1) + }) }) describe('component structure', () => { diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCard.vue b/src/workbench/extensions/manager/components/manager/packCard/PackCard.vue index 6900e71dfc..17ffdd1028 100644 --- a/src/workbench/extensions/manager/components/manager/packCard/PackCard.vue +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCard.vue @@ -36,8 +36,8 @@

-
- {{ nodesCount }} {{ $t('g.nodes') }} +
+ {{ nodesLabel }}
() -const { d } = useI18n() +const { d, t } = useI18n() const colorPaletteStore = useColorPaletteStore() const isLightTheme = computed( @@ -115,6 +115,9 @@ const isDisabled = computed( const nodesCount = computed(() => isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined ) +const nodesLabel = computed(() => + nodesCount.value ? t('g.nodesCount', nodesCount.value) : '' +) const publisherName = computed(() => { if (!nodePack) return null From e6ef99e92ca446bbd869190e725ae2f219ce1a88 Mon Sep 17 00:00:00 2001 From: Simula_r <18093452+simula-r@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:13:54 -0800 Subject: [PATCH 024/375] feat: add isCloud guard to team workspaces feature flag (#8192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensures the team_workspaces_enabled feature flag only returns true when running in cloud environment, preventing the feature from activating in local/desktop installations. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8192-feat-add-isCloud-guard-to-team-workspaces-feature-flag-2ee6d73d3650810bb1d7c1721ebcdd44) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: GitHub Action --- src/composables/useFeatureFlags.ts | 3 +++ .../assets/components/MediaAssetFilterBar.vue | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 136b7ccd1f..ca54bb9c6f 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -1,5 +1,6 @@ import { computed, reactive, readonly } from 'vue' +import { isCloud } from '@/platform/distribution/types' import { remoteConfig } from '@/platform/remoteConfig/remoteConfig' import { api } from '@/scripts/api' @@ -95,6 +96,8 @@ export function useFeatureFlags() { ) }, get teamWorkspacesEnabled() { + if (!isCloud) return false + return ( remoteConfig.value.team_workspaces_enabled ?? api.getServerFeature(ServerFeatureFlag.TEAM_WORKSPACES_ENABLED, false) diff --git a/src/platform/assets/components/MediaAssetFilterBar.vue b/src/platform/assets/components/MediaAssetFilterBar.vue index 0442f5f565..6e4ec5e4c1 100644 --- a/src/platform/assets/components/MediaAssetFilterBar.vue +++ b/src/platform/assets/components/MediaAssetFilterBar.vue @@ -6,20 +6,36 @@ @update:model-value="handleSearchChange" />
- + - + - +
From b1dfbfaa09cd5f76ca8c11715a913283dfd4b3c3 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Tue, 20 Jan 2026 16:44:08 -0800 Subject: [PATCH 025/375] chore: Replace prettier with oxfmt (#8177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure oxfmt ignorePatterns to exclude non-JS/TS files (md, json, css, yaml, etc.) to match previous Prettier behavior. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8177-chore-configure-oxfmt-to-format-only-JS-TS-Vue-files-2ee6d73d3650815080f3cc8a4a932109) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp --- .claude/commands/setup_repo.md | 2 +- .github/workflows/ci-lint-format.yaml | 6 +- .i18nrc.cjs | 18 +- .oxfmtrc.json | 20 ++ .prettierignore | 2 - .prettierrc | 11 - .vscode/extensions.json | 15 +- AGENTS.md | 8 +- eslint.config.ts | 6 +- lint-staged.config.mjs | 25 -- lint-staged.config.ts | 5 +- package.json | 10 +- pnpm-lock.yaml | 323 ++++++------------- pnpm-workspace.yaml | 4 +- src/composables/useContextMenuTranslation.ts | 3 +- vite.config.mts | 3 +- 16 files changed, 158 insertions(+), 303 deletions(-) create mode 100644 .oxfmtrc.json delete mode 100644 .prettierignore delete mode 100644 .prettierrc delete mode 100644 lint-staged.config.mjs diff --git a/.claude/commands/setup_repo.md b/.claude/commands/setup_repo.md index d82e22ec61..71dee96a51 100644 --- a/.claude/commands/setup_repo.md +++ b/.claude/commands/setup_repo.md @@ -122,7 +122,7 @@ echo " pnpm build - Build for production" echo " pnpm test:unit - Run unit tests" echo " pnpm typecheck - Run TypeScript checks" echo " pnpm lint - Run ESLint" -echo " pnpm format - Format code with Prettier" +echo " pnpm format - Format code with oxfmt" echo "" echo "Next steps:" echo "1. Run 'pnpm dev' to start developing" diff --git a/.github/workflows/ci-lint-format.yaml b/.github/workflows/ci-lint-format.yaml index 3ce6d6aa9f..c97f6255ca 100644 --- a/.github/workflows/ci-lint-format.yaml +++ b/.github/workflows/ci-lint-format.yaml @@ -42,7 +42,7 @@ jobs: - name: Run Stylelint with auto-fix run: pnpm stylelint:fix - - name: Run Prettier with auto-format + - name: Run oxfmt with auto-format run: pnpm format - name: Check for changes @@ -60,7 +60,7 @@ jobs: git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add . - git commit -m "[automated] Apply ESLint and Prettier fixes" + git commit -m "[automated] Apply ESLint and Oxfmt fixes" git push - name: Final validation @@ -80,7 +80,7 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Prettier formatting' + body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Oxfmt formatting' }) - name: Comment on PR about manual fix needed diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 86ce06eaa3..4369f0a707 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -1,7 +1,7 @@ // This file is intentionally kept in CommonJS format (.cjs) // to resolve compatibility issues with dependencies that require CommonJS. // Do not convert this file to ESModule format unless all dependencies support it. -const { defineConfig } = require('@lobehub/i18n-cli'); +const { defineConfig } = require('@lobehub/i18n-cli') module.exports = defineConfig({ modelName: 'gpt-4.1', @@ -10,7 +10,19 @@ module.exports = defineConfig({ entry: 'src/locales/en', entryLocale: 'en', output: 'src/locales', - outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR', 'fa'], + outputLocales: [ + 'zh', + 'zh-TW', + 'ru', + 'ja', + 'ko', + 'fr', + 'es', + 'ar', + 'tr', + 'pt-BR', + 'fa' + ], reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. @@ -26,4 +38,4 @@ module.exports = defineConfig({ - Use Arabic-Indic numerals (۰-۹) for numbers where appropriate. - Maintain consistency with terminology used in Persian software and design applications. ` -}); +}) diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000000..5da4febe23 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "singleQuote": true, + "tabWidth": 2, + "semi": false, + "trailingComma": "none", + "printWidth": 80, + "ignorePatterns": [ + "packages/registry-types/src/comfyRegistryTypes.ts", + "src/types/generatedManagerTypes.ts", + "**/*.md", + "**/*.json", + "**/*.css", + "**/*.yaml", + "**/*.yml", + "**/*.html", + "**/*.svg", + "**/*.xml" + ] +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 4403edd8ec..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -packages/registry-types/src/comfyRegistryTypes.ts -src/types/generatedManagerTypes.ts diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index aa43a43ac0..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "singleQuote": true, - "tabWidth": 2, - "semi": false, - "trailingComma": "none", - "printWidth": 80, - "importOrder": ["^@core/(.*)$", "", "^@/(.*)$", "^[./]"], - "importOrderSeparation": true, - "importOrderSortSpecifiers": true, - "plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"] -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 54f28d4002..9cbac42d76 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,25 +1,22 @@ { "recommendations": [ + "antfu.vite", "austenc.tailwind-docs", "bradlc.vscode-tailwindcss", "davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", + "donjayamanne.githistory", "eamodio.gitlens", - "esbenp.prettier-vscode", - "figma.figma-vscode-extension", "github.vscode-github-actions", "github.vscode-pull-request-github", "hbenl.vscode-test-explorer", + "kisstkondoros.vscode-codemetrics", "lokalise.i18n-ally", "ms-playwright.playwright", + "oxc.oxc-vscode", + "sonarsource.sonarlint-vscode", "vitest.explorer", "vue.volar", - "sonarsource.sonarlint-vscode", - "deque-systems.vscode-axe-linter", - "kisstkondoros.vscode-codemetrics", - "donjayamanne.githistory", - "wix.vscode-import-cost", - "prograhammer.tslint-vue", - "antfu.vite" + "wix.vscode-import-cost" ] } diff --git a/AGENTS.md b/AGENTS.md index da2953783c..9938865a94 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,10 +27,10 @@ See @docs/guidance/*.md for file-type-specific conventions (auto-loaded by glob) - Build output: `dist/` - Configs - `vite.config.mts` - - `vitest.config.ts` - `playwright.config.ts` - `eslint.config.ts` - - `.prettierrc` + - `.oxfmtrc.json` + - `.oxlintrc.json` - etc. ## Monorepo Architecture @@ -46,7 +46,7 @@ The project uses **Nx** for build orchestration and task management - `pnpm test:unit`: Run Vitest unit tests - `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`) - `pnpm lint` / `pnpm lint:fix`: Lint (ESLint) -- `pnpm format` / `pnpm format:check`: Prettier +- `pnpm format` / `pnpm format:check`: oxfmt - `pnpm typecheck`: Vue TSC type checking - `pnpm storybook`: Start Storybook development server @@ -72,7 +72,7 @@ The project uses **Nx** for build orchestration and task management - Composition API only - Tailwind 4 styling - Avoid ` diff --git a/src/components/sidebar/SidebarIcon.test.ts b/src/components/sidebar/SidebarIcon.test.ts index 7564e7bcdc..284a298251 100644 --- a/src/components/sidebar/SidebarIcon.test.ts +++ b/src/components/sidebar/SidebarIcon.test.ts @@ -1,6 +1,5 @@ import { mount } from '@vue/test-utils' import PrimeVue from 'primevue/config' -import OverlayBadge from 'primevue/overlaybadge' import Tooltip from 'primevue/tooltip' import { describe, expect, it } from 'vitest' import { createI18n } from 'vue-i18n' @@ -33,8 +32,7 @@ describe('SidebarIcon', () => { return mount(SidebarIcon, { global: { plugins: [PrimeVue, i18n], - directives: { tooltip: Tooltip }, - components: { OverlayBadge } + directives: { tooltip: Tooltip } }, props: { ...exampleProps, ...props }, ...options @@ -54,9 +52,9 @@ describe('SidebarIcon', () => { it('creates badge when iconBadge prop is set', () => { const badge = '2' const wrapper = mountSidebarIcon({ iconBadge: badge }) - const badgeEl = wrapper.findComponent(OverlayBadge) + const badgeEl = wrapper.find('.sidebar-icon-badge') expect(badgeEl.exists()).toBe(true) - expect(badgeEl.find('.p-badge').text()).toEqual(badge) + expect(badgeEl.text()).toEqual(badge) }) it('shows tooltip on hover', async () => { diff --git a/src/components/sidebar/SidebarIcon.vue b/src/components/sidebar/SidebarIcon.vue index 88900c1a70..10dfca8f80 100644 --- a/src/components/sidebar/SidebarIcon.vue +++ b/src/components/sidebar/SidebarIcon.vue @@ -17,22 +17,28 @@ >