Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Brown
c805883743 Merge branch 'main' into fix/hero-animation-load-flash 2026-06-10 12:33:04 -07:00
Michael B
f5f9ab5edb fix(website): unblock nav and remove logo flash on home page load
Hero animation was blocking nav clicks and pop-flashing on refresh.

- Dynamically import three + SVGLoader so they leave the home page's
  initial JS bundle (706KB chunk now lazy-loads).
- Defer Three.js setup behind requestIdleCallback (setTimeout fallback)
  so SiteNav and other client:load islands hydrate to a clickable state
  before WebGL init runs.
- Yield to the main thread between expensive sync steps (shape parse,
  ShapeGeometry, ExtrudeGeometry, mesh assembly) so no single task locks
  the nav.
- Hold the static fallback visible until the first animate() frame has
  actually rendered, eliminating the empty-canvas window between hiding
  the fallback and the WebGL scene being drawable.
- Resize the static fallback to w-full so its visible logo silhouette
  matches the 3D render, making the static->3D handoff a near-identity
  swap instead of a small-to-big pop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 11:56:08 -04:00
24 changed files with 174 additions and 275 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,23 +1,23 @@
{
"name": "Comfy",
"short_name": "Comfy",
"id": "/",
"start_url": "/",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
"purpose": "any"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
"purpose": "any"
}
],
"theme_color": "#211927",
"background_color": "#211927",
"display": "standalone",
"id": "/",
"start_url": "/"
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -25,19 +25,19 @@ const { loaded: logoLoaded } = useHeroLogo(logoContainer)
v-show="!logoLoaded"
src="https://media.comfy.org/website/homepage/hero-logo-seq/Logo00.webp"
alt="Comfy logo"
class="w-3/5"
class="w-full"
/>
</div>
<div class="flex-1 px-6 py-12 lg:px-16">
<h1
class="text-primary-comfy-canvas text-4xl font-light whitespace-pre-line lg:text-6xl"
class="text-4xl font-light whitespace-pre-line text-primary-comfy-canvas lg:text-6xl"
>
{{ t('hero.title', locale) }}
</h1>
<p
class="text-primary-comfy-canvas mt-8 max-w-lg text-sm/relaxed lg:text-base"
class="mt-8 max-w-lg text-sm/relaxed text-primary-comfy-canvas lg:text-base"
>
{{ t('hero.subtitle', locale) }}
</p>

View File

@@ -8,7 +8,6 @@ import {
useDownloadUrl
} from '../../../composables/useDownloadUrl'
import { t } from '../../../i18n/translations'
import { captureDownloadClick } from '../../../scripts/posthog'
import BrandButton from '../../common/BrandButton.vue'
const { locale = 'en', class: customClass = '' } = defineProps<{
@@ -70,7 +69,6 @@ const buttons = computed<ButtonSpec[]>(() => {
size="lg"
:class="customClass"
:aria-label="btn.ariaLabel"
@click="captureDownloadClick(btn.key)"
>
<span class="inline-flex items-center gap-2">
<img

View File

@@ -1,8 +1,7 @@
import type { Ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import * as THREE from 'three'
import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'
import type * as THREE from 'three'
import { prefersReducedMotion } from './useReducedMotion'
@@ -44,34 +43,12 @@ function buildImageUrls(): string[] {
})
}
function parseShapes(): THREE.Shape[] {
const loader = new SVGLoader()
const svgData = loader.parse(SVG_MARKUP)
const shapes: THREE.Shape[] = []
svgData.paths.forEach((path) => {
shapes.push(...SVGLoader.createShapes(path))
})
return shapes
}
function loadTextures(urls: string[]): Promise<THREE.Texture[]> {
return Promise.all(
urls.map(
(url) =>
new Promise<THREE.Texture | null>((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const tex = new THREE.Texture(img)
tex.needsUpdate = true
tex.colorSpace = THREE.SRGBColorSpace
resolve(tex)
}
img.onerror = () => resolve(null)
img.src = url
})
)
).then((results) => results.filter((t): t is THREE.Texture => t !== null))
function yieldToMain(): Promise<void> {
const sched = (
window as unknown as { scheduler?: { yield?: () => Promise<void> } }
).scheduler
if (sched && typeof sched.yield === 'function') return sched.yield()
return new Promise((resolve) => setTimeout(resolve, 0))
}
export function useHeroLogo(
@@ -81,12 +58,70 @@ export function useHeroLogo(
const cfg = { ...DEFAULTS, ...config }
const loaded = ref(false)
let cleanup: (() => void) | undefined
let unmounted = false
let idleHandle: number | undefined
let timeoutHandle: number | undefined
onMounted(async () => {
const cancelScheduled = () => {
if (
idleHandle !== undefined &&
typeof window !== 'undefined' &&
typeof window.cancelIdleCallback === 'function'
) {
window.cancelIdleCallback(idleHandle)
}
idleHandle = undefined
if (timeoutHandle !== undefined) {
window.clearTimeout(timeoutHandle)
timeoutHandle = undefined
}
}
const setup = async () => {
try {
if (unmounted) return
const container = containerRef.value
if (!container || prefersReducedMotion()) return
const [THREE, svgLoaderMod] = await Promise.all([
import('three'),
import('three/addons/loaders/SVGLoader.js')
])
if (unmounted) return
const parseShapes = (): THREE.Shape[] => {
const { SVGLoader } = svgLoaderMod
const loader = new SVGLoader()
const svgData = loader.parse(SVG_MARKUP)
const shapes: THREE.Shape[] = []
svgData.paths.forEach((path) => {
shapes.push(...SVGLoader.createShapes(path))
})
return shapes
}
const loadTextures = (urls: string[]): Promise<THREE.Texture[]> => {
return Promise.all(
urls.map(
(url) =>
new Promise<THREE.Texture | null>((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const tex = new THREE.Texture(img)
tex.needsUpdate = true
tex.colorSpace = THREE.SRGBColorSpace
resolve(tex)
}
img.onerror = () => resolve(null)
img.src = url
})
)
).then((results) =>
results.filter((t): t is THREE.Texture => t !== null)
)
}
const { width, height } = container.getBoundingClientRect()
const renderer = new THREE.WebGLRenderer({
@@ -125,6 +160,9 @@ export function useHeroLogo(
)
camera.position.z = cfg.zoom
await yieldToMain()
if (disposed) return
// SVG shape
const shapes = parseShapes()
const tempGeo = new THREE.ShapeGeometry(shapes)
@@ -135,15 +173,15 @@ export function useHeroLogo(
const scaleFactor = 3 / (bb.max.y - bb.min.y)
tempGeo.dispose()
await yieldToMain()
if (disposed) return
// Image sequence textures — load first frame eagerly, rest lazily
const urls = buildImageUrls()
const textures = await loadTextures(urls.slice(0, 1))
if (disposed) return
renderer.domElement.style.opacity = '1'
loaded.value = true
loadTextures(urls.slice(1)).then((rest) => {
void loadTextures(urls.slice(1)).then((rest) => {
if (!disposed) textures.push(...rest)
})
@@ -167,6 +205,9 @@ export function useHeroLogo(
bgPlane.scale.set(cfg.bgScale, cfg.bgScale, 1)
scene.add(bgPlane)
await yieldToMain()
if (disposed) return
// Logo group
const group = new THREE.Group()
scene.add(group)
@@ -189,6 +230,9 @@ export function useHeroLogo(
logoMesh.renderOrder = 2
group.add(logoMesh)
await yieldToMain()
if (disposed) return
// Extrusion stencil mask
const extrudeGeo = new THREE.ExtrudeGeometry(shapes, {
depth,
@@ -212,6 +256,9 @@ export function useHeroLogo(
extrudeMesh.renderOrder = 0
group.add(extrudeMesh)
await yieldToMain()
if (disposed) return
// Interaction
let isDragging = false
let previousX = 0
@@ -261,6 +308,7 @@ export function useHeroLogo(
window.addEventListener('resize', onResize)
const clock = new THREE.Clock()
let firstFrameRendered = false
function animate() {
if (disposed) return
@@ -294,6 +342,12 @@ export function useHeroLogo(
}
renderer.render(scene, camera)
if (!firstFrameRendered) {
firstFrameRendered = true
renderer.domElement.style.opacity = '1'
loaded.value = true
}
}
animate()
@@ -318,9 +372,29 @@ export function useHeroLogo(
console.error('[useHeroLogo] initialization failed:', err)
cleanup?.()
}
}
onMounted(() => {
if (typeof window === 'undefined') return
if (typeof window.requestIdleCallback === 'function') {
idleHandle = window.requestIdleCallback(
() => {
idleHandle = undefined
void setup()
},
{ timeout: 2000 }
)
} else {
timeoutHandle = window.setTimeout(() => {
timeoutHandle = undefined
void setup()
}, 200)
}
})
onUnmounted(() => {
unmounted = true
cancelScheduled()
cleanup?.()
})

View File

@@ -73,7 +73,7 @@ const websiteJsonLd = {
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" sizes="48x48" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#211927" />

View File

@@ -53,28 +53,3 @@ describe('initPostHog', () => {
expect(result.$set_once).toHaveProperty('plan', 'free')
})
})
describe('captureDownloadClick', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.resetModules()
})
it('captures the download event with the platform', async () => {
const { initPostHog, captureDownloadClick } = await import('./posthog')
initPostHog()
captureDownloadClick('mac')
expect(hoisted.mockCapture).toHaveBeenCalledWith(
'website:download_button_clicked',
{ platform: 'mac' }
)
})
it('does not capture before PostHog is initialized', async () => {
const { captureDownloadClick } = await import('./posthog')
captureDownloadClick('windows')
expect(hoisted.mockCapture).not.toHaveBeenCalled()
})
})

View File

@@ -38,12 +38,3 @@ export function capturePageview() {
console.error('PostHog pageview capture failed', error)
}
}
export function captureDownloadClick(platform: string) {
if (!initialized) return
try {
posthog.capture('website:download_button_clicked', { platform })
} catch (error) {
console.error('PostHog download click capture failed', error)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -33,6 +33,7 @@ const {
items,
gridStyle,
bufferRows = 1,
scrollThrottle = 64,
resizeDebounce = 64,
defaultItemHeight = 200,
defaultItemWidth = 200,
@@ -41,6 +42,7 @@ const {
items: (T & { key: string })[]
gridStyle: CSSProperties
bufferRows?: number
scrollThrottle?: number
resizeDebounce?: number
defaultItemHeight?: number
defaultItemWidth?: number
@@ -59,6 +61,7 @@ const itemWidth = ref(defaultItemWidth)
const container = ref<HTMLElement | null>(null)
const { width, height } = useElementSize(container)
const { y: scrollY } = useScroll(container, {
throttle: scrollThrottle,
eventListenerOptions: { passive: true }
})

View File

@@ -22,7 +22,7 @@ const zAsset = z.object({
})
const zAssetResponse = zListAssetsResponse
.pick({ total: true, has_more: true, next_cursor: true })
.pick({ total: true, has_more: true })
.extend({
assets: z.array(zAsset)
})

View File

@@ -53,7 +53,6 @@ const fetchApiMock = vi.mocked(api.fetchApi)
type AssetListResponseOptions = {
hasMore?: AssetResponse['has_more']
total?: AssetResponse['total']
nextCursor?: AssetResponse['next_cursor']
}
function buildResponse(
@@ -69,18 +68,9 @@ function buildResponse(
function buildAssetListResponse(
assets: AssetItem[],
{
hasMore = false,
total = assets.length,
nextCursor
}: AssetListResponseOptions = {}
{ hasMore = false, total = assets.length }: AssetListResponseOptions = {}
): Response {
return buildResponse({
assets,
total,
has_more: hasMore,
...(nextCursor === undefined ? {} : { next_cursor: nextCursor })
})
return buildResponse({ assets, total, has_more: hasMore })
}
function validAsset(overrides: Partial<AssetItem> = {}): AssetItem {
@@ -522,7 +512,7 @@ describe(assetService.getAllAssetsByTag, () => {
vi.clearAllMocks()
})
it('walks pages by keyset cursor with include_public=true', async () => {
it('paginates tagged asset requests with include_public=true', async () => {
fetchApiMock
.mockResolvedValueOnce(
buildAssetListResponse(
@@ -530,7 +520,7 @@ describe(assetService.getAllAssetsByTag, () => {
validAsset({ id: 'a', tags: ['input'] }),
validAsset({ id: 'b', tags: ['input'] })
],
{ hasMore: true, nextCursor: 'cursor-page-2' }
{ hasMore: true }
)
)
.mockResolvedValueOnce(
@@ -548,8 +538,6 @@ describe(assetService.getAllAssetsByTag, () => {
expect(firstParams.get('include_public')).toBe('true')
expect(firstParams.get('exclude_tags')).toBe(MISSING_TAG)
expect(firstParams.get('limit')).toBe('2')
// First page carries neither a cursor nor an offset.
expect(firstParams.has('after')).toBe(false)
expect(firstParams.has('offset')).toBe(false)
const secondUrl = fetchApiMock.mock.calls[1]?.[0] as string
@@ -557,9 +545,7 @@ describe(assetService.getAllAssetsByTag, () => {
expect(secondParams.get('include_public')).toBe('true')
expect(secondParams.get('exclude_tags')).toBe(MISSING_TAG)
expect(secondParams.get('limit')).toBe('2')
// Subsequent pages resume from the prior response's next_cursor, never offset.
expect(secondParams.get('after')).toBe('cursor-page-2')
expect(secondParams.has('offset')).toBe(false)
expect(secondParams.get('offset')).toBe('2')
})
it('honors has_more when walking tagged asset pages', async () => {
@@ -570,7 +556,7 @@ describe(assetService.getAllAssetsByTag, () => {
validAsset({ id: 'first', tags: ['input'] }),
validAsset({ id: 'second', tags: ['input'] })
],
{ hasMore: true, nextCursor: 'cursor-next' }
{ hasMore: true }
)
)
.mockResolvedValueOnce(
@@ -591,45 +577,7 @@ describe(assetService.getAllAssetsByTag, () => {
throw new Error('Expected a second asset request URL')
}
const secondParams = new URL(secondUrl, 'http://localhost').searchParams
expect(secondParams.get('after')).toBe('cursor-next')
})
it('stops walking when next_cursor is absent even if has_more is true', async () => {
fetchApiMock.mockResolvedValueOnce(
buildAssetListResponse([validAsset({ id: 'only', tags: ['input'] })], {
hasMore: true
})
)
const assets = await assetService.getAllAssetsByTag('input', true, {
limit: 2
})
expect(assets.map((a) => a.id)).toEqual(['only'])
expect(fetchApiMock).toHaveBeenCalledOnce()
})
it('stops walking when the server returns a non-advancing cursor', async () => {
fetchApiMock
.mockResolvedValueOnce(
buildAssetListResponse([validAsset({ id: 'a', tags: ['input'] })], {
hasMore: true,
nextCursor: 'stuck'
})
)
.mockResolvedValueOnce(
buildAssetListResponse([validAsset({ id: 'b', tags: ['input'] })], {
hasMore: true,
nextCursor: 'stuck'
})
)
const assets = await assetService.getAllAssetsByTag('input', true, {
limit: 1
})
expect(assets.map((a) => a.id)).toEqual(['a', 'b'])
expect(fetchApiMock).toHaveBeenCalledTimes(2)
expect(secondParams.get('offset')).toBe('2')
})
it.for([
@@ -688,7 +636,7 @@ describe(assetService.getAllAssetsByTag, () => {
validAsset({ id: 'a', tags: ['input'] }),
validAsset({ id: 'b', tags: ['input'] })
],
{ hasMore: true, nextCursor: 'cursor-page-2' }
{ hasMore: true }
)
})

View File

@@ -31,11 +31,6 @@ export interface PaginationOptions {
}
interface AssetPaginationOptions extends PaginationOptions {
/**
* Opaque keyset cursor from a prior response's `next_cursor`. When set, the
* server resumes after that cursor and `offset` is ignored.
*/
after?: string
signal?: AbortSignal
}
@@ -43,7 +38,6 @@ interface AssetRequestOptions extends PaginationOptions {
includeTags: string[]
excludeTags?: string[]
includePublic?: boolean
after?: string
signal?: AbortSignal
}
@@ -292,7 +286,6 @@ function createAssetService() {
excludeTags = DEFAULT_EXCLUDED_ASSET_TAGS,
limit = DEFAULT_LIMIT,
offset,
after,
includePublic,
signal
} = options
@@ -306,11 +299,7 @@ function createAssetService() {
if (normalizedExcludeTags.length > 0) {
queryParams.set('exclude_tags', normalizedExcludeTags.join(','))
}
// `after` (keyset cursor) takes precedence over `offset`; the server ignores
// `offset` when a cursor is supplied, so we avoid sending a redundant param.
if (after) {
queryParams.set('after', after)
} else if (offset !== undefined && offset > 0) {
if (offset !== undefined && offset > 0) {
queryParams.set('offset', offset.toString())
}
if (includePublic !== undefined) {
@@ -492,17 +481,11 @@ function createAssetService() {
async function getAssetsByTag(
tag: string,
includePublic: boolean = true,
{
limit = DEFAULT_LIMIT,
offset = 0,
after,
signal
}: AssetPaginationOptions = {}
{ limit = DEFAULT_LIMIT, offset = 0, signal }: AssetPaginationOptions = {}
): Promise<AssetItem[]> {
const data = await getAssetsPageByTag(tag, includePublic, {
limit,
offset,
after,
signal
})
@@ -515,27 +498,17 @@ function createAssetService() {
async function getAssetsPageByTag(
tag: string,
includePublic: boolean = true,
{
limit = DEFAULT_LIMIT,
offset = 0,
after,
signal
}: AssetPaginationOptions = {}
{ limit = DEFAULT_LIMIT, offset = 0, signal }: AssetPaginationOptions = {}
): Promise<AssetResponse> {
return await handleAssetRequest(
{ includeTags: [tag], limit, offset, after, includePublic, signal },
{ includeTags: [tag], limit, offset, includePublic, signal },
`assets for tag ${tag}`
)
}
/**
* Gets every asset for a tag by walking paginated asset API responses.
*
* Uses keyset (cursor) pagination: each page is fetched with the prior
* response's `next_cursor`, which is stable under concurrent inserts/deletes
* and avoids the duplicate/skip drift that offset paging exhibits when the
* underlying set changes mid-walk. Falls back to terminating on `has_more`
* when the server omits `next_cursor`.
* Pagination follows the required server-provided `has_more` flag.
*
* @param tag - The tag to filter by (e.g., 'models', 'input')
* @param includePublic - Whether to include public assets (default: true)
@@ -547,21 +520,18 @@ function createAssetService() {
async function getAllAssetsByTag(
tag: string,
includePublic: boolean = true,
{
limit = DEFAULT_LIMIT,
signal
}: Pick<AssetPaginationOptions, 'limit' | 'signal'> = {}
{ limit = DEFAULT_LIMIT, signal }: AssetPaginationOptions = {}
): Promise<AssetItem[]> {
const assets: AssetItem[] = []
const pageSize = limit > 0 ? limit : DEFAULT_LIMIT
let after: string | undefined
let offset = 0
while (true) {
if (signal?.aborted) throw createAbortError()
const data = await getAssetsPageByTag(tag, includePublic, {
limit: pageSize,
after,
offset,
signal
})
const batch = data.assets
@@ -571,12 +541,11 @@ function createAssetService() {
assets.push(...batch)
// A server that returns a non-advancing cursor would loop forever.
if (!data.has_more || !data.next_cursor || data.next_cursor === after) {
if (!data.has_more) {
return assets
}
after = data.next_cursor
offset += batch.length
}
}

View File

@@ -53,8 +53,7 @@ describe('useReleaseService', () => {
project: 'comfyui',
current_version: '1.0.0'
},
signal: undefined,
headers: undefined
signal: undefined
})
expect(result).toEqual(mockReleases)
@@ -77,8 +76,7 @@ describe('useReleaseService', () => {
current_version: '1.0.0',
form_factor: 'desktop-windows'
},
signal: undefined,
headers: undefined
signal: undefined
})
expect(result).toEqual(mockReleases)
@@ -88,30 +86,11 @@ describe('useReleaseService', () => {
const abortController = new AbortController()
mockAxiosInstance.get.mockResolvedValue({ data: mockReleases })
await service.getReleases(
{ project: 'comfyui' },
{ signal: abortController.signal }
)
await service.getReleases({ project: 'comfyui' }, abortController.signal)
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/releases', {
params: { project: 'comfyui' },
signal: abortController.signal,
headers: undefined
})
})
it('should send Comfy-Env header when deployEnvironment is provided', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: mockReleases })
await service.getReleases(
{ project: 'comfyui' },
{ deployEnvironment: 'local-desktop' }
)
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/releases', {
params: { project: 'comfyui' },
signal: undefined,
headers: { 'Comfy-Env': 'local-desktop' }
signal: abortController.signal
})
})

View File

@@ -98,9 +98,8 @@ export const useReleaseService = () => {
// Fetch release notes from API
const getReleases = async (
params: GetReleasesParams,
options: { signal?: AbortSignal; deployEnvironment?: string } = {}
signal?: AbortSignal
): Promise<ReleaseNote[] | null> => {
const { signal, deployEnvironment } = options
const endpoint = '/releases'
const errorContext = 'Failed to get releases'
const routeSpecificErrors = {
@@ -111,10 +110,7 @@ export const useReleaseService = () => {
() =>
releaseApiClient.get<ReleaseNote[]>(endpoint, {
params,
signal,
headers: deployEnvironment
? { 'Comfy-Env': deployEnvironment }
: undefined
signal
}),
errorContext,
routeSpecificErrors

View File

@@ -228,15 +228,12 @@ describe('useReleaseStore', () => {
await store.initialize()
expect(releaseService.getReleases).toHaveBeenCalledWith(
{
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'git-windows',
locale: 'en'
},
{ deployEnvironment: undefined }
)
expect(releaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'git-windows',
locale: 'en'
})
})
})
@@ -303,15 +300,12 @@ describe('useReleaseStore', () => {
await store.initialize()
expect(releaseService.getReleases).toHaveBeenCalledWith(
{
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'git-windows',
locale: 'en'
},
{ deployEnvironment: undefined }
)
expect(releaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'git-windows',
locale: 'en'
})
expect(store.releases).toEqual([mockRelease])
})
@@ -324,30 +318,12 @@ describe('useReleaseStore', () => {
await store.initialize()
expect(releaseService.getReleases).toHaveBeenCalledWith(
{
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'desktop-mac',
locale: 'en'
},
{ deployEnvironment: undefined }
)
})
it('should pass deploy_environment from system stats', async () => {
const store = useReleaseStore()
const releaseService = useReleaseService()
const systemStatsStore = useSystemStatsStore()
systemStatsStore.systemStats!.system.deploy_environment = 'local-desktop'
vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease])
await store.initialize()
expect(releaseService.getReleases).toHaveBeenCalledWith(
expect.anything(),
{ deployEnvironment: 'local-desktop' }
)
expect(releaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui',
current_version: '1.0.0',
form_factor: 'desktop-mac',
locale: 'en'
})
})
it('should skip fetching when --disable-api-nodes is present', async () => {

View File

@@ -266,18 +266,12 @@ export const useReleaseStore = defineStore('release', () => {
await until(systemStatsStore.isInitialized)
}
const fetchedReleases = await releaseService.getReleases(
{
project: isCloud ? 'cloud' : 'comfyui',
current_version: currentVersion.value,
form_factor: systemStatsStore.getFormFactor(),
locale: stringToLocale(locale.value)
},
{
deployEnvironment:
systemStatsStore.systemStats?.system?.deploy_environment
}
)
const fetchedReleases = await releaseService.getReleases({
project: isCloud ? 'cloud' : 'comfyui',
current_version: currentVersion.value,
form_factor: systemStatsStore.getFormFactor(),
locale: stringToLocale(locale.value)
})
if (fetchedReleases !== null) {
releases.value = fetchedReleases

View File

@@ -252,7 +252,6 @@ const zSystemStats = z.object({
python_version: z.string(),
embedded_python: z.boolean(),
comfyui_version: z.string(),
deploy_environment: z.string().optional(),
pytorch_version: z.string(),
required_frontend_version: z.string().optional(),
argv: z.array(z.string()),

View File

@@ -108,11 +108,6 @@ interface QueuePromptRequestBody {
* ```
*/
api_key_comfy_org?: string
/**
* Identifies the client submitting the prompt. Forwarded by the backend
* to API nodes' upstream requests via the Comfy-Usage-Source header.
*/
comfy_usage_source?: string
/**
* Override the preview method for this prompt execution.
* 'default' uses the server's CLI setting.
@@ -872,7 +867,6 @@ export class ComfyApi extends EventTarget {
extra_data: {
auth_token_comfy_org: this.authToken,
api_key_comfy_org: this.apiKey,
comfy_usage_source: 'comfyui-frontend',
extra_pnginfo: { workflow },
...(options?.previewMethod &&
options.previewMethod !== 'default' && {