Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,12 @@ When referencing Comfy-Org repos:

Rules for agent-based coding tasks.

### Chrome DevTools MCP

When using `take_snapshot` to inspect dropdowns, listboxes, or other components with dynamic options:
- Use `verbose: true` to see the full accessibility tree including list items
- Non-verbose snapshots often omit nested options in comboboxes/listboxes

### Temporary Files

- Put planning documents under `/temp/plans/`
Expand Down
4 changes: 4 additions & 0 deletions browser_tests/tests/versionMismatchWarnings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ test.describe('Version Mismatch Warnings', () => {

test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting(
'Comfy.VersionCompatibility.DisableWarnings',
false
)
})

test('should show version mismatch warnings when installed version lower than required', async ({
Expand Down
1 change: 1 addition & 0 deletions src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Clear public interfaces
- Restrict extension access
- Clean up subscriptions
- Only expose state/actions that are used externally; keep internal state private

## General Guidelines

Expand Down
9 changes: 3 additions & 6 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

<script setup lang="ts">
import { captureException } from '@sentry/vue'
import { useEventListener } from '@vueuse/core'
import BlockUI from 'primevue/blockui'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted } from 'vue'
Expand All @@ -21,15 +20,13 @@ import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'

import { electronAPI, isElectron } from './utils/envUtil'
import { app } from '@/scripts/app'

const workspaceStore = useWorkspaceStore()
app.extensionManager = useWorkspaceStore()

const conflictDetection = useConflictDetection()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
}
useEventListener(window, 'keydown', handleKey)
useEventListener(window, 'keyup', handleKey)

const showContextMenu = (event: MouseEvent) => {
const { target } = event
Expand Down
111 changes: 64 additions & 47 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
</template>

<script setup lang="ts">
import { useEventListener, whenever } from '@vueuse/core'
import { until, useEventListener } from '@vueuse/core'
import {
computed,
nextTick,
Expand Down Expand Up @@ -129,7 +129,7 @@ import { useCopy } from '@/composables/useCopy'
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
import { usePaste } from '@/composables/usePaste'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { mergeCustomNodesI18n, t } from '@/i18n'
import { t } from '@/i18n'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
Expand All @@ -144,12 +144,15 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { UnauthorizedError, api } from '@/scripts/api'
import { UnauthorizedError } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { newUserService } from '@/services/newUserService'
import { storeToRefs } from 'pinia'

import { useBootstrapStore } from '@/stores/bootstrapStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
Expand All @@ -175,11 +178,16 @@ const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore()
const canvasStore = useCanvasStore()
const workflowStore = useWorkflowStore()
const executionStore = useExecutionStore()
const toastStore = useToastStore()
const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService()
const canvasInteractions = useCanvasInteractions()
const bootstrapStore = useBootstrapStore()
const { isI18nReady, i18nError } = storeToRefs(bootstrapStore)
const { isReady: isSettingsReady, error: settingsError } =
storeToRefs(settingStore)

const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
Expand Down Expand Up @@ -386,15 +394,6 @@ useEventListener(
{ passive: true }
)

const loadCustomNodesI18n = async () => {
try {
const i18nData = await api.getCustomNodesI18n()
mergeCustomNodesI18n(i18nData)
} catch (error) {
console.error('Failed to load custom nodes i18n', error)
}
}

const comfyAppReady = ref(false)
const workflowPersistence = useWorkflowPersistence()
const { flags } = useFeatureFlags()
Expand All @@ -404,35 +403,72 @@ useCanvasDrop(canvasRef)
useLitegraphSettings()
useNodeBadge()

onMounted(async () => {
useGlobalLitegraph()
useContextMenuTranslation()
useCopy()
usePaste()
useWorkflowAutoSave()
useVueFeatureFlags()
useGlobalLitegraph()
useContextMenuTranslation()
useCopy()
usePaste()
useWorkflowAutoSave()

comfyApp.vueAppReady = true
// Start watching for locale change after the initial value is loaded.
watch(
() => settingStore.get('Comfy.Locale'),
async (_newLocale, oldLocale) => {
if (!oldLocale) return
await Promise.all([
until(() => isSettingsReady.value || !!settingsError.value).toBe(true),
until(() => isI18nReady.value || !!i18nError.value).toBe(true)
])
if (settingsError.value || i18nError.value) {
console.warn(
'Somehow the Locale setting was changed while the settings or i18n had a setup error'
)
}
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
await useWorkflowService().reloadCurrentWorkflow()
}
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding an onCleanup here?

There's some race condition potential here, but I think the chances of it occurring are near nonexistant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, but I'm not sure what I'd be cleaning up yet.

useEventListener(
() => canvasStore.canvas?.canvas,
'litegraph:set-graph',
() => {
workflowStore.updateActiveGraph()
}
)

onMounted(async () => {
comfyApp.vueAppReady = true
workspaceStore.spinner = true
// ChangeTracker needs to be initialized before setup, as it will overwrite
// some listeners of litegraph canvas.
ChangeTracker.init()
await loadCustomNodesI18n()
try {
await settingStore.loadSettingValues()
} catch (error) {
if (error instanceof UnauthorizedError) {

await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this logic/nuance be abstracted and exported from the store? So it's used like await untilSettingsReady()? To be fair it's already quite expressive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I considered doing that for each of the lazily initialized stores instead of keeping the logic in bootstrap. I'll try it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just lacking an analogous store for i18n pieces.


if (settingsError.value) {
if (settingsError.value instanceof UnauthorizedError) {
localStorage.removeItem('Comfy.userId')
localStorage.removeItem('Comfy.userName')
window.location.reload()
} else {
throw error
return
}
throw settingsError.value
}

// Register core settings immediately after settings are ready
CORE_SETTINGS.forEach(settingStore.addSetting)

await newUserService().initializeIfNewUser(settingStore)
// Wait for both i18n and newUserService in parallel
// (newUserService only needs settings, not i18n)
await Promise.all([
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
newUserService().initializeIfNewUser(settingStore)
])
if (i18nError.value) {
console.warn(
'[GraphCanvas] Failed to load custom nodes i18n:',
i18nError.value
)
}

// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
Expand Down Expand Up @@ -487,25 +523,6 @@ onMounted(async () => {
const releaseStore = useReleaseStore()
void releaseStore.initialize()

// Start watching for locale change after the initial value is loaded.
watch(
() => settingStore.get('Comfy.Locale'),
async () => {
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
await useWorkflowService().reloadCurrentWorkflow()
}
)

whenever(
() => useCanvasStore().canvas,
(canvas) => {
useEventListener(canvas.canvas, 'litegraph:set-graph', () => {
useWorkflowStore().updateActiveGraph()
})
},
{ immediate: true }
)

emit('ready')
})

Expand Down
3 changes: 3 additions & 0 deletions src/composables/node/useNodeBadge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export const useNodeBadge = () => {
}

onMounted(() => {
if (extensionStore.isExtensionInstalled('Comfy.NodeBadge')) return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// TODO: Fix the composables and watchers being setup in onMounted
const nodePricing = useNodePricing()

watch(
Expand Down
15 changes: 10 additions & 5 deletions src/composables/useCoreCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,25 +167,30 @@ describe('useCoreCommands', () => {
return {
get: vi.fn().mockReturnValue(getReturnValue),
addSetting: vi.fn(),
loadSettingValues: vi.fn(),
load: vi.fn(),
set: vi.fn(),
exists: vi.fn(),
getDefaultValue: vi.fn(),
isReady: true,
isLoading: false,
error: undefined,
settingValues: {},
settingsById: {},
$id: 'setting',
$state: {
settingValues: {},
settingsById: {}
settingsById: {},
isReady: true,
isLoading: false,
error: undefined
},
$patch: vi.fn(),
$reset: vi.fn(),
$subscribe: vi.fn(),
$onAction: vi.fn(),
$dispose: vi.fn(),
_customProperties: new Set(),
_p: {}
} as ReturnType<typeof useSettingStore>
_customProperties: new Set()
} satisfies ReturnType<typeof useSettingStore>
}

beforeEach(() => {
Expand Down
3 changes: 1 addition & 2 deletions src/composables/useCoreCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,9 @@ import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelector
import { useMaskEditorStore } from '@/stores/maskEditorStore'
import { useDialogStore } from '@/stores/dialogStore'

const { isActiveSubscription, showSubscriptionDialog } = useSubscription()

const moveSelectedNodesVersionAdded = '1.22.2'
export function useCoreCommands(): ComfyCommand[] {
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
const dialogService = useDialogService()
Expand Down
5 changes: 5 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { VueFire, VueFireAuth } from 'vuefire'
import { getFirebaseConfig } from '@/config/firebase'
import '@/lib/litegraph/public/css/litegraph.css'
import router from '@/router'
import { useBootstrapStore } from '@/stores/bootstrapStore'

import App from './App.vue'
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
Expand Down Expand Up @@ -43,6 +44,7 @@ const firebaseApp = initializeApp(getFirebaseConfig())

const app = createApp(App)
const pinia = createPinia()

Sentry.init({
app,
dsn: __SENTRY_DSN__,
Expand Down Expand Up @@ -88,4 +90,7 @@ app
modules: [VueFireAuth()]
})

const bootstrapStore = useBootstrapStore(pinia)
void bootstrapStore.startStoreBootstrap()

app.mount('#vue-app')
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ function useSubscriptionInternal() {
if (loggedIn) {
try {
await fetchSubscriptionStatus()
} catch (error) {
// Network errors are expected during navigation/component unmount
// and when offline - log for debugging but don't surface to user
console.error('Failed to fetch subscription status:', error)
} finally {
isInitialized.value = true
}
Expand Down
Loading