From 6535138e0b74decb1e31a0fa49ec73c0916a78b9 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 7 Feb 2026 15:30:20 -0800 Subject: [PATCH 1/4] fix(vue-nodes): hide slot labels for reroute nodes with empty names (#8574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes reroute node styling in Vue Nodes 2.0 by hiding slot labels when slot names are intentionally empty. | Before | After | | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | image | image | ## Problem Reroute nodes displayed unwanted fallback labels ("Input 0", "Output 0") instead of appearing as minimal connection-only nodes. This happened because: - Reroute nodes intentionally use empty string (`""`) for slot names - Slot components used `||` operator for fallback labels, treating `''` as falsy ## Solution - Add `hasNoLabel` computed property to detect when all label sources (`label`, `localized_name`, `name`) are empty/falsy - Derive `dotOnly` from either the existing prop OR `hasNoLabel` being true - When `dotOnly` is true: label text is hidden, padding removed (`lg-slot--dot-only` class), only connection dot visible Combined with existing `NO_TITLE` support from #7589, reroutes now display as minimal nodes with just connection dots—matching classic reroute appearance. ## Summary by CodeRabbit ## Release Notes * **Bug Fixes** * Enhanced input and output slot label handling to automatically conceal labels when unavailable * Improved fallback display names for slots with more reliable naming logic ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8574-fix-vue-nodes-hide-slot-labels-for-reroute-nodes-with-empty-names-2fc6d73d365081c38031e260402283d3) by [Unito](https://www.unito.io) --- .../extensions/vueNodes/components/InputSlot.vue | 15 +++++++++++---- .../extensions/vueNodes/components/OutputSlot.vue | 14 +++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue index 61c4dd35a..ba8ae57b1 100644 --- a/src/renderer/extensions/vueNodes/components/InputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue @@ -7,7 +7,7 @@ cn( 'lg-slot lg-slot--input flex items-center group rounded-r-lg m-0', 'cursor-crosshair', - props.dotOnly ? 'lg-slot--dot-only' : 'pr-6', + dotOnly ? 'lg-slot--dot-only' : 'pr-6', { 'lg-slot--connected': props.connected, 'lg-slot--compatible': props.compatible, @@ -36,7 +36,7 @@
- - {{ slotData.localized_name || slotData.name || `Output ${index}` }} + + {{ slotData.localized_name || (slotData.name ?? `Output ${index}`) }}
@@ -44,6 +47,11 @@ interface OutputSlotProps { const props = defineProps() +const hasNoLabel = computed( + () => !props.slotData.localized_name && props.slotData.name === '' +) +const dotOnly = computed(() => props.dotOnly || hasNoLabel.value) + // Error boundary implementation const renderError = ref(null) @@ -79,7 +87,7 @@ const slotWrapperClass = computed(() => cn( 'lg-slot lg-slot--output flex items-center justify-end group rounded-l-lg h-6', 'cursor-crosshair', - props.dotOnly ? 'lg-slot--dot-only justify-center' : 'pl-6', + dotOnly.value ? 'lg-slot--dot-only justify-center' : 'pl-6', { 'lg-slot--connected': props.connected, 'lg-slot--compatible': props.compatible, From 828323e263dc4f1d59c209cb07a3b83f641ebf49 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Sat, 7 Feb 2026 21:19:37 -0500 Subject: [PATCH 2/4] fix: add post-processing script to fix i18n nodeDefs array corruption (#8718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary @lobehub/i18n-cli (GPT-4.1) converts numeric-keyed objects like {"0": {...}, "1": {...}} into JSON arrays with null gaps, which crashes vue-i18n path resolution. Add a post-processing step that converts arrays back to objects after translation. ## Screenshots (if applicable) before https://github.com/user-attachments/assets/44e81790-feae-405b-b2c4-098b06a98785 after https://github.com/user-attachments/assets/5d1bd836-c923-437a-aca0-7ebd4d8acb89 image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8718-fix-add-post-processing-script-to-fix-i18n-nodeDefs-array-corruption-3006d73d365081dab020fea997ec4c0a) by [Unito](https://www.unito.io) --- .github/workflows/i18n-update-core.yaml | 2 +- .../workflows/i18n-update-custom-nodes.yaml | 2 +- .github/workflows/i18n-update-nodes.yaml | 2 +- package.json | 1 + scripts/fix-i18n-node-defs.ts | 66 +++++++++++++++++++ 5 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 scripts/fix-i18n-node-defs.ts diff --git a/.github/workflows/i18n-update-core.yaml b/.github/workflows/i18n-update-core.yaml index 5f0985b93..38898f014 100644 --- a/.github/workflows/i18n-update-core.yaml +++ b/.github/workflows/i18n-update-core.yaml @@ -43,7 +43,7 @@ jobs: env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - name: Update translations - run: pnpm locale && pnpm format + run: pnpm locale && pnpm fix-i18n-node-defs && pnpm format env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Commit updated locales diff --git a/.github/workflows/i18n-update-custom-nodes.yaml b/.github/workflows/i18n-update-custom-nodes.yaml index 225c1b3e3..4c6788fad 100644 --- a/.github/workflows/i18n-update-custom-nodes.yaml +++ b/.github/workflows/i18n-update-custom-nodes.yaml @@ -67,7 +67,7 @@ jobs: env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - name: Update translations - run: pnpm locale + run: pnpm locale && pnpm fix-i18n-node-defs env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Diff base vs updated i18n diff --git a/.github/workflows/i18n-update-nodes.yaml b/.github/workflows/i18n-update-nodes.yaml index 5a72e5b10..8c974addb 100644 --- a/.github/workflows/i18n-update-nodes.yaml +++ b/.github/workflows/i18n-update-nodes.yaml @@ -36,7 +36,7 @@ jobs: env: PLAYWRIGHT_TEST_URL: http://localhost:5173 - name: Update translations - run: pnpm locale + run: pnpm locale && pnpm fix-i18n-node-defs env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Create Pull Request diff --git a/package.json b/package.json index 643b3c71a..9181a69b9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dev": "nx serve", "devtools:pycheck": "python3 -m compileall -q tools/devtools", "format:check": "oxfmt --check", + "fix-i18n-node-defs": "tsx scripts/fix-i18n-node-defs.ts", "format": "oxfmt --write", "json-schema": "tsx scripts/generate-json-schema.ts", "knip:no-cache": "knip", diff --git a/scripts/fix-i18n-node-defs.ts b/scripts/fix-i18n-node-defs.ts new file mode 100644 index 000000000..569e79be6 --- /dev/null +++ b/scripts/fix-i18n-node-defs.ts @@ -0,0 +1,66 @@ +import { readFileSync, readdirSync, writeFileSync } from 'fs' +import { join } from 'path' + +const LOCALES_DIR = 'src/locales' + +/** + * Convert arrays with numeric indices back to objects. + * GPT-4.1 (used by @lobehub/i18n-cli) sometimes converts + * {"0": {...}, "1": {...}} objects into JSON arrays with null gaps. + */ +function fixArraysToObjects(value: unknown): Record | unknown { + if (!value || typeof value !== 'object') return value + + if (Array.isArray(value)) { + const obj: Record = {} + for (let i = 0; i < value.length; i++) { + if (value[i] != null) { + obj[String(i)] = fixArraysToObjects(value[i]) + } + } + return obj + } + + const record = value as Record + const result: Record = {} + for (const key of Object.keys(record)) { + result[key] = fixArraysToObjects(record[key]) + } + return result +} + +function run() { + const locales = readdirSync(LOCALES_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory() && d.name !== 'en') + .map((d) => d.name) + + let totalFixes = 0 + + for (const locale of locales) { + const filePath = join(LOCALES_DIR, locale, 'nodeDefs.json') + let raw: string + try { + raw = readFileSync(filePath, 'utf-8') + } catch { + continue + } + + const data = JSON.parse(raw) + const fixed = fixArraysToObjects(data) as Record + const fixedJson = JSON.stringify(fixed, null, 2) + '\n' + + if (fixedJson !== raw) { + writeFileSync(filePath, fixedJson) + totalFixes++ + console.warn(`Fixed: ${filePath}`) + } + } + + if (totalFixes === 0) { + console.warn('No fixes needed') + } else { + console.warn(`Fixed ${totalFixes} file(s)`) + } +} + +run() From b7fef1c744988d46f97547db2d422b46a72c400b Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 7 Feb 2026 19:46:23 -0800 Subject: [PATCH 3/4] fix: update queue tooltip copy to include right-click hint (#8733) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation - Update the run-menu queue tooltip to include the right-click-to-clear-queue hint so the UI copy matches the requested product wording. ### Description - Replaced `sideToolbar.queueProgressOverlay.viewJobHistory` value in `src/locales/en/main.json` with `View active jobs (right-click to clear queue)`. ### Testing - Ran `pnpm lint` and `pnpm typecheck`, and both completed successfully. ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_6987ee702bdc8330ad0d58f0b014c262) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8733-fix-update-queue-tooltip-copy-to-include-right-click-hint-3016d73d365081c09fa7f582ee51c6c2) by [Unito](https://www.unito.io) --- src/locales/en/main.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 3113b703a..6675195d8 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -812,7 +812,7 @@ "activeJobsSuffix": "active jobs", "jobQueue": "Job Queue", "expandCollapsedQueue": "Expand job queue", - "viewJobHistory": "View job history", + "viewJobHistory": "View active jobs (right-click to clear queue)", "noActiveJobs": "No active jobs", "stubClipTextEncode": "CLIP Text Encode:", "jobsCompleted": "{count} job completed | {count} jobs completed", From 6c8473e4e4a77fa439503c49e2361cdce06bb288 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 7 Feb 2026 19:47:05 -0800 Subject: [PATCH 4/4] refactor: replace runtime isElectron() with build-time isDesktop constant (#8710) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace all runtime `isElectron()` function calls with the build-time `isDesktop` constant from `@/platform/distribution/types`, enabling dead-code elimination in non-desktop builds. ## Changes - **What**: Migrate 30 files from runtime `isElectron()` detection (checking `window.electronAPI`) to the compile-time `isDesktop` constant (driven by `__DISTRIBUTION__` Vite define). Remove `isElectron` from `envUtil.ts`. Update `isNativeWindow()` to use `isDesktop`. Guard `electronAPI()` calls behind `isDesktop` checks in stores. Update 7 test files to use `vi.hoisted` + getter mock pattern for per-test `isDesktop` toggling. Add `DISTRIBUTION=desktop` to `dev:electron` script. ## Review Focus - The `electronDownloadStore.ts` now guards the top-level `electronAPI()` call behind `isDesktop` to prevent crashes on non-desktop builds. - Test mocking pattern uses `vi.hoisted` with a getter to allow per-test toggling of the `isDesktop` value. - Pre-existing issues not addressed: `as ElectronAPI` cast in `envUtil.ts`, `:class="[]"` in `BaseViewTemplate.vue`, `@ts-expect-error` in `ModelLibrarySidebarTab.vue`. - This subsumes PR #8627 and renders PR #6122 and PR #7374 obsolete. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8710-refactor-replace-runtime-isElectron-with-build-time-isDesktop-constant-3006d73d365081c08037f0e61c2f6c77) by [Unito](https://www.unito.io) --- package.json | 2 +- src/App.vue | 5 +- src/components/TopMenuSection.test.ts | 15 ++++-- src/components/TopMenuSection.vue | 3 +- .../tabs/terminal/BaseTerminal.test.ts | 9 +++- .../tabs/terminal/BaseTerminal.vue | 5 +- .../dialog/content/MissingModelsWarning.vue | 4 +- .../helpcenter/HelpCenterMenuContent.vue | 18 +++---- .../sidebar/tabs/ModelLibrarySidebarTab.vue | 4 +- src/components/topbar/WorkflowTabs.vue | 4 +- .../sidebarTabs/useModelLibrarySidebarTab.ts | 4 +- src/composables/useExternalLink.test.ts | 21 +++++--- src/composables/useExternalLink.ts | 5 +- src/extensions/core/electronAdapter.ts | 5 +- src/platform/distribution/types.ts | 6 +-- .../settings/composables/useSettingUI.ts | 10 ++-- .../updates/common/releaseStore.test.ts | 22 ++++---- src/platform/updates/common/releaseStore.ts | 9 ++-- .../ReleaseNotificationToast.test.ts | 25 ++++++---- .../components/ReleaseNotificationToast.vue | 4 +- src/router.ts | 9 ++-- src/stores/aboutPanelStore.ts | 6 +-- src/stores/electronDownloadStore.ts | 50 +++++++++---------- src/stores/systemStatsStore.test.ts | 23 +++++---- src/stores/systemStatsStore.ts | 4 +- src/stores/workspace/bottomPanelStore.test.ts | 8 ++- src/stores/workspace/bottomPanelStore.ts | 4 +- src/utils/envUtil.ts | 8 ++- src/views/GraphView.vue | 10 ++-- src/views/templates/BaseViewTemplate.vue | 5 +- 30 files changed, 165 insertions(+), 142 deletions(-) diff --git a/package.json b/package.json index 9181a69b9..5dff07bd5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts", "dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve", "dev:desktop": "nx dev @comfyorg/desktop-ui", - "dev:electron": "nx serve --config vite.electron.config.mts", + "dev:electron": "cross-env DISTRIBUTION=desktop nx serve --config vite.electron.config.mts", "dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve", "dev": "nx serve", "devtools:pycheck": "python3 -m compileall -q tools/devtools", diff --git a/src/App.vue b/src/App.vue index f843a7347..f9e34fc68 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,7 +19,8 @@ import config from '@/config' import { useWorkspaceStore } from '@/stores/workspaceStore' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' -import { electronAPI, isElectron } from './utils/envUtil' +import { electronAPI } from '@/utils/envUtil' +import { isDesktop } from '@/platform/distribution/types' import { app } from '@/scripts/app' const workspaceStore = useWorkspaceStore() @@ -42,7 +43,7 @@ const showContextMenu = (event: MouseEvent) => { onMounted(() => { window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version - if (isElectron()) { + if (isDesktop) { document.addEventListener('contextmenu', showContextMenu) } diff --git a/src/components/TopMenuSection.test.ts b/src/components/TopMenuSection.test.ts index b2bf7041c..ade4a8ae6 100644 --- a/src/components/TopMenuSection.test.ts +++ b/src/components/TopMenuSection.test.ts @@ -18,9 +18,8 @@ import { useCommandStore } from '@/stores/commandStore' import { useExecutionStore } from '@/stores/executionStore' import { TaskItemImpl, useQueueStore } from '@/stores/queueStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' -import { isElectron } from '@/utils/envUtil' -const mockData = vi.hoisted(() => ({ isLoggedIn: false })) +const mockData = vi.hoisted(() => ({ isLoggedIn: false, isDesktop: false })) vi.mock('@/composables/auth/useCurrentUser', () => ({ useCurrentUser: () => { @@ -30,7 +29,13 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({ } })) -vi.mock('@/utils/envUtil') +vi.mock('@/platform/distribution/types', () => ({ + isCloud: false, + isNightly: false, + get isDesktop() { + return mockData.isDesktop + } +})) vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => ({ currentUser: null, @@ -107,6 +112,8 @@ describe('TopMenuSection', () => { beforeEach(() => { vi.resetAllMocks() localStorage.clear() + mockData.isDesktop = false + mockData.isLoggedIn = false }) describe('authentication state', () => { @@ -129,7 +136,7 @@ describe('TopMenuSection', () => { describe('on desktop platform', () => { it('should display LoginButton and not display CurrentUserButton', () => { - vi.mocked(isElectron).mockReturnValue(true) + mockData.isDesktop = true const wrapper = createWrapper() expect(wrapper.findComponent(LoginButton).exists()).toBe(true) expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false) diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 13b28e904..85a01d1a1 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -153,7 +153,7 @@ import { useQueueStore, useQueueUIStore } from '@/stores/queueStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' import { useWorkspaceStore } from '@/stores/workspaceStore' -import { isElectron } from '@/utils/envUtil' +import { isDesktop } from '@/platform/distribution/types' import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment' import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState' import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes' @@ -163,7 +163,6 @@ const workspaceStore = useWorkspaceStore() const rightSidePanelStore = useRightSidePanelStore() const managerState = useManagerState() const { isLoggedIn } = useCurrentUser() -const isDesktop = isElectron() const { t, n } = useI18n() const { toastErrorHandler } = useErrorHandling() const commandStore = useCommandStore() diff --git a/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts b/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts index b99e54ea1..a651475fb 100644 --- a/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts +++ b/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts @@ -55,10 +55,17 @@ vi.mock('@/composables/bottomPanelTabs/useTerminal', () => ({ })) vi.mock('@/utils/envUtil', () => ({ - isElectron: vi.fn(() => false), electronAPI: vi.fn(() => null) })) +const mockData = vi.hoisted(() => ({ isDesktop: false })) + +vi.mock('@/platform/distribution/types', () => ({ + get isDesktop() { + return mockData.isDesktop + } +})) + // Mock clipboard API Object.defineProperty(navigator, 'clipboard', { value: { diff --git a/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue b/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue index 655df0b65..54b337c20 100644 --- a/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue +++ b/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue @@ -35,7 +35,8 @@ import { useI18n } from 'vue-i18n' import Button from '@/components/ui/button/Button.vue' import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal' -import { electronAPI, isElectron } from '@/utils/envUtil' +import { electronAPI } from '@/utils/envUtil' +import { isDesktop } from '@/platform/distribution/types' import { cn } from '@/utils/tailwindUtil' const { t } = useI18n() @@ -85,7 +86,7 @@ const showContextMenu = (event: MouseEvent) => { electronAPI()?.showContextMenu({ type: 'text' }) } -if (isElectron()) { +if (isDesktop) { useEventListener(terminalEl, 'contextmenu', showContextMenu) } diff --git a/src/components/dialog/content/MissingModelsWarning.vue b/src/components/dialog/content/MissingModelsWarning.vue index 1b044e4d3..f939b6c6c 100644 --- a/src/components/dialog/content/MissingModelsWarning.vue +++ b/src/components/dialog/content/MissingModelsWarning.vue @@ -13,7 +13,7 @@
@@ -52,7 +52,7 @@ import { useProgressFavicon } from '@/composables/useProgressFavicon' import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig' import { i18n, loadLocale } from '@/i18n' import ModelImportProgressDialog from '@/platform/assets/components/ModelImportProgressDialog.vue' -import { isCloud } from '@/platform/distribution/types' +import { isCloud, isDesktop } from '@/platform/distribution/types' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning' @@ -78,7 +78,7 @@ import { useServerConfigStore } from '@/stores/serverConfigStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' -import { electronAPI, isElectron } from '@/utils/envUtil' +import { electronAPI } from '@/utils/envUtil' import LinearView from '@/views/LinearView.vue' import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue' @@ -111,7 +111,7 @@ watch( document.body.classList.add(DARK_THEME_CLASS) } - if (isElectron()) { + if (isDesktop) { electronAPI().changeTheme({ color: 'rgba(0, 0, 0, 0)', symbolColor: newTheme.colors.comfy_base['input-text'] @@ -121,7 +121,7 @@ watch( { immediate: true } ) -if (isElectron()) { +if (isDesktop) { watch( () => queueStore.tasks, (newTasks, oldTasks) => { diff --git a/src/views/templates/BaseViewTemplate.vue b/src/views/templates/BaseViewTemplate.vue index 786cd77ac..b6b245cf4 100644 --- a/src/views/templates/BaseViewTemplate.vue +++ b/src/views/templates/BaseViewTemplate.vue @@ -22,7 +22,8 @@