refactor: replace runtime isElectron() with build-time isDesktop constant (#8710)

## 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)
This commit is contained in:
Christian Byrne
2026-02-07 19:47:05 -08:00
committed by GitHub
parent b7fef1c744
commit 6c8473e4e4
30 changed files with 165 additions and 142 deletions

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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: {

View File

@@ -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)
}

View File

@@ -13,7 +13,7 @@
</div>
<ListBox :options="missingModels" class="comfy-missing-models">
<template #option="{ option }">
<Suspense v-if="isElectron()">
<Suspense v-if="isDesktop">
<ElectronFileDownload
:url="option.url"
:label="option.label"
@@ -40,7 +40,7 @@ import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
import FileDownload from '@/components/common/FileDownload.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
// TODO: Read this from server internal API rather than hardcoding here
// as some installations may wish to use custom sources

View File

@@ -159,13 +159,13 @@ import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useExternalLink } from '@/composables/useExternalLink'
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 type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useCommandStore } from '@/stores/commandStore'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { electronAPI } from '@/utils/envUtil'
import { formatVersionAnchor } from '@/utils/formatUtil'
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
@@ -241,7 +241,7 @@ const moreItems = computed<MenuItem[]>(() => {
key: 'desktop-guide',
type: 'item',
label: t('helpCenter.desktopUserGuide'),
visible: isElectron(),
visible: isDesktop,
action: () => {
trackResourceClick('docs', true)
openExternalLink(
@@ -257,7 +257,7 @@ const moreItems = computed<MenuItem[]>(() => {
key: 'dev-tools',
type: 'item',
label: t('helpCenter.openDevTools'),
visible: isElectron(),
visible: isDesktop,
action: () => {
openDevTools()
emit('close')
@@ -266,13 +266,13 @@ const moreItems = computed<MenuItem[]>(() => {
{
key: 'divider-1',
type: 'divider',
visible: isElectron()
visible: isDesktop
},
{
key: 'reinstall',
type: 'item',
label: t('helpCenter.reinstall'),
visible: isElectron(),
visible: isDesktop,
action: () => {
onReinstall()
emit('close')
@@ -374,7 +374,7 @@ const menuItems = computed<MenuItem[]>(() => {
})
}
// Update ComfyUI - only for non-desktop, non-cloud with new manager UI
if (!isElectron() && !isCloud && isNewManagerUI.value) {
if (!isDesktop && !isCloud && isNewManagerUI.value) {
items.push({
key: 'update-comfyui',
type: 'item',
@@ -551,13 +551,13 @@ const onSubmenuLeave = (): void => {
}
const openDevTools = (): void => {
if (isElectron()) {
if (isDesktop) {
electronAPI().openDevTools()
}
}
const onReinstall = (): void => {
if (isElectron()) {
if (isDesktop) {
void electronAPI().reinstall()
}
}

View File

@@ -35,7 +35,7 @@
</div>
</template>
<template #body>
<ElectronDownloadItems v-if="isElectron()" />
<ElectronDownloadItems v-if="isDesktop" />
<Divider type="dashed" class="m-2" />
<TreeExplorer
@@ -69,7 +69,7 @@ import type { ComfyModelDef, ModelFolder } from '@/stores/modelStore'
import { ResourceState, useModelStore } from '@/stores/modelStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
import type { TreeExplorerNode, TreeNode } from '@/types/treeExplorerTypes'
import { isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { buildTree } from '@/utils/treeUtil'
const modelStore = useModelStore()

View File

@@ -115,7 +115,7 @@ import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workfl
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { whileMouseDown } from '@/utils/mouseDownUtil'
import WorkflowOverflowMenu from './WorkflowOverflowMenu.vue'
@@ -148,8 +148,6 @@ const showOverflowArrows = ref(false)
const leftArrowEnabled = ref(false)
const rightArrowEnabled = ref(false)
const isDesktop = isElectron()
const workflowToOption = (workflow: ComfyWorkflow): WorkflowOption => ({
value: workflow.path,
workflow

View File

@@ -1,9 +1,9 @@
import { markRaw } from 'vue'
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
import { isDesktop } from '@/platform/distribution/types'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
return {
@@ -15,7 +15,7 @@ export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
component: markRaw(ModelLibrarySidebarTab),
type: 'vue',
iconBadge: () => {
if (isElectron()) {
if (isDesktop) {
const electronDownloadStore = useElectronDownloadStore()
if (electronDownloadStore.inProgressDownloads.length > 0) {
return electronDownloadStore.inProgressDownloads.length.toString()

View File

@@ -1,8 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockData = vi.hoisted(() => ({ isDesktop: false }))
vi.mock('@/platform/distribution/types', () => ({
get isDesktop() {
return mockData.isDesktop
}
}))
// Mock the environment utilities
vi.mock('@/utils/envUtil', () => ({
isElectron: vi.fn(),
electronAPI: vi.fn()
}))
@@ -20,14 +27,14 @@ vi.mock('@/i18n', () => ({
// Import after mocking to get the mocked versions
import { useExternalLink } from '@/composables/useExternalLink'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { electronAPI } from '@/utils/envUtil'
describe('useExternalLink', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset to default state
i18n.global.locale.value = 'en'
vi.mocked(isElectron).mockReturnValue(false)
mockData.isDesktop = false
})
describe('staticUrls', () => {
@@ -95,7 +102,7 @@ describe('useExternalLink', () => {
it('should add platform suffix when requested', () => {
i18n.global.locale.value = 'en'
vi.mocked(isElectron).mockReturnValue(true)
mockData.isDesktop = true
vi.mocked(electronAPI).mockReturnValue({
getPlatform: () => 'darwin'
} as ReturnType<typeof electronAPI>)
@@ -107,7 +114,7 @@ describe('useExternalLink', () => {
it('should add platform suffix with trailing slash', () => {
i18n.global.locale.value = 'en'
vi.mocked(isElectron).mockReturnValue(true)
mockData.isDesktop = true
vi.mocked(electronAPI).mockReturnValue({
getPlatform: () => 'win32'
} as ReturnType<typeof electronAPI>)
@@ -119,7 +126,7 @@ describe('useExternalLink', () => {
it('should combine locale and platform', () => {
i18n.global.locale.value = 'zh'
vi.mocked(isElectron).mockReturnValue(true)
mockData.isDesktop = true
vi.mocked(electronAPI).mockReturnValue({
getPlatform: () => 'darwin'
} as ReturnType<typeof electronAPI>)
@@ -136,7 +143,7 @@ describe('useExternalLink', () => {
it('should not add platform when not desktop', () => {
i18n.global.locale.value = 'en'
vi.mocked(isElectron).mockReturnValue(false)
mockData.isDesktop = false
const { buildDocsUrl } = useExternalLink()
const url = buildDocsUrl('/installation/desktop', { platform: true })

View File

@@ -1,6 +1,7 @@
import { computed } from 'vue'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { electronAPI } from '@/utils/envUtil'
import { i18n } from '@/i18n'
/**
@@ -30,7 +31,7 @@ export function useExternalLink() {
})
const platform = computed(() => {
if (!isElectron()) {
if (!isDesktop) {
return null
}

View File

@@ -8,9 +8,10 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { checkMirrorReachable } from '@/utils/electronMirrorCheck'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { electronAPI as getElectronAPI } from '@/utils/envUtil'
;(async () => {
if (!isElectron()) return
if (!isDesktop) return
const electronAPI = getElectronAPI()
const desktopAppVersion = await electronAPI.getElectronVersion()

View File

@@ -1,5 +1,3 @@
import { isElectron } from '@/utils/envUtil'
/**
* Distribution types and compile-time constants for managing
* multi-distribution builds (Desktop, Localhost, Cloud)
@@ -15,10 +13,8 @@ declare global {
/** Current distribution - replaced at compile time */
const DISTRIBUTION: Distribution = __DISTRIBUTION__
/** Distribution type checks */
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
export const isDesktop = DISTRIBUTION === 'desktop'
export const isCloud = DISTRIBUTION === 'cloud'
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
/**
* Whether this is a nightly build (from main branch).

View File

@@ -6,11 +6,11 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import type { SettingTreeNode } from '@/platform/settings/settingStore'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildTree } from '@/utils/treeUtil'
@@ -226,7 +226,7 @@ export function useSettingUI(
...(shouldShowWorkspacePanel.value ? [workspacePanel] : []),
keybindingPanel,
extensionPanel,
...(isElectron() ? [serverConfigPanel] : []),
...(isDesktop ? [serverConfigPanel] : []),
...(shouldShowPlanCreditsPanel.value && subscriptionPanel
? [subscriptionPanel]
: []),
@@ -283,7 +283,7 @@ export function useSettingUI(
translateCategory(keybindingPanel.node),
translateCategory(extensionPanel.node),
translateCategory(aboutPanel.node),
...(isElectron() ? [translateCategory(serverConfigPanel.node)] : [])
...(isDesktop ? [translateCategory(serverConfigPanel.node)] : [])
]
}),
// Custom node settings (only shown if custom nodes have registered settings)
@@ -332,7 +332,7 @@ export function useSettingUI(
keybindingPanel.node,
extensionPanel.node,
aboutPanel.node,
...(isElectron() ? [serverConfigPanel.node] : [])
...(isDesktop ? [serverConfigPanel.node] : [])
].map(translateCategory)
}
])

View File

@@ -9,7 +9,6 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useReleaseService } from '@/platform/updates/common/releaseService'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { isElectron } from '@/utils/envUtil'
import { createTestingPinia } from '@pinia/testing'
import type { SystemStats } from '@/types'
@@ -19,11 +18,14 @@ vi.mock('semver', () => ({
valid: vi.fn(() => '1.0.0')
}))
vi.mock('@/utils/envUtil', () => ({
isElectron: vi.fn(() => true)
}))
const mockData = vi.hoisted(() => ({ isDesktop: true }))
vi.mock('@/platform/distribution/types', () => ({ isCloud: false }))
vi.mock('@/platform/distribution/types', () => ({
get isDesktop() {
return mockData.isDesktop
},
isCloud: false
}))
vi.mock('@/platform/updates/common/releaseService', () => {
const getReleases = vi.fn()
@@ -675,10 +677,10 @@ describe('useReleaseStore', () => {
})
})
describe('isElectron environment checks', () => {
describe('when running in Electron (desktop)', () => {
describe('isDesktop environment checks', () => {
describe('when running on desktop', () => {
beforeEach(() => {
vi.mocked(isElectron).mockReturnValue(true)
mockData.isDesktop = true
})
it('should show toast when conditions are met', () => {
@@ -709,9 +711,9 @@ describe('useReleaseStore', () => {
})
})
describe('when NOT running in Electron (web)', () => {
describe('when NOT running on desktop (web)', () => {
beforeEach(() => {
vi.mocked(isElectron).mockReturnValue(false)
mockData.isDesktop = false
})
it('should NOT show toast even when all other conditions are met', () => {

View File

@@ -3,10 +3,9 @@ import { defineStore } from 'pinia'
import { compare, valid } from 'semver'
import { computed, ref } from 'vue'
import { isCloud } from '@/platform/distribution/types'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { isElectron } from '@/utils/envUtil'
import { stringToLocale } from '@/utils/formatUtil'
import { useReleaseService } from './releaseService'
@@ -95,7 +94,7 @@ export const useReleaseStore = defineStore('release', () => {
// Show toast if needed
const shouldShowToast = computed(() => {
// Only show on desktop version
if (!isElectron() || isCloud) {
if (!isDesktop || isCloud) {
return false
}
@@ -127,7 +126,7 @@ export const useReleaseStore = defineStore('release', () => {
// Show red-dot indicator
const shouldShowRedDot = computed(() => {
// Only show on desktop version
if (!isElectron() || isCloud) {
if (!isDesktop || isCloud) {
return false
}
@@ -172,7 +171,7 @@ export const useReleaseStore = defineStore('release', () => {
})
const shouldShowPopup = computed(() => {
if (!isElectron() && !isCloud) {
if (!isDesktop && !isCloud) {
return false
}

View File

@@ -5,9 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ReleaseNote } from '../common/releaseService'
import ReleaseNotificationToast from './ReleaseNotificationToast.vue'
interface TestWindow extends Window {
electronAPI?: Record<string, unknown>
}
const mockData = vi.hoisted(() => ({ isDesktop: false }))
const { commandExecuteMock } = vi.hoisted(() => ({
commandExecuteMock: vi.fn()
@@ -17,6 +15,14 @@ const { toastErrorHandlerMock } = vi.hoisted(() => ({
toastErrorHandlerMock: vi.fn()
}))
vi.mock('@/platform/distribution/types', () => ({
isCloud: false,
isNightly: false,
get isDesktop() {
return mockData.isDesktop
}
}))
// Mock dependencies
vi.mock('vue-i18n', () => ({
useI18n: vi.fn(() => ({
@@ -105,6 +111,7 @@ describe('ReleaseNotificationToast', () => {
beforeEach(() => {
vi.clearAllMocks()
mockData.isDesktop = false
// Reset store state
mockReleaseStore.recentRelease = null
mockReleaseStore.shouldShowToast = true // Force show for testing
@@ -183,7 +190,8 @@ describe('ReleaseNotificationToast', () => {
)
})
it('executes desktop updater flow when running in Electron', async () => {
it('executes desktop updater flow when running on desktop', async () => {
mockData.isDesktop = true
mockReleaseStore.recentRelease = {
version: '1.2.3',
content: '# Test Release'
@@ -196,7 +204,6 @@ describe('ReleaseNotificationToast', () => {
value: mockWindowOpen,
writable: true
})
;(window as TestWindow).electronAPI = {}
wrapper = mountComponent()
await wrapper.vm.handleUpdate()
@@ -206,11 +213,10 @@ describe('ReleaseNotificationToast', () => {
)
expect(mockWindowOpen).not.toHaveBeenCalled()
expect(toastErrorHandlerMock).not.toHaveBeenCalled()
delete (window as TestWindow).electronAPI
})
it('shows an error toast if the desktop updater flow fails in Electron', async () => {
it('shows an error toast if the desktop updater flow fails on desktop', async () => {
mockData.isDesktop = true
mockReleaseStore.recentRelease = {
version: '1.2.3',
content: '# Test Release'
@@ -224,15 +230,12 @@ describe('ReleaseNotificationToast', () => {
value: mockWindowOpen,
writable: true
})
;(window as TestWindow).electronAPI = {}
wrapper = mountComponent()
await wrapper.vm.handleUpdate()
expect(toastErrorHandlerMock).toHaveBeenCalledWith(error)
expect(mockWindowOpen).not.toHaveBeenCalled()
delete (window as TestWindow).electronAPI
})
it('calls handleShowChangelog when learn more link is clicked', async () => {

View File

@@ -72,7 +72,7 @@ import { useI18n } from 'vue-i18n'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useExternalLink } from '@/composables/useExternalLink'
import { useCommandStore } from '@/stores/commandStore'
import { isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { formatVersionAnchor } from '@/utils/formatUtil'
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
@@ -177,7 +177,7 @@ const handleLearnMore = () => {
}
const handleUpdate = async () => {
if (isElectron()) {
if (isDesktop) {
try {
await useCommandStore().execute('Comfy-Desktop.CheckForUpdates')
dismissToast()

View File

@@ -8,12 +8,11 @@ import {
import type { RouteLocationNormalized } from 'vue-router'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useUserStore } from '@/stores/userStore'
import { isElectron } from '@/utils/envUtil'
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
import { installPreservedQueryTracker } from '@/platform/navigation/preservedQueryTracker'
@@ -30,7 +29,7 @@ const isFileProtocol = window.location.protocol === 'file:'
* to support deployments like http://mysite.com/ComfyUI/
*/
function getBasePath(): string {
if (isElectron()) return '/'
if (isDesktop) return '/'
if (isCloud) return import.meta.env?.BASE_URL || '/'
return window.location.pathname
}
@@ -182,7 +181,7 @@ if (isCloud) {
// Handle other protected routes
if (!isLoggedIn) {
// For Electron, use dialog
if (isElectron()) {
if (isDesktop) {
const dialogService = useDialogService()
const loginSuccess = await dialogService.showSignInDialog()
return loginSuccess ? next() : next(false)
@@ -197,7 +196,7 @@ if (isCloud) {
// User is logged in - check if they need onboarding (when enabled)
// For root path, check actual user status to handle waitlisted users
if (!isElectron() && isLoggedIn && to.path === '/') {
if (!isDesktop && isLoggedIn && to.path === '/') {
if (!flags.onboardingSurveyEnabled) {
return next()
}

View File

@@ -2,9 +2,9 @@ import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useExternalLink } from '@/composables/useExternalLink'
import { isCloud } from '@/platform/distribution/types'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import type { AboutPageBadge } from '@/types/comfy'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { electronAPI } from '@/utils/envUtil'
import { formatCommitHash } from '@/utils/formatUtil'
import { useExtensionStore } from './extensionStore'
@@ -24,7 +24,7 @@ export const useAboutPanelStore = defineStore('aboutPanel', () => {
// so the python server's API doesn't have the version info.
{
label: `ComfyUI ${
isElectron()
isDesktop
? 'v' + electronAPI().getComfyUIVersion()
: formatCommitHash(coreVersion.value)
}`,

View File

@@ -3,7 +3,8 @@ import type { DownloadState } from '@comfyorg/comfyui-electron-types'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { electronAPI } from '@/utils/envUtil'
export interface ElectronDownload extends Pick<
DownloadState,
@@ -17,35 +18,34 @@ export interface ElectronDownload extends Pick<
/** Electron downloads store handler */
export const useElectronDownloadStore = defineStore('downloads', () => {
const downloads = ref<ElectronDownload[]>([])
const { DownloadManager } = electronAPI()
const DownloadManager = isDesktop ? electronAPI().DownloadManager : undefined
const findByUrl = (url: string) =>
downloads.value.find((download) => url === download.url)
const initialize = async () => {
if (isElectron()) {
const allDownloads = await DownloadManager.getAllDownloads()
if (!isDesktop || !DownloadManager) return
for (const download of allDownloads) {
downloads.value.push(download)
const allDownloads = await DownloadManager.getAllDownloads()
for (const download of allDownloads) {
downloads.value.push(download)
}
DownloadManager.onDownloadProgress((data) => {
if (!findByUrl(data.url)) {
downloads.value.push(data)
}
// ToDO: replace with ElectronDownload type
DownloadManager.onDownloadProgress((data) => {
if (!findByUrl(data.url)) {
downloads.value.push(data)
}
const download = findByUrl(data.url)
const download = findByUrl(data.url)
if (download) {
download.progress = data.progress
download.status = data.status
download.filename = data.filename
download.savePath = data.savePath
}
})
}
if (download) {
download.progress = data.progress
download.status = data.status
download.filename = data.filename
download.savePath = data.savePath
}
})
}
void initialize()
@@ -58,10 +58,10 @@ export const useElectronDownloadStore = defineStore('downloads', () => {
url: string
savePath: string
filename: string
}) => DownloadManager.startDownload(url, savePath, filename)
const pause = (url: string) => DownloadManager.pauseDownload(url)
const resume = (url: string) => DownloadManager.resumeDownload(url)
const cancel = (url: string) => DownloadManager.cancelDownload(url)
}) => DownloadManager!.startDownload(url, savePath, filename)
const pause = (url: string) => DownloadManager!.pauseDownload(url)
const resume = (url: string) => DownloadManager!.resumeDownload(url)
const cancel = (url: string) => DownloadManager!.cancelDownload(url)
return {
downloads,

View File

@@ -5,7 +5,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SystemStats } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { isElectron } from '@/utils/envUtil'
const mockData = vi.hoisted(() => ({ isDesktop: false }))
// Mock the API
vi.mock('@/scripts/api', () => ({
@@ -14,13 +15,13 @@ vi.mock('@/scripts/api', () => ({
}
}))
// Mock the envUtil
vi.mock('@/utils/envUtil', () => ({
isElectron: vi.fn()
vi.mock('@/platform/distribution/types', () => ({
get isDesktop() {
return mockData.isDesktop
},
isCloud: false
}))
vi.mock('@/platform/distribution/types', () => ({ isCloud: false }))
describe('useSystemStatsStore', () => {
let store: ReturnType<typeof useSystemStatsStore>
@@ -161,9 +162,9 @@ describe('useSystemStatsStore', () => {
expect(store.getFormFactor()).toBe('other')
})
describe('desktop environment (Electron)', () => {
describe('desktop environment', () => {
beforeEach(() => {
vi.mocked(isElectron).mockReturnValue(true)
mockData.isDesktop = true
})
it('should return "desktop-windows" for Windows desktop', () => {
@@ -239,9 +240,9 @@ describe('useSystemStatsStore', () => {
})
})
describe('git environment (non-Electron)', () => {
describe('git environment (non-desktop)', () => {
beforeEach(() => {
vi.mocked(isElectron).mockReturnValue(false)
mockData.isDesktop = false
})
it('should return "git-windows" for Windows git', () => {
@@ -319,7 +320,7 @@ describe('useSystemStatsStore', () => {
describe('case insensitive OS detection', () => {
beforeEach(() => {
vi.mocked(isElectron).mockReturnValue(false)
mockData.isDesktop = false
})
it('should handle uppercase OS names', () => {

View File

@@ -1,10 +1,9 @@
import { useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia'
import { isCloud } from '@/platform/distribution/types'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import type { SystemStats } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { isElectron } from '@/utils/envUtil'
export const useSystemStatsStore = defineStore('systemStats', () => {
const fetchSystemStatsData = async () => {
@@ -40,7 +39,6 @@ export const useSystemStatsStore = defineStore('systemStats', () => {
}
const os = systemStats.value.system.os.toLowerCase()
const isDesktop = isElectron()
if (isDesktop) {
if (os.includes('windows')) {

View File

@@ -48,8 +48,12 @@ vi.mock('@/stores/commandStore', () => ({
})
}))
vi.mock('@/utils/envUtil', () => ({
isElectron: () => false
const mockData = vi.hoisted(() => ({ isDesktop: false }))
vi.mock('@/platform/distribution/types', () => ({
get isDesktop() {
return mockData.isDesktop
}
}))
describe('useBottomPanelStore', () => {

View File

@@ -3,10 +3,10 @@ import { computed, ref } from 'vue'
import { useShortcutsTab } from '@/composables/bottomPanelTabs/useShortcutsTab'
import { isDesktop } from '@/platform/distribution/types'
import { useCommandStore } from '@/stores/commandStore'
import type { ComfyExtension } from '@/types/comfy'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
type PanelType = 'terminal' | 'shortcuts'
@@ -138,7 +138,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
const { useLogsTerminalTab, useCommandTerminalTab } =
await import('@/composables/bottomPanelTabs/useTerminalTabs')
registerBottomPanelTab(useLogsTerminalTab())
if (isElectron()) {
if (isDesktop) {
registerBottomPanelTab(useCommandTerminalTab())
}
} catch (error) {

View File

@@ -1,5 +1,7 @@
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import { isDesktop } from '@/platform/distribution/types'
/**
* Extend Window interface to include electronAPI
* Used by desktop-ui app storybook stories
@@ -9,10 +11,6 @@ export type ElectronWindow = typeof window & {
electronAPI?: ElectronAPI
}
export function isElectron() {
return 'electronAPI' in window && window.electronAPI !== undefined
}
export function electronAPI() {
return (window as ElectronWindow).electronAPI as ElectronAPI
}
@@ -22,5 +20,5 @@ export function showNativeSystemMenu() {
}
export function isNativeWindow() {
return isElectron() && !!window.navigator.windowControlsOverlay?.visible
return isDesktop && !!window.navigator.windowControlsOverlay?.visible
}

View File

@@ -19,7 +19,7 @@
<RerouteMigrationToast />
<ModelImportProgressDialog />
<ManagerProgressToast />
<UnloadWindowConfirmDialog v-if="!isElectron()" />
<UnloadWindowConfirmDialog v-if="!isDesktop" />
<MenuHamburger />
</template>
@@ -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) => {

View File

@@ -22,7 +22,8 @@
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { electronAPI, isElectron, isNativeWindow } from '@/utils/envUtil'
import { electronAPI, isNativeWindow } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
const { dark = false } = defineProps<{
dark?: boolean
@@ -40,7 +41,7 @@ const lightTheme = {
const topMenuRef = ref<HTMLDivElement | null>(null)
onMounted(async () => {
if (isElectron()) {
if (isDesktop) {
await nextTick()
electronAPI().changeTheme({