Compare commits

..

9 Commits

Author SHA1 Message Date
Connor Byrne
7256ae081f fix: add explicit type parameters to useTemplateRef in LinearView
Add explicit type parameters to fix TS7022 errors during declaration
file generation where refs were implicitly typed as 'any'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-19 15:58:41 -07:00
pythongosssss
c8b5589768 feat: remove subscript prompt dialog on load (#13012)
## Summary

The first thing a user sees when they log in is the paywall prompt which
is a poor UX. This removes the dialog that pops up as the run buttons
are already gated to prompt to subscribe.

## Changes

- **What**: 
- Remove extension & invocation
2026-06-19 21:31:17 +00:00
Christian Byrne
b4b95980da fix: publish core/* releases immediately to create tags (#12995)
## Summary
- Fix release pipeline where core/* releases hang indefinitely waiting
for tags

## Problem
Draft releases don't create git tags, but `publish-pypi` workflow waits
for the tag to exist. For `core/*` branches, releases were always
created as drafts, causing the pipeline to wait forever.

## Solution
Only use draft releases for prereleases (alpha/beta/rc). Publish all
stable releases (main and core/*) immediately so tags are created.

## Test plan
- [x] Verify workflow change logic is correct
- [ ] Test on next core/1.45 release

Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-19 14:28:49 -07:00
Deep Mehta
8c0af36c4a fix: re-disable ultralytics asset-picker registration (#13020)
## Summary

Re-comment the `ultralytics/bbox` and `ultralytics/segm` entries in
`MODEL_NODE_MAPPINGS` so `UltralyticsDetectorProvider` falls back to the
static combo from `/api/object_info`, restoring the working post-#12075
behavior. Companion to
[Comfy-Org/cloud#4395](https://github.com/Comfy-Org/cloud/pull/4395)
(ONNX dummy-placeholder fix).

## Why

PR #12151 (2026-05-12) re-added these entries before BE-689 (the
cloud-side asset-ingestion fix) landed, reintroducing both halves of the
regression that #12075 originally cited:

1. **Tag lookup mismatch.** Cloud stores model tags as combined values
like `ultralytics/bbox`, but the asset query splits them into `(models,
ultralytics)` with exact-match filtering, so the picker returns no
results.
2. **Submitted value mismatch.** Picker returns basenames like
`face_yolov8m.pt`, but the ingest service validates against the
subdirectory-prefixed form (`bbox/face_yolov8m.pt`) that the static
combo path produces. The combo rejects the basename with `Value not in
list` (the exact error users hit in BE-1116).

Until BE-689 lands both halves of the cloud fix, the asset-picker
registration cannot ship without breaking the node.

## Changes

- **What**: comment out the two ultralytics entries in
`src/platform/assets/mappings/modelNodeMappings.ts` with an inline note
pointing at BE-689 and PR #12075 / #12151 for context.
- **Breaking**: none. The node returns to the same static-combo behavior
it had between #12075 and #12151.
- **Dependencies**: none.

## Review Focus

- The existing assertion in `src/stores/modelToNodeStore.test.ts:278`
already expects `ultralytics`, `ultralytics/bbox`, and
`ultralytics/segm` to NOT resolve to a default provider — that test was
added with #12075 and was left in place when #12151 re-added the
mappings. After this change the mappings file is consistent with the
test again. 55/55 in `modelToNodeStore.test.ts` pass locally.
- This is a targeted revert of the ultralytics half of #12151. The other
entries that PR added (`background_removal`, `frame_interpolation`,
`film`) are unaffected.

## Companion PRs

- Comfy-Org/cloud#4395 — fixes the `ONNXDetectorProvider` half of the
FaceDetailer-broken report (BE-1116) by repairing the dummy-placeholder
mechanism so the static combo carries the right model list. ONNX uses
the static-combo path (no `MODEL_NODE_MAPPINGS` entry), so the
cloud-side fix is sufficient there.

<!-- Fixes BE-689 -->
2026-06-19 20:59:56 +00:00
jaeone94
78a8d6f8fc test: stabilize node help locale e2e (#12998)
## Summary

Stabilizes the locale-specific Node Help E2E by setting the locale
through the existing Playwright settings fixture before app bootstrap
instead of racing a workflow reload pulse.

## Changes

- **What**: Removed the brittle in-page locale mutation helper and uses
`test.use({ initialSettings: { 'Comfy.Locale': 'ja' } })` for the
locale-specific documentation case.
- **What**: Keeps the Japanese and English doc routes local to the test,
then verifies the Japanese help content after loading the default
workflow.
- **Dependencies**: None.

## Review Focus

Please focus on whether the E2E now waits on the correct setup boundary.
The previous helper watched `ChangeTracker.isLoadingGraph` after
changing `Comfy.Locale`; once unrelated workflow-load work became
faster, that loading pulse could complete before the helper observed it.
Pre-boot `initialSettings` avoids that timing dependency and uses
existing test infrastructure.

Verification:

- `pnpm exec oxfmt --check browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5176 pnpm
exec playwright test browser_tests/tests/nodeHelp.spec.ts --grep "Should
handle locale-specific documentation" --project=chromium
--repeat-each=10`

## Screenshots (if applicable)

N/A
2026-06-19 19:50:04 +00:00
jaeone94
cc41e3e1ac test: stabilize cloud template filtering e2e (#12999)
## Summary

Stabilizes the cloud template filtering E2E by making the test own both
startup asset API responses and the complete template universe it
asserts against.

## Changes

- **What**: Uses the existing `createCloudAssetsFixture([])` fixture so
startup `/api/assets` calls do not create an unrelated error toast that
can intercept the Clear Filters click.
- **What**: Extends `TemplateHelper.mockIndex()` to also mock
`/api/workflow_templates` as an empty custom-template map, so tests that
configure a core template index do not accidentally include custom
templates from the local backend.
- **Dependencies**: None.

## Review Focus

Please focus on fixture ownership. This spec asserts exact template
counts, so `templateApi.mockIndex()` should isolate the core template
index and the custom workflow-template endpoint together. The asset
fixture change is intentionally scoped to this cloud spec and reuses
existing infrastructure instead of dismissing arbitrary toasts or
forcing clicks.

Verification:

- `pnpm exec oxfmt --check
browser_tests/tests/templateFilteringCount.spec.ts
browser_tests/fixtures/helpers/TemplateHelper.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/templateFilteringCount.spec.ts
browser_tests/fixtures/helpers/TemplateHelper.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test
browser_tests/tests/templateFilteringCount.spec.ts --grep "clear filters
button resets" --project=cloud --repeat-each=5`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test
browser_tests/tests/templateFilteringCount.spec.ts --project=cloud`

## Screenshots (if applicable)

N/A
2026-06-19 17:34:23 +00:00
Dante
f994673dd1 fix(test): import AppMode from utils/appMode in WorkflowHelper (#13003)
## Summary

`pnpm typecheck:browser` (`vue-tsc --project
browser_tests/tsconfig.json`) currently fails on `main`, turning
`lint-and-format` red for every PR built on it:

```
browser_tests/fixtures/helpers/WorkflowHelper.ts(5,15): error TS2459:
Module '"@/composables/useAppMode"' declares 'AppMode' locally, but it is not exported.
```

## Cause

#12925 (`bc885f383c`, "Decouple run telemetry context from providers")
moved the `AppMode` type from `@/composables/useAppMode` to
`@/utils/appMode`. `useAppMode` now only `import type`s `AppMode` (it no
longer re-exports it), so this fixture's `import type { AppMode } from
'@/composables/useAppMode'` became invalid. The other `useAppMode`
import sites are unaffected — they import the `useAppMode` composable
(still exported), not the type.

## Fix

Import `AppMode` from its actual source, `@/utils/appMode`. One line, no
behavior change.

Verified `vue-tsc --project browser_tests/tsconfig.json` no longer
reports the `WorkflowHelper.ts` error.
2026-06-19 15:17:57 +00:00
jaeone94
444dc3fccd test: stabilize mask editor screenshot e2e (#13011)
<img width="1155" height="648" alt="스크린샷 2026-06-19 오후 11 24 27"
src="https://github.com/user-attachments/assets/01ed2607-662f-4735-b0c2-2f1a2c8a8811"
/>

## Summary

Stabilizes the Mask Editor screenshot E2E by hiding the transient brush
cursor before capturing the dialog.

## Changes

- **What**: Moves the pointer from the mask editor pointer zone to the
Brush Settings panel before the screenshot and asserts that the brush
cursor is hidden.
- **Dependencies**: None.

## Review Focus

Please check that the test still exercises the dialog UI while excluding
only cursor-position noise from the screenshot. The PR also includes the
existing browser-test `AppMode` type import fix needed for
`typecheck:browser` on branches that touch `browser_tests/**`.


Validation:
- `pnpm exec oxfmt --check browser_tests/tests/maskEditor.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/maskEditor.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm
exec playwright test browser_tests/tests/maskEditor.spec.ts --grep
"opens mask editor from image preview button" --project=chromium
--repeat-each=10`

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-06-19 14:57:31 +00:00
jaeone94
ed028a88be Fix LiteGraph hidden widget metadata handling (FE-1014) (#12916)
## Summary

Fixes FE-1014 by making legacy LiteGraph honor backend-provided hidden
widget metadata.

This PR adds a regression test for the Painter node and updates
LiteGraph widget construction so that a backend input spec with `hidden:
true` is mirrored onto the top-level `widget.hidden` property that the
legacy canvas renderer actually reads.

## Problem

Backend node definitions can mark inputs as hidden, for example with
`extra_dict={"hidden": True}`. That metadata already flows into the
frontend widget options as `widget.options.hidden`, which is why Vue
nodes correctly hide those fields.

Legacy LiteGraph, however, does not use `widget.options.hidden` for
canvas visibility. Its rendering, layout, and hit-testing paths check
top-level `widget.hidden` instead. As a result, a field could be hidden
in Vue nodes while still appearing as an editable control in the legacy
LiteGraph canvas.

For affected nodes, this exposes fields that are intended to be
implementation details, schema/version values, or other non-user-facing
inputs.

## Root Cause

The frontend widget construction path copied backend display metadata
into `widget.options`, including:

- `advanced`
- `hidden`

But it did not mirror backend `hidden` metadata into `widget.hidden`.

That created a renderer split:

- Vue nodes and the right panel use `widget.options.hidden`.
- Legacy LiteGraph uses top-level `widget.hidden`.

So backend-hidden widgets were hidden in Vue mode but still visible and
clickable in legacy LiteGraph mode.

## Implementation

The production change is intentionally small and scoped to
backend-provided hidden metadata:

- Continue assigning `inputSpec.hidden` to `widget.options.hidden` as
before.
- When `inputSpec.hidden` is explicitly defined, also assign it to
top-level `widget.hidden`.

This keeps Vue behavior unchanged while making the legacy LiteGraph
renderer receive the same backend hidden signal through the field it
already uses for visibility.

The fix deliberately does not mirror `advanced` into top-level
`widget.advanced`. While investigating this area, I found that many
backend inputs define `advanced`, and changing legacy advanced-widget
behavior would be a much broader behavioral change than FE-1014
requires. This PR only addresses hidden metadata.

## Test Coverage

This PR adds and tightens Painter regression coverage because Painter
currently provides a concrete backend-hidden widget case:

- In Vue mode, the test verifies hidden Painter widgets are not rendered
to the user.
- In legacy LiteGraph mode, the test disables Vue nodes, loads the
Painter workflow, clicks the rows where backend-hidden number widgets
used to be exposed, and verifies the legacy graph editor dialog does not
open.

The legacy test specifically covers the backend-hidden number widgets
`width` and `height`. It uses user-observable behavior rather than
asserting internal widget flags directly.

A follow-up discussion is ongoing about the broader contract between
`widget.options.hidden` and top-level `widget.hidden`, especially for
frontend-extension-only hiding such as Painter `bg_color`. This PR
intentionally keeps that broader renderer-contract question out of scope
and focuses on backend `hidden` metadata from FE-1014.

## Validation

Validated locally with targeted Playwright coverage:

```bash
PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/painter.spec.ts --project=chromium -g "Does not render hidden standard widgets|Does not open editors for backend-hidden number widget rows"
```

Result:

```text
2 passed
```

Also validated with linting:

```bash
pnpm eslint src/services/litegraphService.ts browser_tests/tests/painter.spec.ts
pnpm eslint browser_tests/tests/painter.spec.ts
```

The commit hooks also passed:

- `oxfmt`
- `oxlint`
- `eslint`
- `pnpm typecheck`
- `pnpm typecheck:browser`

## Notes

The new legacy test was confirmed red before the production fix and
green after the production fix, so it is not a vacuous assertion. The
final cleanup commit only tightens test naming and coordinate handling
while preserving the same regression intent.
2026-06-19 14:05:49 +00:00
23 changed files with 129 additions and 604 deletions

View File

@@ -92,9 +92,7 @@ jobs:
make_latest: >-
${{ github.event.pull_request.base.ref == 'main' &&
needs.build.outputs.is_prerelease == 'false' }}
draft: >-
${{ github.event.pull_request.base.ref != 'main' ||
needs.build.outputs.is_prerelease == 'true' }}
draft: ${{ needs.build.outputs.is_prerelease == 'true' }}
prerelease: >-
${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true

View File

@@ -1,169 +0,0 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { externalLinks } from '../src/config/routes'
import type { Locale } from '../src/i18n/translations'
import { t } from '../src/i18n/translations'
import { test } from './fixtures/blockExternalMedia'
const PATH_EN = '/drops'
const PATH_ZH = '/zh-CN/drops'
const CLOUD_URL = 'https://cloud.comfy.org'
const LOCALES: ReadonlyArray<readonly [string, Locale]> = [
[PATH_EN, 'en'],
[PATH_ZH, 'zh-CN']
]
function heroSection(page: Page, locale: Locale) {
return page.locator('section').filter({
has: page.getByRole('heading', {
level: 1,
name: t('drops.hero.title', locale)
})
})
}
function ctaSection(page: Page, locale: Locale) {
return page.locator('section').filter({
has: page.getByRole('heading', {
level: 2,
name: t('drops.cta.heading', locale)
})
})
}
test.describe('Drops landing — desktop @smoke', () => {
test('renders the configured title at /drops', async ({ page }) => {
await page.goto(PATH_EN)
await expect(page).toHaveTitle(t('drops.page.title', 'en'))
})
test('renders the localized title at /zh-CN/drops', async ({ page }) => {
await page.goto(PATH_ZH)
await expect(page).toHaveTitle(t('drops.page.title', 'zh-CN'))
})
test('is indexable at both locales', async ({ page }) => {
await page.goto(PATH_EN)
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
await page.goto(PATH_ZH)
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
})
test('hero h1 renders the localized title in both locales', async ({
page
}) => {
await page.goto(PATH_EN)
await expect(
page.getByRole('heading', {
level: 1,
name: t('drops.hero.title', 'en')
})
).toBeVisible()
await page.goto(PATH_ZH)
await expect(
page.getByRole('heading', {
level: 1,
name: t('drops.hero.title', 'zh-CN')
})
).toBeVisible()
})
test('hero primary CTA links to /download per locale', async ({ page }) => {
for (const [path, locale, expectedHref] of [
[PATH_EN, 'en', '/download'],
[PATH_ZH, 'zh-CN', '/zh-CN/download']
] as const) {
await page.goto(path)
const primary = heroSection(page, locale).getByRole('link', {
name: t('drops.hero.primary', locale)
})
await expect(primary).toBeVisible()
await expect(primary).toHaveAttribute('href', expectedHref)
}
})
test('hero secondary CTA opens external Cloud in a new tab on both locales', async ({
page
}) => {
for (const [path, locale] of LOCALES) {
await page.goto(path)
const secondary = heroSection(page, locale).getByRole('link', {
name: t('drops.hero.secondary', locale)
})
await expect(secondary).toBeVisible()
await expect(secondary).toHaveAttribute('href', CLOUD_URL)
await expect(secondary).toHaveAttribute('target', '_blank')
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
}
})
test('subscribe banner shows text and a sign-up link in both locales', async ({
page
}) => {
for (const [path, locale] of LOCALES) {
await page.goto(path)
await expect(page.getByText(t('drops.banner.text', locale))).toBeVisible()
const signUp = page.getByRole('link', {
name: t('drops.banner.cta', locale)
})
await expect(signUp).toBeVisible()
await expect(signUp).toHaveAttribute('href', externalLinks.youtube)
await expect(signUp).toHaveAttribute('target', '_blank')
await expect(signUp).toHaveAttribute('rel', 'noopener noreferrer')
}
})
test('closing CTA shows heading and both action buttons in both locales', async ({
page
}) => {
for (const [path, locale] of LOCALES) {
await page.goto(path)
const section = ctaSection(page, locale)
await expect(
section.getByRole('heading', {
level: 2,
name: t('drops.cta.heading', locale)
})
).toBeVisible()
const primary = section.getByRole('link', {
name: t('drops.cta.primary', locale)
})
await expect(primary).toBeVisible()
await expect(primary).toHaveAttribute('href', externalLinks.cloud)
await expect(primary).toHaveAttribute('target', '_blank')
await expect(primary).toHaveAttribute('rel', 'noopener noreferrer')
const secondary = section.getByRole('link', {
name: t('drops.cta.secondary', locale)
})
await expect(secondary).toBeVisible()
await expect(secondary).toHaveAttribute('href', externalLinks.workflows)
await expect(secondary).toHaveAttribute('target', '_blank')
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
}
})
})
test.describe('Drops landing — mobile @mobile', () => {
test('closing CTA heading stays within viewport width', async ({ page }) => {
await page.goto(PATH_EN)
const heading = page.getByRole('heading', {
level: 2,
name: t('drops.cta.heading', 'en')
})
await heading.scrollIntoViewIfNeeded()
await expect(heading).toBeVisible()
const box = await heading.boundingBox()
expect(box, 'CTA heading bounding box').not.toBeNull()
expect(box!.x + box!.width).toBeLessThanOrEqual(
page.viewportSize()!.width + 1
)
})
})

View File

@@ -1,13 +1,10 @@
<script setup lang="ts">
import type { AnchorHTMLAttributes } from 'vue'
import Button from '../ui/button/Button.vue'
import BrandButton from '../common/BrandButton.vue'
type Cta = {
label: string
href: string
target?: AnchorHTMLAttributes['target']
rel?: AnchorHTMLAttributes['rel']
target?: '_blank' | '_self' | '_parent' | '_top'
}
type TermsLink = {
@@ -15,18 +12,11 @@ type TermsLink = {
href: string
}
const { heading, primaryCta, secondaryCta, termsLink } = defineProps<{
defineProps<{
heading: string
primaryCta: Cta
secondaryCta?: Cta
termsLink?: TermsLink
termsLink: TermsLink
}>()
function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
return (
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
)
}
</script>
<template>
@@ -39,32 +29,18 @@ function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
{{ heading }}
</h2>
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
<Button
as="a"
:href="primaryCta.href"
:target="primaryCta.target"
:rel="resolveRel(primaryCta)"
variant="outline"
size="lg"
>
{{ primaryCta.label }}
</Button>
<Button
v-if="secondaryCta"
as="a"
:href="secondaryCta.href"
:target="secondaryCta.target"
:rel="resolveRel(secondaryCta)"
variant="outline"
size="lg"
>
{{ secondaryCta.label }}
</Button>
</div>
<BrandButton
:href="primaryCta.href"
:target="primaryCta.target"
:rel="primaryCta.target === '_blank' ? 'noopener noreferrer' : undefined"
variant="outline"
size="lg"
class="mt-10 px-20 py-4 text-base uppercase lg:mt-12"
>
{{ primaryCta.label }}
</BrandButton>
<a
v-if="termsLink"
:href="termsLink.href"
class="mt-8 text-sm text-primary-comfy-canvas/70 underline underline-offset-4 transition-colors hover:text-primary-comfy-canvas"
>

View File

@@ -1,95 +0,0 @@
<script setup lang="ts">
import type { AnchorHTMLAttributes } from 'vue'
import Button from '../ui/button/Button.vue'
type Cta = {
label: string
href: string
target?: AnchorHTMLAttributes['target']
rel?: AnchorHTMLAttributes['rel']
}
type Visual = {
src: string
alt: string
width?: number
height?: number
}
const { visual, eyebrow, title, subtitle, primaryCta, secondaryCta } =
defineProps<{
visual?: Visual
eyebrow?: string
title: string
subtitle?: string
primaryCta: Cta
secondaryCta?: Cta
}>()
function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
return (
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
)
}
</script>
<template>
<section
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
>
<img
v-if="visual"
:src="visual.src"
:alt="visual.alt"
:width="visual.width"
:height="visual.height"
fetchpriority="high"
decoding="async"
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-lg"
/>
<p
v-if="eyebrow"
class="mb-4 text-sm font-medium tracking-wide text-primary-comfy-canvas/70 uppercase"
>
{{ eyebrow }}
</p>
<h1
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
>
{{ title }}
</h1>
<p
v-if="subtitle"
class="mt-6 max-w-2xl text-base text-primary-comfy-canvas/70 lg:text-lg"
>
{{ subtitle }}
</p>
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
<Button
as="a"
:href="primaryCta.href"
:target="primaryCta.target"
:rel="resolveRel(primaryCta)"
size="lg"
>
{{ primaryCta.label }}
</Button>
<Button
v-if="secondaryCta"
as="a"
:href="secondaryCta.href"
:target="secondaryCta.target"
:rel="resolveRel(secondaryCta)"
variant="outline"
size="lg"
>
{{ secondaryCta.label }}
</Button>
</div>
</section>
</template>

View File

@@ -8,7 +8,6 @@ const baseRoutes = {
cloudEnterprise: '/cloud/enterprise',
api: '/api',
gallery: '/gallery',
drops: '/drops',
about: '/about',
careers: '/careers',
customers: '/customers',

View File

@@ -4928,63 +4928,6 @@ const translations = {
'affiliate.cta.termsLabel': {
en: 'Read the affiliate program terms',
'zh-CN': '阅读联盟计划条款'
},
// Drops page (/drops) — head metadata
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.page.title': {
en: 'Drops — Everything new in ComfyUI',
'zh-CN': 'Drops — ComfyUI 最新动态'
},
'drops.page.description': {
en: 'Explore everything new in Comfy — releases, features, models, and resources across platform, cloud, community, and developer tools.',
'zh-CN':
'探索 Comfy 的最新动态 — 涵盖平台、云端、社区和开发者工具的发布、功能、模型和资源。'
},
// Drops page (/drops) — hero section
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.hero.title': {
en: 'Everything new in ComfyUI',
'zh-CN': 'ComfyUI 全新内容'
},
'drops.hero.primary': {
en: 'Download Desktop',
'zh-CN': '下载桌面版'
},
'drops.hero.secondary': {
en: 'Launch Cloud',
'zh-CN': '启动云端'
},
'drops.hero.visualAlt': {
en: 'Comfy',
'zh-CN': 'Comfy'
},
// Drops page (/drops) — subscribe banner
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.banner.text': {
en: 'Subscribe to the live stream and get your questions answered in real time.',
'zh-CN': '订阅直播,实时获得您的问题解答。'
},
'drops.banner.cta': {
en: 'Sign up now',
'zh-CN': '立即注册'
},
// Drops page (/drops) — closing CTA
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.cta.heading': {
en: 'Everything Comfy ships. All in one place.',
'zh-CN': 'Comfy 的全部内容,一处尽享。'
},
'drops.cta.primary': {
en: 'Open Comfy Cloud',
'zh-CN': '打开 Comfy Cloud'
},
'drops.cta.secondary': {
en: 'Try Workflow',
'zh-CN': '试用工作流'
}
} as const satisfies Record<string, Record<Locale, string>>

View File

@@ -1,18 +0,0 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import CtaSection from '../templates/drops/CtaSection.vue'
import HeroSection from '../templates/drops/HeroSection.vue'
import SubscribeBanner from '../templates/drops/SubscribeBanner.vue'
import { t } from '../i18n/translations'
const locale = 'en' as const
---
<BaseLayout
title={t('drops.page.title', locale)}
description={t('drops.page.description', locale)}
>
<SubscribeBanner locale={locale} />
<HeroSection locale={locale} />
<CtaSection locale={locale} />
</BaseLayout>

View File

@@ -1,18 +0,0 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import CtaSection from '../../templates/drops/CtaSection.vue'
import HeroSection from '../../templates/drops/HeroSection.vue'
import SubscribeBanner from '../../templates/drops/SubscribeBanner.vue'
import { t } from '../../i18n/translations'
const locale = 'zh-CN' as const
---
<BaseLayout
title={t('drops.page.title', locale)}
description={t('drops.page.description', locale)}
>
<SubscribeBanner locale={locale} />
<HeroSection locale={locale} />
<CtaSection locale={locale} />
</BaseLayout>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import CtaCenter01 from '../../components/blocks/CtaCenter01.vue'
import { externalLinks } from '../../config/routes'
import { t } from '../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<CtaCenter01
:heading="t('drops.cta.heading', locale)"
:primary-cta="{
label: t('drops.cta.primary', locale),
href: externalLinks.cloud,
target: '_blank'
}"
:secondary-cta="{
label: t('drops.cta.secondary', locale),
href: externalLinks.workflows,
target: '_blank'
}"
/>
</template>

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import HeroCenter01 from '../../components/blocks/HeroCenter01.vue'
import { externalLinks, getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
</script>
<template>
<HeroCenter01
:visual="{
src: '/affiliates/brand/comfy-amplified-logo.png',
alt: t('drops.hero.visualAlt', locale),
width: 632,
height: 632
}"
:title="t('drops.hero.title', locale)"
:primary-cta="{
label: t('drops.hero.primary', locale),
href: routes.download
}"
:secondary-cta="{
label: t('drops.hero.secondary', locale),
href: externalLinks.cloud,
target: '_blank'
}"
/>
</template>

View File

@@ -1,28 +0,0 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import { externalLinks } from '../../config/routes'
import { t } from '../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
// V1 fallback: point at the Comfy YouTube channel until content team supplies
// a dedicated event-registration URL (Google Form, Eventbrite, etc.).
const signUpHref = externalLinks.youtube
</script>
<template>
<div
class="bg-primary-comfy-plum text-primary-warm-white flex w-full flex-col items-center justify-center gap-2 px-6 py-3 text-center text-sm sm:flex-row sm:gap-4"
>
<p>{{ t('drops.banner.text', locale) }}</p>
<a
:href="signUpHref"
target="_blank"
rel="noopener noreferrer"
class="font-semibold uppercase underline underline-offset-4 hover:no-underline"
>
{{ t('drops.banner.cta', locale) }}
</a>
</div>
</template>

View File

@@ -69,6 +69,24 @@ export class TemplateHelper {
}
async mockIndex(): Promise<void> {
const customTemplatesHandler = async (route: Route) => {
const customTemplates: Record<string, string[]> = {}
await route.fulfill({
status: 200,
body: JSON.stringify(customTemplates),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
}
const customTemplatesPattern = '**/api/workflow_templates'
this.routeHandlers.push({
pattern: customTemplatesPattern,
handler: customTemplatesHandler
})
await this.page.route(customTemplatesPattern, customTemplatesHandler)
const indexHandler = async (route: Route) => {
const payload = this.index ?? mockTemplateIndex(this.templates)
await route.fulfill({

View File

@@ -2,7 +2,7 @@ import { readFileSync } from 'fs'
import { test } from '@playwright/test'
import type { AppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/utils/appMode'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON

View File

@@ -32,6 +32,10 @@ test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
await expect(dialog.getByText('Save')).toBeVisible()
await expect(dialog.getByText('Cancel')).toBeVisible()
await dialog.getByTestId('pointer-zone').hover()
await dialog.getByText('Brush Settings').hover()
await expect(dialog.getByTestId('brush-cursor')).toHaveCSS('opacity', '0')
await comfyPage.expectScreenshot(dialog, 'mask-editor-dialog-open.png')
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 324 KiB

View File

@@ -4,7 +4,6 @@ import {
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'
import type { WorkspaceStore } from '@e2e/types/globals'
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
// TODO: there might be a better solution for this
@@ -35,56 +34,6 @@ async function openSelectionToolboxHelp(comfyPage: ComfyPage) {
return comfyPage.page.getByTestId('properties-panel')
}
async function setLocaleAndWaitForWorkflowReload(
comfyPage: ComfyPage,
locale: string
) {
await comfyPage.page.evaluate(async (targetLocale) => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
if (!workflow) {
throw new Error('No active workflow while waiting for locale reload')
}
const changeTracker = workflow.changeTracker.constructor as unknown as {
isLoadingGraph: boolean
}
let sawLoading = false
const waitForReload = new Promise<void>((resolve, reject) => {
const timeoutAt = performance.now() + 5000
const tick = () => {
if (changeTracker.isLoadingGraph) {
sawLoading = true
}
if (sawLoading && !changeTracker.isLoadingGraph) {
resolve()
return
}
if (performance.now() > timeoutAt) {
reject(
new Error(
`Timed out waiting for workflow reload after setting locale to ${targetLocale}`
)
)
return
}
requestAnimationFrame(tick)
}
tick()
})
await window.app!.extensionManager.setting.set('Comfy.Locale', targetLocale)
await waitForReload
}, locale)
}
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
@@ -398,34 +347,33 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
await expect(helpPage.locator('img[alt="Safe Image"]')).toBeVisible()
})
test('Should handle locale-specific documentation', async ({
comfyPage
}) => {
// Mock different responses for different locales
await comfyPage.page.route('**/docs/KSampler/ja.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSamplerード
test.describe('Locale-specific documentation', () => {
test.use({ initialSettings: { 'Comfy.Locale': 'ja' } })
test('Should handle locale-specific documentation', async ({
comfyPage
}) => {
// Mock different responses for different locales
await comfyPage.page.route('**/docs/KSampler/ja.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSamplerード
これは日本語のドキュメントです。
`
})
})
})
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Node
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Node
This is English documentation.
`
})
})
})
// Set locale to Japanese
await setLocaleAndWaitForWorkflowReload(comfyPage, 'ja')
try {
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
@@ -434,9 +382,7 @@ This is English documentation.
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('KSamplerード')
await expect(helpPage).toContainText('これは日本語のドキュメントです')
} finally {
await setLocaleAndWaitForWorkflowReload(comfyPage, 'en')
}
})
})
test('Should handle network errors gracefully', async ({ comfyPage }) => {

View File

@@ -10,13 +10,16 @@ import {
} from '@e2e/fixtures/utils/painter'
import type { TestGraphAccess } from '@e2e/types/globals'
const HIDDEN_PAINTER_WIDGET_NAMES = ['width', 'height', 'bg_color'] as const
const HIDDEN_PAINTER_NUMBER_WIDGET_NAMES = ['width', 'height'] as const
test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
})
test.describe('Widget rendering', { tag: ['@widget'] }, () => {
test.describe('Widget rendering', () => {
test('Node enforces minimum size', async ({ comfyPage }) => {
const size = await comfyPage.page.evaluate(() => {
const graph = window.graph as TestGraphAccess | undefined
@@ -28,17 +31,15 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
expect(size![1]).toBeGreaterThanOrEqual(550)
})
test('Width, height, and bg_color standard widgets are hidden', async ({
test('Does not render hidden standard widgets in Vue mode', async ({
comfyPage
}) => {
const hiddenFlags = await comfyPage.page.evaluate(() => {
const graph = window.graph as TestGraphAccess | undefined
const node = graph?._nodes_by_id?.['1']
return (node?.widgets ?? [])
.filter((w) => ['width', 'height', 'bg_color'].includes(w.name))
.map((w) => w.options.hidden ?? false)
})
expect(hiddenFlags).toEqual([true, true, true])
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node).toBeVisible()
for (const widgetName of HIDDEN_PAINTER_WIDGET_NAMES) {
await expect(node.getByLabel(widgetName, { exact: true })).toBeHidden()
}
})
})
@@ -788,6 +789,49 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
})
})
test.describe(
'Painter legacy LiteGraph rendering',
{ tag: ['@widget', '@canvas'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
})
test('Does not open editors for backend-hidden number widget rows in legacy LiteGraph', async ({
comfyPage
}) => {
const painterNodes = await comfyPage.nodeOps.getNodeRefsByType('Painter')
expect(painterNodes).toHaveLength(1)
const painterNode = painterNodes[0]!
const maskWidget = await painterNode.getWidgetByName('mask')
const maskWidgetClientPosition = await maskWidget.getPosition()
const widgetRowClientHeight = await comfyPage.page.evaluate(
() =>
(window.LiteGraph!.NODE_WIDGET_HEIGHT + 4) *
window.app!.canvas.ds.scale
)
const legacyPrompt = comfyPage.page.locator('.graphdialog')
await expect(legacyPrompt).toBeHidden()
for (const [
index,
widgetName
] of HIDDEN_PAINTER_NUMBER_WIDGET_NAMES.entries()) {
await test.step(`Click ${widgetName} row`, async () => {
await comfyPage.page.mouse.click(
maskWidgetClientPosition.x,
maskWidgetClientPosition.y + widgetRowClientHeight * (index + 1)
)
await comfyPage.nextFrame()
await expect(legacyPrompt).toBeHidden()
})
}
})
}
)
test.describe(
'Painter — input image connection',
{ tag: ['@widget', '@vue-nodes', '@slow'] },

View File

@@ -1,13 +1,13 @@
import { expect, mergeTests } from '@playwright/test'
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import { createCloudAssetsFixture } from '@e2e/fixtures/assetApiFixture'
import { makeTemplate } from '@e2e/fixtures/data/templateFixtures'
import { withTemplates } from '@e2e/fixtures/helpers/TemplateHelper'
import { TestIds } from '@e2e/fixtures/selectors'
import { templateApiFixture } from '@e2e/fixtures/templateApiFixture'
const test = mergeTests(comfyPageFixture, templateApiFixture)
const test = mergeTests(createCloudAssetsFixture([]), templateApiFixture)
const Cloud = TemplateIncludeOnDistributionEnum.Cloud
const Desktop = TemplateIncludeOnDistributionEnum.Desktop

View File

@@ -1,26 +0,0 @@
import { watch } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useExtensionService } from '@/services/extensionService'
/**
* Cloud-only extension that enforces active subscription requirement
*/
useExtensionService().registerExtension({
name: 'Comfy.Cloud.Subscription',
setup: async () => {
const { isLoggedIn } = useCurrentUser()
const { requireActiveSubscription } = useBillingContext()
const checkSubscriptionStatus = () => {
if (!isLoggedIn.value) return
void requireActiveSubscription()
}
watch(() => isLoggedIn.value, checkSubscriptionStatus, {
immediate: true
})
}
})

View File

@@ -36,10 +36,6 @@ if (isCloud) {
await import('./cloudRemoteConfig')
await import('./cloudBadges')
await import('./cloudSessionCookie')
if (window.__CONFIG__?.subscription_required) {
await import('./cloudSubscription')
}
}
// Feedback button for cloud and nightly builds

View File

@@ -223,8 +223,18 @@ export const MODEL_NODE_MAPPINGS: ReadonlyArray<
['film', 'FILM VFI', 'ckpt_name'],
// ---- Ultralytics YOLO detectors (ComfyUI-Impact-Pack) ----
['ultralytics/bbox', 'UltralyticsDetectorProvider', 'model_name'],
['ultralytics/segm', 'UltralyticsDetectorProvider', 'model_name'],
// Intentionally NOT mapped to the asset-picker. The cloud asset-ingestion
// metadata for nested model folders (`ultralytics/bbox`, `ultralytics/segm`)
// still has the two known half-bugs described in #12075:
// 1. Tag lookup mismatch (cloud stores combined tags, picker queries split).
// 2. Submitted value mismatch (picker returns basenames, ingest expects
// subdirectory-prefixed `bbox/<file>` / `segm/<file>`).
// PR #12151 re-added the bbox/segm entries before either half was fixed,
// reintroducing the FaceDetailer breakage. Until BE-689 lands the cloud-side
// fixes, leave these disabled so the node falls back to the static combo
// populated from `/api/object_info`.
// ['ultralytics/bbox', 'UltralyticsDetectorProvider', 'model_name'],
// ['ultralytics/segm', 'UltralyticsDetectorProvider', 'model_name'],
// ---- Mel-Band RoFormer audio separation (ComfyUI-MelBandRoFormer) ----
['diffusion_models', 'MelBandRoFormerModelLoader', 'model_name'],

View File

@@ -302,6 +302,7 @@ export const useLitegraphService = () => {
advanced: inputSpec.advanced,
hidden: inputSpec.hidden
})
if (inputSpec.hidden !== undefined) widget.hidden = inputSpec.hidden
if (dynamic) widget.tooltip = inputSpec.tooltip
}

View File

@@ -88,9 +88,10 @@ const { onResizeEnd } = useStablePrimeVueSplitterSizer(
const TYPEFORM_WIDGET_ID = 'jmmzmlKw'
const bottomLeftRef = useTemplateRef('bottomLeftRef')
const bottomRightRef = useTemplateRef('bottomRightRef')
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
const bottomLeftRef = useTemplateRef<HTMLDivElement>('bottomLeftRef')
const bottomRightRef = useTemplateRef<HTMLDivElement>('bottomRightRef')
const linearWorkflowRef =
useTemplateRef<InstanceType<typeof LinearControls>>('linearWorkflowRef')
function dragDrop(e: DragEvent) {
const { dataTransfer } = e