getByRole('button', { name: 'Media Assets' }) never matched a real
button: the assets tab button is labelled "Assets" (its tooltip), while
"Media Assets" is only the panel header span (no button role). Open the
tab via the assetsTab page object, as appMode.spec already does.
Adds the single billing-status banner slot for team plans, rendered in
priority order — **paused > payment declined > out of credits > ending**
— with role-aware CTAs. At most one state shows at a time.
Placement matches the prototype (`comfydesigner/team-workspaces-v1`) and
Figma: the banner lives in the **workspace settings panel**, in one slot
between the tab list and the tab body, so it shows on every tab. Our
container has two tabs, so one mount covers both rather than duplicating
the banner into each panel as the prototype does.
Derivation lives in a pure `deriveBillingBanner()`; the component only
renders. State comes from `billing-status` fields, never from error
strings (per Luke's user story).
## Gated on the plan, not the workspace type
`useBillingContext` gains `isTeamPlan`: a credit stop marks the
per-credit Team plan, a `team-` slug the retired seat-based ones.
Workspace type is the wrong question in both directions:
- A **team workspace can sit on a retired seat-based plan** (tier
STANDARD/CREATOR/PRO) — the type gate admits it.
- Once consolidated billing lands (cloud#5010), a **personal workspace
can hold a team plan**. The backend blocks that today pending BE-1526,
so the type gate is only accidentally right, and only for now. Per
[Hunter](https://comfy-organization.slack.com/archives/C0BEE5503RQ/p1784151520761519?thread_ts=1784051425.176829):
*"We should deprecate the workspace type and derive it from the
subscription."*
Two deliberate non-choices:
- **Not gated on `isActiveSubscription`**, unlike the existing
`isLegacyTeamPlan`. The spend gate folds `billing_status` into
`is_active`, so a paused or payment-failed team plan reports
`is_active=false` — it must still read as a team plan exactly when the
banner is needed.
- **Not gated on `subscription_tier === 'TEAM'`**, which would silently
drop every legacy team subscriber. The FE cannot express `'TEAM'`
anyway: `tierPricing.ts` resolves `SubscriptionTier` from the
**registry** spec for what is an **ingest** field. That's a real bug,
but orthogonal — filing separately.
## Fixes payment_failed, which was dead code
`payment_failed` denies spend, so it always arrives with
`is_active=false` — and the check sat **below** the `is_active` gate,
two lines under a comment documenting that exact trap for `paused`.
**Every team in Stripe dunning saw no banner.** Now hoisted alongside
`paused`.
Its tests passed only because they spread the `funded` fixture
(`isActiveSubscription: true`) onto `payment_failed` — a pairing the
backend never emits. They now pin `is_active=false`. Both this and the
`isTeamPlan` decoupling are mutation-tested: reverting either kills
tests.
Members no longer fall through from `payment_failed` to out-of-credits —
with the real pairing that path was unreachable, and DES-380 says
members never see the payment banner.
## Renders
The six role/state variants render from the Storybook stories at
`Platform/Workspace/BillingStatusBanner`. Each story drives the real
`deriveBillingBanner` through a stubbed billing context, so a story can
only show a state the backend can actually produce — not a hand-set
banner kind.
Run them with `DISTRIBUTION=cloud pnpm storybook` — the banner is
cloud-only and `isCloud` is compile-time, so a plain `pnpm storybook`
renders every story empty.
Icon language follows the prototype's severity rule: amber
triangle-alert for every action-needed state (paused, payment declined,
out of credits), muted circle-alert reserved for the informational "plan
ends" notice.
## Feature flag
Personal-workspace routing now keys off **`billing_control_enabled`**,
replacing `consolidated_billing_enabled` (which had exactly one consumer
— this routing check — so it is replaced rather than left alongside).
Cloud registers the new flag in
[Comfy-Org/cloud#5091](https://github.com/Comfy-Org/cloud/pull/5091).
This coupling is load-bearing for the banner. `isTeamPlan` requires the
workspace billing rail, so a personal workspace holding a team plan only
surfaces billing state once its routing flag is on. Had routing stayed
on `consolidated_billing_enabled` while the subscribe path unblocks
under `billing_control_enabled`, the two could diverge: a personal
workspace could buy a team plan and then be routed to legacy, which
carries no `billing_status` at all, blanking the banner.
**Merge order:** cloud#5091 must merge and `billing_control_enabled`
must exist in PostHog before this ships, otherwise the flag resolves
false for everyone and every personal workspace routes to legacy. Both
flags default false, so the floor is today's behaviour — but any users
already rolled out on `consolidated_billing_enabled` would revert to
legacy billing until `billing_control_enabled` is rolled out to them.
Worth confirming the current rollout state before merge.
## Backend status
`billing_status: 'paused'` merged in cloud
[#5075](https://github.com/Comfy-Org/cloud/pull/5075) but is **not
emitted yet**: the lifecycle handler that writes it is unreachable in
prod (ingest's forward allowlist omits `customer.subscription.*`;
billing-api is ClusterIP with no ingress). That's BE-1530's unfinished
half, tracked backend-side. The paused banner stays inert until then.
The other three states render from fields the API already returns:
`billing_status=payment_failed` + `renewal_date`, `has_funds`, and
`cancel_at`.
## How has this been tested?
- `useBillingContext.test.ts` — `isTeamPlan` across per-credit, legacy,
paused, payment-failed, and team-workspace-on-personal-plan
- `deriveBillingBanner.test.ts` — priority order, role gating, and the
realistic `is_active=false` pairing for both paused and payment_failed
- `BillingStatusBanner.test.ts` — per-variant copy and actions, date
interpolation + no-date fallback, cross-mount dismiss, ending read-only
for a non-original owner
- `useBillingBanner.test.ts` — dismiss resets after a top-up so a later
exhaustion re-shows
- `WorkspacePanelContent.test.ts` — the banner takes one slot above the
tab body
- 696 tests pass across the touched areas; typecheck / lint / knip clean
## Known gaps
- **Blocked-run surface:** settings-scoped per the prototype, so a user
who exhausts credits mid-canvas won't see it until they open settings.
Queue-time blocked-submit is carried by the existing
insufficient-credits dialog. An app-shell surface would be a design
call, not this PR.
- **Personal workspaces on consolidated billing** get no banner —
`isTeamPlan` excludes them by design, matching FE-1246's team scope and
the `!isInPersonalWorkspace` precedent in `useSubscriptionDialog`. Their
copy would need personal variants (`outOfCredits.body` says "Your
team…") and a free-tier decision. Belongs with the consolidated-billing
rollout.
- **Prototype slot:** the prototype's banner exposes an `actions` slot
used only by its Invoices view. We have no such view, so it's omitted
until needed.
## screenshots
<img width="1150" height="574" alt="01-paused-owner"
src="https://github.com/user-attachments/assets/c1ff0abd-0286-4453-85f2-6f89cff1e655"
/>
<img width="1150" height="574" alt="02-paused-member"
src="https://github.com/user-attachments/assets/ad5ce843-cbc6-49d1-95fc-28e908bc010a"
/>
<img width="1150" height="574" alt="03-payment-declined"
src="https://github.com/user-attachments/assets/717c1191-8f12-4835-90c6-5fe345b20780"
/>
<img width="1150" height="574" alt="04-payment-declined-no-date"
src="https://github.com/user-attachments/assets/c7e483e4-e0cb-4810-b090-a9c432546dbd"
/>
<img width="1150" height="574" alt="05-out-of-credits-owner"
src="https://github.com/user-attachments/assets/9261fd39-ecdb-4501-a401-47d9cbee04dc"
/>
<img width="1150" height="574" alt="06-out-of-credits-member"
src="https://github.com/user-attachments/assets/4a91af88-4405-43e1-8975-b64fdf180a13"
/>
<img width="1150" height="574" alt="07-ending-owner"
src="https://github.com/user-attachments/assets/80e5011e-ef11-482f-bc49-1ddfbe991221"
/>
<img width="1150" height="574" alt="08-ending-non-original-owner"
src="https://github.com/user-attachments/assets/129b79aa-1427-4286-829c-eed501a6d44d"
/>
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds `/booking-confirmation`, a hidden `noindex` page intended as the
post-submit redirect target after a visitor books a meeting with the GTM
team. The page confirms the booking, tells the visitor to check their
email for the calendar invite, and offers a small list of "while you
wait" resources.
## Design
Mirrors the `/individual-submission` template landed in #13697 —
`BaseLayout` + `HeroSection` — so typography and spacing feel native to
the marketing site. The "Resources while you wait" heading uses the
yellow-italic accent used elsewhere on the site; each resource is a
centered `label — url` row.
Resource links source their URLs from `config/routes.ts` where possible
(`routes.learning`, `routes.customers`, `externalLinks.workflows`,
`externalLinks.docs`) so the page stays in sync if those paths ever
move.
## Hidden-page mechanics
Follows the pattern already used for `/individual-submission`,
`/terms-of-service`, and `/payment/*`:
- `noindex` meta tag on the page (both `en` and `zh-CN`)
- Both `/booking-confirmation` and `/zh-CN/booking-confirmation` added
to `SITEMAP_EXCLUDED_PATHNAMES` in `astro.config.ts` via
`LOCALE_PREFIXES`, so neither URL is emitted in the sitemap
- Not linked from `routes.ts`, `HeaderMain`, or `SiteFooter`
- English + Simplified Chinese variants both exist as full pages
(matching the existing `/individual-submission` treatment)
## Next step (not in this PR)
Someone with HubSpot / Chili Piper access needs to configure the
meeting-booking flow to redirect visitors to
`https://comfy.org/booking-confirmation` after they schedule.
## Verification
- `pnpm typecheck` → 0 errors, 0 warnings (10 pre-existing hints
unrelated to this change)
- `pnpm build` (in `apps/website`) → 504 pages built;
`/booking-confirmation/` and `/zh-CN/booking-confirmation/` emitted;
neither appears in `dist/sitemap-0.xml`
- `oxlint` and `oxfmt` clean on changed files (also enforced by
lint-staged pre-commit)
- Playwright preview at 1280×900 (English + zh-CN) and 390×844 (English
mobile) — hero, body copy, section header, and all 4 resource rows
render as expected; no horizontal overflow on mobile; only console noise
was transient Vite dep-optimizer 504s unrelated to this change
- `/review` (oracle) flagged one issue on the initial revision: the
zh-CN page was `noindex` but its URL still appeared in the sitemap.
Fixed by extending the exclusion set across both `LOCALE_PREFIXES`; the
follow-up `/review` returned 0 findings.
## Screenshots
## Screenshots



---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Michael B <michael@imick.io>
## Summary
Move node-definition locale serialization into a focused, testable
module while preserving generated locale behavior.
## Changes
- **What**: Extract locale key normalization and Vue I18n escaping from
the Playwright collector into `nodeDefLocaleSerializer.ts`.
- **What**: Keep input and output tooltips raw while escaping fields
compiled by Vue I18n.
- **What**: Replace broad utility tests with behavioral serializer
coverage for escaping, output shapes, ordering, and missing values.
## Review Focus
Check parity with the generator introduced in #13631, especially raw
tooltips, runtime widget labels, known data-type output elision, and
normalized keys.
Validation: `pnpm typecheck`, focused Vitest, oxlint, ESLint, and oxfmt.
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds `/individual-submission`, a hidden `noindex` page intended as the
post-submit redirect target for the HubSpot sales form when the visitor
selects the "individual" option. The page thanks the visitor, points
them at the self-serve plans on `/cloud/pricing`, and offers a
`mailto:gtm-team@comfy.org` fallback for enterprise inquiries.
## Design
Mirrors the `terms-of-service.astro` template — `BaseLayout` +
`HeroSection` — so typography and spacing feel native to the marketing
site. Plan names (**Standard**, **Creator**, **Pro**, **Teams**) are
highlighted with the yellow-italic accent used elsewhere on the site.
## Hidden-page mechanics
Follows the pattern already used for `/terms-of-service` and
`/payment/*`:
- `noindex` meta tag on the page
- Path added to `SITEMAP_EXCLUDED_PATHNAMES` in `astro.config.ts`
- English-only, with a `/zh-CN/individual-submission` →
`/individual-submission` redirect (matches `termsOfService` handling)
- Not linked from `routes.ts`, `HeaderMain`, or `SiteFooter`
## Next step (not in this PR)
Someone with HubSpot access needs to configure the form (portal
`244637579`, form `94e05eab-1373-47f7-ab5e-d84f9e6aa262`) to redirect to
`https://comfy.org/individual-submission` when the "individual" option
is selected. Happy to add a JS-side fallback in a follow-up PR if
HubSpot's conditional redirects aren't a fit.
## Verification
- `pnpm typecheck` → 0 errors (10 pre-existing hints unrelated to this
change)
- `pnpm build` → 499 pages built; `/individual-submission/` and
`/zh-CN/individual-submission/` (redirect stub) emitted; sitemap
excludes both
- Playwright preview at 1280×900 and 390×844 — hero title, prose,
plan-name highlighting, CTA button, and mailto link all render as
expected; no console errors; `whitespace-nowrap` keeps the email address
on one line on mobile
- `/review` (oracle) on the previous revision: 0 findings; only change
since is the URL rename
- `/zh-CN/individual-submission` redirect verified in preview
## Screenshots


---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: imick-io <michael@concreo.io>
Co-authored-by: Michael B <michael@imick.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- residual view store + controller bridging the shared coachmark engine
- overlay: multi-hole scrim, camera choreography, Floating UI placement, shared
CoachmarkCard, result media and generating states
- post-run nudge with deferral behind the upgrade modal
- first-run telemetry funnel; e2e coverage
- onboarding_tour feature flag (remoteConfig + useFeatureFlags)
- fold first-run telemetry events into the shared onboarding schema
- useDesktopLayout composable (shared by the tour gate and app-mode triggers)
- expose UPGRADE_DIALOG_KEYS; thread templateId through useTemplateUrlLoader
- data-testids on the action bar and tab list; onboarding candidate predicate
- EntryPath adds 'firstRun'; TOURS is Partial so an injected tour needs no static entry
- startTour accepts a precomputed step definition; activeTour is exposed read-only
- engine telemetry and TourOverlay cover only registry (TOURS) tours; injected tours self-render and self-report
- TourOverlay.test mock provides activeTour for the new gate (assertions unchanged)
## Summary
Reports each hosted Cloud renderer's declarative Firebase auth state to
Desktop. Desktop remains the sole owner of PostHog identity and
arbitrates all live `WebContents` before changing the process-global
person.
Linear: [GTM-278](https://linear.app/comfyorg/issue/GTM-278) · Parent:
[GTM-93](https://linear.app/comfyorg/issue/GTM-93)
## Changes
- reports `pending` as soon as host auth synchronization starts
- waits for Firebase initialization, then reports `signed_out` or
`signed_in` with the Firebase UID
- reports restored sessions, logout, and direct account switches through
the same declarative state contract
- makes repeated same-UID Firebase updates a no-op
- adds the optional `reportFirebaseAuthState` contract to the Desktop
bridge types
- keeps host bridge failures from interrupting renderer startup or
Firebase auth
## Ownership and privacy
- the frontend never calls PostHog `identify`, `alias`, bind, or unbind
APIs
- browser PostHog provider behavior is unchanged
- the auth-state message is local to Desktop; Desktop #1272 applies its
own telemetry-consent gate before any PostHog identify or emission
- `enable_telemetry` controls only the frontend `HostTelemetrySink`
(renderer event forwarding), so this observer deliberately does not
share that rollout flag
- no canonical `comfy_user_id`, `/api/user` lookup, Cloud API
dependency, Redis state, token redemption, or website transport change
is added
## Review and rollout order
Review [Comfy-Desktop
#1272](https://github.com/Comfy-Org/Comfy-Desktop/pull/1272) first
because it defines the trusted state-reporting API and consensus policy.
Deploy this frontend first: the method is optional and therefore a no-op
on older Desktop builds. Then release Desktop #1272, and only afterward
deploy [comfy-router
#34](https://github.com/Comfy-Org/comfy-router/pull/34).
Auth [Comfy-Desktop
#1222](https://github.com/Comfy-Org/Comfy-Desktop/pull/1222) must not
become an identity authority; with Desktop #1272 its legacy imperative
bind is inert and the post-reload Firebase report is the source of
truth.
## Testing
- `pnpm test:unit src/platform/telemetry/hostUserIdSync.test.ts
src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts`
— 53 tests passed
- `pnpm typecheck`
- targeted Oxlint and ESLint
- targeted `oxfmt --check`
- `pnpm knip`
- pre-commit typecheck/lint/format hooks
- pre-push `knip`
- `git diff --check`
No user-facing UI changes.
## Summary
Report the current URL to Customer.io after its in-app plugin
initializes and on every client-side navigation, allowing queued web
in-app messages to satisfy page rules and render.
## Changes
- **What**: Wait for the in-app plugin to register, flush an initial
URL-based `page()` call, and consume the router's existing page-view
dispatches for subsequent SPA navigation.
- **Tests**: Cover initial page reporting, plugin-registration ordering,
and client-side route changes.
## Review Focus
Customer.io intentionally receives `page()` without an explicit page
name. Passing the telemetry page title would make the in-app SDK match
page rules against that title instead of the full URL; the live campaign
requires `contains cloud.comfy.org`.
Root cause: the production SDK debugger showed a correctly identified
user and active SSE connection, but `Route: NONE` and no eligible
messages because the provider did not implement `trackPageView()`.
Complements #13556, which addresses identity stitching for existing
email-keyed Customer.io profiles.
Validation: targeted unit tests (21/21), `pnpm typecheck`, targeted
ESLint and oxfmt checks, and the pre-push `knip` gate.
We need this fix urgently, blocking all welcome emails and fixing
several vectors.
## Summary
Identify Customer.io users with Firebase UID and email traits, then wait
for that profile update before sending `app:user_auth_completed` so
Welcome Onboarding entrants satisfy the Valid Email Address segment.
Preserve the correct session or anonymous identity after the auth event
is handed to the SDK.
## Changes
- **Auth delivery ordering**: Queue auth events with their Firebase
identity and send the event only after Customer.io `identify(uid, {
email })` settles. Missing-email identities fall back to UID-only
identification, and rejected identifies are logged without suppressing
auth telemetry.
- **Identity lifecycle**: Serialize identity transitions and event
handoff, deduplicate matching `identify()` operations, and snapshot
resolved UID/email together. After auth delivery, restore the configured
or resolved session identity—or reset to anonymous—including for
SDK-late buffered events and auth callbacks that arrive after logout.
Ordinary event delivery does not hold the identity queue open while its
network request completes.
- **Payload privacy**: Use email as a Customer.io person trait while
excluding raw `email` and `share_id` from Customer.io event properties.
- **Dependencies**: None.
## Incident evidence
- Campaign 1 triggers on `app:user_auth_completed` with
`is_new_user=true` and requires the Valid Email Address segment.
- In a recent 15-minute sample, all 30 new-user events had an email
property, while all 30 corresponding Customer.io person profiles had
`identifiers.email=null`.
- Starts fell from 556/day through June 30 to 23.5/day after July 1,
excluding a one-day spike.
- Event properties do not update Customer.io person identifiers, and the
previous fire-and-forget identify allowed the auth event to race the
profile update.
## Review Focus
The auth result's `user_id` is the authoritative Firebase UID for the
identify that gates `app:user_auth_completed`. Once the SDK snapshots
that event, the provider restores the current configured/resolved
identity or the signed-out anonymous state before handing off later
events. Matching identity operations are deduplicated; repeated auth
completions remain distinct telemetry events. A rejected identify must
not suppress auth telemetry.
This supersedes the partial fixes in #13556 (email trait without
ordering) and #12888 (reset/re-login and redaction without the
email-profile race fix).
## Validation
- `pnpm test:unit
src/platform/telemetry/providers/cloud/CustomerIoTelemetryProvider.test.ts`
— 35 passed
- `pnpm typecheck`
- File-scoped type-aware Oxlint
- File-scoped ESLint
- File-scoped oxfmt check
- `pnpm knip` via the pre-push hook
A browser E2E test is not practical for this SDK delivery-ordering fix:
the regressions require controlling unresolved Customer.io
`identify`/`track` promises and observing the active identity when each
event is handed to the SDK. The focused unit suite controls those
boundaries directly and covers SDK-late queueing, configured overrides,
identity restoration, logout races, missing email, rejection fallback,
payload redaction, and non-blocking event delivery.
## Rollout / backfill
- Deploy or backport this change to the active cloud release line.
- Verify new-user Customer.io profiles receive a non-null email
identifier before the auth event is processed, then monitor Campaign 1
starts against the pre-July baseline.
- Backfill or re-trigger already affected new users separately; this
frontend change repairs future auth deliveries and does not replay
historical events.
- No live Customer.io campaign changes are included.
## Summary
Stacked on #13693. Ports the capabilities six-card expansion and the
nine-item FAQ rewrite from #13649 (imick-io) onto the reworked page,
keeping the two-option setup module from the base branch as the single
setup implementation.
## Changes
- **What**:
- Capabilities section grows from three to six cards (Direct any model,
Generate in batches, Ship it as an app) with real media wired;
`FeatureRows01` and `VideoPlayer` gain a `fit: cover | contain` option
and a fixed-ratio media column.
- FAQ rewritten to nine items, reordered so setup facts come first;
`FAQSplit01` autolinks URLs in answers (regex includes coderabbit's
character-class fix from the source PR).
- Quality pass on top of the port: FAQ links get the same branded
`focus-visible` rings as the setup section, "download the assets to
local" copy polished, and two new e2e tests cover the six cards and the
nine-item FAQ with its autolinked server URL.
- Deliberately NOT ported from #13649: its
`SetupSection`/`FeatureGrid01` Step 2 changes and the `cloudMcpServer`
route entry. Those solve the buried-URL problem on the old three-step
layout; the base branch's two-option redesign already covers it via
`externalLinks.mcpEndpoint`.
## Review Focus
- Translation splice: `mcp.tools.*` and `mcp.faq.*` blocks taken
verbatim from #13649's branch (en + zh-CN); base branch's hero/setup
keys untouched.
- All 7 e2e tests pass against the production build; tools and FAQ
sections verified rendering with live media.
Linear:
[PM-132](https://linear.app/comfyorg/issue/PM-132/comfyorgmcp-faq-is-stale-and-contradicts-the-page-rewrite-faq-fix)
Source: #13649
## Screenshots (if applicable)
---------
Co-authored-by: Michael B <michael@imick.io>
## Summary
Add the Brand Portal page at `/brand` (and `/zh-CN/brand`), implementing
the Figma design in English and Simplified Chinese.
## Changes
- **What**: New marketing page with hero, logo system, color palette,
voice guidelines, trademark, and questions sections. Reuses
`BaseLayout`, `Button`, and `SectionHeader`; adds `data/brandColors.ts`
(palette with hex/rgb/hsl/cmyk) and `templates/brand/` sections. The
hero "View brand guidelines" downloads the guidelines PDF; both
"Download logos" actions download the brand-assets zip. Route registered
in `config/routes.ts`; copy in `i18n/translations.ts` (en + zh-CN).
- **Dependencies**: none
## Review Focus
- Typography, spacing, and palette values were matched to the Figma per
section; the Chinese is a first-pass translation.
- The guidelines PDF and logo zip are served from `media.comfy.org` with
`Content-Disposition: attachment` so the buttons download rather than
open inline.
- Contact links point to `/contact` in the same tab (the Figma prototype
annotation used a new tab).
Implements FE-1217.
## Screenshots
**Desktop**
<img width="1440" height="3648" alt="brand-portal-desktop"
src="https://github.com/user-attachments/assets/050855b2-dd2a-4616-b5c8-c2c79b049df8"
/>
**Mobile**
<img width="390" height="6342" alt="brand-portal-mobile"
src="https://github.com/user-attachments/assets/85f3ca97-655e-4fcc-987d-976faeae2efa"
/>
## Test plan
- [x] `pnpm --filter @comfyorg/website typecheck`
- [x] `pnpm lint`
- [x] `pnpm format`
- [x] `e2e/brand.spec.ts` (sections render; logo bundle and
guidelines-PDF download links)
- [x] Verified responsive at 1440 / 834 / 390
## Summary
Implements the genuine parts of the schema enrichment follow-up to
#13480 (FE-1232): the ComfyUI `SoftwareApplication` now names its
canonical page and links back to its `SoftwareSourceCode`. Several other
items from the recommendations doc are deliberately **not** implemented,
with reasoning below.
## Changes
- **`mainEntityOfPage` on the ComfyUI application.** The WebPage already
pointed at the app via `mainEntity`; the app now points back at the home
page, completing the relationship.
- **`isBasedOn` linking the app to its source code.**
`SoftwareSourceCode` already declared `targetProduct` -> the app, but
only on the home page, and the app had no reverse link. The app now
references the source node, so the application and its GitHub repository
resolve as one connected entity.
- **Download pages now emit the source code node** (en + zh-CN). They
render the application node, so without this the new `isBasedOn`
reference would not resolve inside their graph. `/download` previously
described the app with no link to the repo at all.
- Extracted a shared `comfyUiSourceCodeId()` so the `#sourcecode` anchor
has one definition, matching the `comfyUiSoftwareId()` pattern from
#13480 review.
- 3 unit tests: canonical page is set, `isBasedOn` round-trips to the
source node's real `@id`, and third-party software gets neither.
## Review Focus
**Why `mainEntityOfPage` is a plain URL, not an `@id` reference.** The
app node is shared by `/` and `/download` under one site-wide `@id`
(`/#software`). A per-page reference would make the same entity claim
two different canonical pages, and pointing at the home page's
`#webpage` id would leave an unresolved reference on `/download`, which
`validate:jsonld` fails the build on. A plain URL keeps the node
byte-identical everywhere it is emitted.
**Coupling worth knowing about:** any page emitting
`comfyUiApplicationNode` must also emit `comfyUiSourceCodeNode`, or the
`isBasedOn` reference dangles. This is enforced at build time by
`validate:jsonld`, so it fails loudly rather than shipping a broken
graph.
**Third-party software is untouched.** Packs and models get neither
property, the same isolation already applied to `author`, `publisher`,
`seller` and `sameAs`. They are not our application and do not share our
repository.
## Not implemented, and why
The recommendations doc proposed six schema changes. Four are not
actionable:
1. **`Review` + `AggregateRating` (4.8 / 126, "Verified User")** -
invented numbers with no real review system behind them. Google
disallows self-serving ratings that are not backed by genuine on-page
reviews, and this repo's own build validator fails on fabricated
ratings.
2. **`brand` on the `SoftwareApplication`** - not valid. Per schema.org,
`brand` applies to Organization, Person, Product and Service.
`SoftwareApplication` is a `CreativeWork`, not a `Product`, so `brand`
is outside its domain. The doc's own proposed code puts `brand` on a
`Product` node, not on a `SoftwareApplication`, so the recommendation
does not match the example it ships with. The relationship it is
reaching for already exists here: the app declares `author` and
`publisher` -> Comfy Org, the free Offer declares `seller` -> Comfy Org,
and our actual `Product` (Comfy Cloud, on `/cloud/pricing`) already
carries `brand` -> Comfy Org.
3. **`seller` on the Offer** - already shipped in #13480. Live on the
free Offer and on all three pricing tiers.
4. **Strengthening the Organization (logo, `sameAs`)** - already
shipped, and the proposed version is a downgrade: it uses `favicon.ico`
in place of our 512x512 PNG, and its `sameAs` points at the Wikipedia
article for "Comfort" (the feeling). The accompanying `about`/`mentions`
entities map our title words to unrelated pages (Canvas -> Canvas
Networks, Scratch -> Abrasion). We already link the correct Wikidata
entities, the ComfyUI Wikipedia article and G2.
The document's larger "Watch Page" checklist targets a video
watch/listing page that does not exist on this site, and is mostly
content work rather than schema.
## Scope note
This is entity-graph hygiene, not a ranking change. `mainEntityOfPage`
and `SoftwareSourceCode` do not produce rich results. The value is that
machines can resolve that Comfy Org publishes ComfyUI, an open-source
application whose code lives on GitHub.
## Verification
`test:unit` 199 passed, `astro check` 0 errors, `knip` clean, `build`
498 pages, **`validate:jsonld` passed across 501 pages** (confirms no
unresolved `@id` on any page), and e2e for the download, cloud-nodes and
affiliates specs (the JSON-LD asserting ones) 38 passed.
No visual change: this is `<head>` structured data only, so there is
nothing to screenshot.
Ref: FE-1232
## Summary
Prevent single-select dropdowns from clearing their value when the
currently selected item is selected again. This fixes Load Image and
Load Video previews switching to a missing-media error after media
reselection.
Linear: FE-1239
## Changes
- **What**: Keep the current single-select value, close the dropdown,
and restore trigger focus when the selected item is chosen again.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
The behavior is implemented in the shared `FormDropdown` selection
handler, so it applies consistently to all single-select dropdown
consumers rather than special-casing media widgets. Multi-select
toggling remains unchanged.
## Red-Green Verification
- `383576446` adds the unit and Playwright regressions without the
production fix. CI Unit run `29196142841` failed on the new
empty-selection assertion, proving red.
- `d4ae9eb2c` adds the production fix after the red result was
confirmed. CI Unit run `29196531028` passed the same Vitest suite green.
## Test Plan
- Unit regression verifies no selection update is emitted and the
dropdown closes with focus restored.
- Playwright regression reselects `example.png` in Load Image and
verifies the menu closes, the value remains selected, and no load error
appears.
- Targeted Playwright regression passed 10 consecutive local runs with
the production fix applied locally.
## Screenshots (if applicable)
Before
https://github.com/user-attachments/assets/c3724e16-04fc-4b88-bd92-87004db71596
After
https://github.com/user-attachments/assets/c99924f1-e446-4eca-aef7-cb8df961ab22
## Problem
The animated logo in the site footer (a webp frame sequence drawn to a
`<canvas>`) renders with dull, darkened colors. The brand neon yellow
`rgb(242, 255, 90)` shows up as a muddy olive `≈ rgb(198, 209, 80)`, and
the gray/pastel faces are darkened and purple-tinted.
## Cause
The footer `<canvas>` carried an `opacity-80` Tailwind class. At 80%
opacity the browser composites the animation 20% over the dark purple
footer background (`rgb(33, 25, 39)`), shifting every color. The source
webp frames themselves are correct — the shift only happens at display
time.
## Fix
Remove `opacity-80` from the canvas in `SiteFooter.vue` so it renders
the authored frame colors at full opacity.
## Verify
- Pre-check (no deploy): in DevTools, select the footer `<canvas>` and
untick `opacity: 0.8` — colors pop back immediately.
- After change: sample a yellow cube face in the footer animation with a
color picker — it should read `#F2FF5A` / `rgb(242, 255, 90)`, not
`rgb(198, 209, 80)`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds marquee (rubber-band) multi-select and Ctrl/Cmd+A select-all to the
Media Assets panel, clips the canvas drag-selection rectangle to the
canvas panel, and turns on live (real-time) node-graph rubber-band
selection by default.
## Changes
- **Marquee select** — rubber-band drag from empty grid space selects
the covered cards; hold Ctrl/Cmd to start a marquee from over a card;
Ctrl/Cmd or Shift alone makes the marquee additive to the current
selection, Ctrl/Cmd+Shift subtracts the covered cards from it
(designer-approved), and no modifier replaces it. Cards update their
selected state live during the drag.
- **Ctrl/Cmd+A** — selects all loaded assets when the pointer is over
the panel, otherwise falls through to the canvas (select all nodes). It
`stopImmediatePropagation`s so a panel select-all never also fires the
global node select-all, and it yields while an `aria-modal` dialog is
open or a text input is focused.
- **Select-all recovers after "deselect all"** — the shortcut was gated
only on `useElementHover`, which latched stale when the floating
selection bar under the cursor unmounted on deselect. It now also checks
the live pointer position against the panel rect, so a second Ctrl/Cmd+A
right after deselecting no longer falls through to the browser's native
page select-all.
- **Canvas rectangle clip** — the canvas drag-selection rectangle is
clamped to the canvas panel bounds (`SelectionRectangle.vue`,
display-only).
- **Graph live selection on by default** — flips the existing
`Comfy.Graph.LiveSelection` setting's default to on, so node-graph
rubber-band selection updates in real time during the drag (matching the
assets panel) instead of committing only on mouse-up. The behavior was
already implemented behind the setting; this changes only the default,
and users with an explicit value keep it.
- **Robustness/UX** — the pointer is captured on drag-engage rather than
on press (so a Ctrl/Cmd-click on a card isn't hijacked); no global
`document.body.userSelect` mutation (replaced by a panel-scoped
`selectstart` guard); the marquee overlay uses the semantic
`primary-background` token; post-drag click-suppression auto-resets so a
cancelled drag can't swallow a later click; `setPointerCapture` is
wrapped in try/catch; a Ctrl/Cmd-held card `dragstart` is cancelled so
no native ghost-drag image appears.
- **Breaking**: none — `useAssetSelection` is extended additively (new
`setSelectedIds` helper, nothing removed or altered) and the new
composable exposes only `{ marqueeStyle }`.
- **Dependencies**: none.
## Review Focus
- **`SelectionRectangle.vue`** is shared canvas code; the change is
display-only (clamps the rectangle to the panel; no node-selection
behavior change).
- **`coreSettings.ts`** — a one-line `Comfy.Graph.LiveSelection` default
flip is the only change that affects graph behavior; the live-select
code path itself is pre-existing.
- **`useAssetGridSelection.ts`** — listener lifecycle/teardown, the
panel-scoped `selectstart` guard, the click-suppression timer, the
capture-on-drag-engage logic, the pointer-position select-all fallback,
and the subtractive-mode snapshot at pointerdown.
- **Ctrl/Cmd+A routing** — panel hover (or a live pointer inside the
panel) gates select-all vs. the canvas, and `stopImmediatePropagation`
prevents double-handling.
- Pure geometry/selection logic is extracted into
`marqueeSelectionUtil.ts` and unit-tested in isolation (`RectEdges` is
`Pick<DOMRect, ...>`, the DOM edge subset); `MediaAssetCard.dragStart`
keeps `main`'s `display_name` payload.
Relates to Linear **FE-910**.
## Testing
- **Unit:** `useAssetGridSelection` (39 cases — marquee selection,
additive/replace, subtractive Ctrl/Cmd+Shift (incl. the macOS Cmd
variant and a shrink-restore drag), interactive-element + list-view
guards, `selectstart` scoping, click-suppression auto-reset,
pointer-capture-throw and capture-on-drag-not-press, modal-aware
Ctrl/Cmd+A, non-propagation, and the deselect-recovery pointer-in-panel
path), plus `MediaAssetCard`, `marqueeSelectionUtil` (11 cases incl.
subtractive, and a 5-case fast-check property suite pinning the
additive/subtractive set invariants), `SelectionRectangle`,
`useAssetSelection`, and `mathUtil`.
- **E2E (`assetsSidebarTab.spec.ts`):** 10 Playwright scenarios running
in CI — Ctrl/Cmd+A hover vs. canvas; a marquee from the panel header; a
modifier-held additive marquee; a Ctrl/Cmd+Shift subtractive marquee;
Ctrl/Cmd-drag from a card and within a single card; Ctrl/Cmd+A ignored
in a focused search box and under an aria-modal dialog; and a drag from
the search box not marquee-selecting. The empty-space marquee path is
covered by the panel-header scenario plus the unit suite (a dedicated
empty-space e2e could not run headless without a local backend and was
dropped as redundant).
## Future work
- **Escape key** — not handled by the marquee/select-all flow yet (the
composable handles only Ctrl/Cmd+A). Follow-up: press Escape to cancel
an in-progress marquee drag (abort the rubber-band and restore the
pre-drag selection) and to clear the current selection while the panel
has focus.
- **Ctrl+A across pagination** — select-all covers the loaded assets
only (confirmed as the intended behavior with design); a
load-all-then-select variant can follow if needed.
## Demo
https://github.com/user-attachments/assets/3841bf3c-db75-4229-a5e7-fb363b4882d6
## Summary
Fixes the legacy personal and legacy workspace pricing dialogs so Reka
owns the dialog width and the pricing table no longer overflows the
default 576px frame.
## Changes
- **What**: Replace the shared PrimeVue-only `style` and `pt` dialog
props with Reka `renderer`, `size`, and `contentClass` props for both
legacy pricing paths.
- **What**: Preserve `modal: false` for the legacy workspace path so its
teleported PrimeVue plan-details popover remains interactive.
- **What**: Add unit coverage for both routes and assert that ignored
PrimeVue shell props are no longer passed.
## Review Focus
- **Regression origin**:
[#12593](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12593) made
Reka the default dialog renderer.
[#12666](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12666) then
added shared PrimeVue `style` and `pt` props for these pricing dialogs.
Reka ignored those props and fell back to `size="md"` (`max-w-xl`,
576px).
[#13092](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13092) fixed
the unified pricing path only and explicitly left the two legacy paths
for follow-up.
- **Sizing ownership**: The fix puts width on the Reka dialog frame with
`size: 'full'` and `sm:max-w-7xl`. Pricing content no longer has to
compensate for a narrow shell.
- **Legacy workspace behavior**: `modal: false` remains intentional
because the legacy table opens a PrimeVue popover teleported to `body`.
- **Scope**: This PR contains only global subscription-dialog sizing.
Agent side-panel behavior remains in
[#13472](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13472).
- **Validation**: Focused tests pass (41 tests), `pnpm typecheck`
passes, and targeted ESLint passes. Chrome validation at 1352x705
rendered a 1280px dialog with no horizontal overflow in both the
isolated PR preview and the combined agent-panel preview. The docked
agent panel remained mounted behind the modal.
## Screenshots (if applicable)
- Original report and screenshots: [Slack
thread](https://comfy-organization.slack.com/archives/C0A8Z4U7Y1K/p1783967281397449?thread_ts=1783966866.227439&cid=C0A8Z4U7Y1K)
## Problem
`scripts/cicd/resolve-comfyui-release.ts` derived the release target
purely from ComfyUI's `requirements.txt` pin and **hardcoded major
version `1`** (`core/1.${minor}`, `v1.${minor}.*`,
`1.${minor}.${patch}`) even though it parsed `major` and never used it.
Consequences:
- Could not release an out-of-cadence branch (e.g. skip a dead 1.46 to
ship 1.47 directly).
- Could not do a major bump (2.x).
## Changes
1. **Resolver (`resolve-comfyui-release.ts`)** — uses the parsed
`targetMajor` (defaults to the current pin's major) for every
branch/tag/version string instead of literal `1`. `getLatestPatchTag`
now takes a `major` param and globs `v${major}.${minor}.*`.
2. **`TARGET_BRANCH` env override (highest precedence)** — when set,
validates `^core/(\d+)\.(\d+)$`, verifies `origin/<branch>` exists, and
skips the `RELEASE_TYPE`/pin-derived selection entirely. If both
`TARGET_BRANCH` and `RELEASE_TYPE` are set, the override wins.
`current_version` still comes from the pin (for `diff_url` and the
ComfyUI PR "from" version), so the requirements bump jumps straight from
the pin to `target_version` (e.g. 1.45.20 → 1.47.8, skipping 1.46).
3. **Workflow (`release-biweekly-comfyui.yaml`)** — new optional
`target_branch` `workflow_dispatch` input, wired into the resolve step's
`env` as `TARGET_BRANCH`, and surfaced in the run summary. Downstream
jobs consume `target_branch`/`target_version` outputs unchanged.
The output JSON shape is identical. Extracted pure helpers
(`parseTargetBranchOverride`, `computeTargetVersion`) and guarded the
main block so the module is importable by tests.
`release-version-bump.yaml` and `release-branch-create.yaml` were
**already** major-aware and are left untouched.
## Tests
New `scripts/cicd/resolve-comfyui-release.test.ts` (15 cases) covering
`parseRequirementsVersion` (==, >=, missing/absent), `isValidSemver`,
`parseTargetBranchOverride` (valid `core/1.47` and `core/2.0`, malformed
rejected), and `computeTargetVersion` including a non-1 major (`v2.0.3`
+ commits → `2.0.4`).
## Usage
```
gh workflow run release-biweekly-comfyui.yaml --field target_branch=core/1.47
```
releases 1.47.8 directly (skipping a dead 1.46), or `--field
target_branch=core/2.0` for a major bump.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds the data seam for the Settings > Plan & Credits Invoices tab
(FE-1245): derives the next-invoice amount, date, and billing cadence
from billing state already fetched by the billing context, replacing the
prototype's hardcoded mock.
Updated for the 2026-07-13 design decisions (Slack thread + Willie's
Figma updates): the banner surfaces the BE-provided next-invoice date,
and annual subscriptions now show their yearly total and real renewal
date instead of hiding the banner.
## Changes
- **What**: `useNextInvoice` composable + pure `deriveNextInvoice` in
`src/composables/billing/` — returns `{ nextInvoice:
ComputedRef<NextInvoice | null> }`, `NextInvoice = { amountCents,
renewalDate, duration }`
- Monthly: subscribed team credit stop `monthly.price_cents` (status
`team_credit_stop.id` matched against the ladder) with plan
`price_cents` fallback — unchanged precedence
- Annual: stop `yearly.price_cents * 12` (stop yearly prices are
per-month figures, per `useWorkspacePlanPricing`) or the ANNUAL plan's
`price_cents` as-is (already the yearly total, per
`UnifiedPricingTable`)
- `renewalDate`: BE-computed `renewal_date` passed through untouched —
backends own period math including month-end bias. It goes null the
moment a cancellation is scheduled (mutually exclusive with
`end_date`/`cancel_at` on both billing backends)
- null (banner hidden) when inactive, cancelled, the amount is
unresolvable/non-positive (covers legacy billing's empty plan list and
free tier), or the plan resolved by slug disagrees with the
subscription's cadence
- 13 unit tests cover all branches, including x12-regression fixtures
with discriminated list/discount prices
Intentionally excludes usage/overage pending charges until the backend
exposes an authoritative upcoming-invoice amount (see FE-1245).
Follow-ups (not this PR):
- Cancelled-state toast (Figma 4617:29298 month-remaining / 4617:29992
terminal): separate seam UI work. Data is already available —
`subscription.endDate` is populated by both billing backends exactly
during the cancelled-but-paid window, and `useResubscribe()` covers the
Renew plan action
- "/year" amount framing is shown with placeholder copy; no annual
variant exists in the Figma file yet (design follow-up)
## Review Focus
- Annual unit semantics: stop `yearly.price_cents` is a per-month figure
(`useWorkspacePlanPricing.ts` `teamMonthlyCostCents`), while
`Plan.price_cents` for ANNUAL plans is the yearly total
(`UnifiedPricingTable.vue` `getAnnualTotal`) — hence x12 in one branch
and as-is in the other
- Consumed by the `useWorkspaceInvoices` seam once #13591 lands; the
swap now also requires the seam template to render
`renewalDate`/`duration`, so it is no longer a one-line body change
- FE-1245 / DES-497
## Screenshots
Live captures through the real `WorkspaceInvoicesContent` + this
composable with `/api/billing/*` mocked per state (isolated preview
harness). Date format follows the existing billing convention; "/ year"
framing is placeholder pending the annual design variant.
Active monthly subscription, team credit stop `team_320` — $320 USD with
next-invoice date:

Active monthly subscription without a credit stop — plan-price fallback,
$20 USD:

Annual subscription — banner now shown: yearly total from the per-month
stop figure (288 x 12 = $3,456) and the real yearly renewal date:

Paused subscription — unchanged: next-invoice banner hidden, the paused
banner hosts the Full invoice history action (capture from the earlier
full-app harness):

## Summary
On Cloud, `releaseStore.currentVersion` sourced `cloud_version` (e.g.
`0.160.1`) while the `/releases` feed keys its entries by **ComfyUI**
version (e.g. `0.27.1`). The what's-new popup compares the latest feed
entry against the running version, so `0.27.1 < 0.160.1` read as
"already ahead of the latest release" and the popup never showed.
- Analytics confirmed the regression: `release_note` clicks fell from
**13.4% (90d) → 0% (30d)**; `cloud_release_note` was effectively never
clicked.
## Change
- `currentVersion` always uses `comfyui_version` (drops the `isCloud →
cloud_version` branch). The feed request keeps `project: 'cloud'`.
- Rationale: the changelog page and each note's "learn more" link are
ComfyUI-versioned, and Cloud has no separate versioned feed or changelog
page. The changelog is maintained with ComfyUI versions, updated after
each Cloud deploy lands.
## Tests
- Adds a regression test (`isCloud environment (FE-1237)`) pinning that
Cloud uses `comfyui_version`, not `cloud_version`. Verified via negative
control: reverting the fix fails it with `0.160.1` vs expected `0.27.1`.
- `test:unit` (releaseStore): 49 passed · `typecheck`, `lint`,
`format:check`, `knip`: clean.
## ADR
Adds `docs/adr/0012-cloud-release-notes-use-comfyui-version.md`
(Accepted) recording the rationale and history, and updates the ADR
index.
Fixes FE-1237
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Defense-in-depth fix for the `Invalid linked format` crash: escape
vue-i18n message-syntax characters at node-def generation so the locale
bundle is always valid, **and** harden the runtime so any compile error
(custom nodes, already-shipped bundles) degrades to the raw message
instead of crashing the app.
This folds in the runtime guard from #13603 (@xmarre) — see credit below
— so the two layers land together.
## Changes
**Layer 1 — generation-time escaping (source data)**
- Add `escapeVueI18nMessageSyntax()` to
`packages/shared-frontend-utils/src/formatUtil.ts`. It escapes every
character vue-i18n's message compiler treats as syntax in text — `@ { }
| %` (verified against `@intlify/message-compiler`'s `readText`
tokenizer): `@` and unbalanced `{`/`}` throw, `|` mis-renders as plural
branches, `%` matters immediately before `{`. Each becomes a literal
interpolation `{'x'}` (the only escape vue-i18n supports); the set isn't
exported by the library so it's hardcoded with source/doc references.
Single-pass, applied once.
- Apply it in `scripts/collect-i18n-node-defs.ts` to the fields
**compiled** via `t()`/`st()`: `display_name`, `description`,
input/output `name`, widget labels, `dataTypes`, `nodeCategories`.
Tooltip fields go through `tm()`/`stRaw()` (no compile step) and are
intentionally left unescaped.
- Regression test (`src/locales/escapeNodeDefI18n.test.ts`) compiles the
escaped output for all five characters through real vue-i18n and asserts
it round-trips verbatim.
**Layer 2 — runtime fallback (consumer), folded in from #13603 by
@xmarre**
- `st()` now wraps `t(key)` in try/catch: a vue-i18n `SyntaxError` falls
back to the raw locale message (`tm()`) instead of crashing bootstrap.
Shared `rawTranslationOrFallback` helper reused by `stRaw()`. Tests in
`src/i18n.safeTranslation.test.ts` (isolated per-test, and covering the
non-`SyntaxError` rethrow branch).
**Tooling**
- Lint the collection scripts instead of ignoring them: import the
shared helper via the `@/utils/formatUtil` tsconfig alias (fixes
`import-x/no-relative-packages`; verified it resolves under Playwright)
and drop a stray progress `console.log`. No lint config changes and no
rules relaxed — the scripts are now genuinely linted (they were
previously excluded).
- **Breaking**: none.
## Review Focus
- **Root cause**: values read via `t()`/`st()` are compiled by vue-i18n,
which parses `@ { } | %` as message syntax; a malformed `@` throws
`Invalid linked format`. `app.ts` translates every node description at
startup, so one bad string aborts router load. Surfaced by the 1.47.7
locale sync (#13246), whose `ByteDanceSeedAudio` description contains
`@Audio1-3`.
- **The two layers compose**: escaped output compiles cleanly, so the
runtime catch never fires for generated strings; the catch is the safety
net for strings that bypass generation (custom/third-party nodes via
`mergeCustomNodesI18n`, and locale bundles already shipped in
1.47.x/1.48.x).
- **Compiled vs. raw boundary**: escaping is applied only to
compiler-consumed fields; leaving tooltips raw matches the
`stRaw()`/`tm()` work in #12469.
- **Translation propagation**: lobe-i18n's config prompt already keeps
`{...}` placeholders intact, so the escaped English propagates to all 13
locales.
- **Follow-up (out of scope)**: `scripts/collect-i18n-general.ts`
(commands/settings/menus) shares the same generation-time hazard and
could reuse the same helper later; Layer 2 already covers it at runtime.
## Credit
The runtime fallback (Layer 2) is @xmarre's work from #13603, folded in
here with authorship preserved on the original commits. Closing #13603
in favor of this combined PR.
Fixes#13545Fixes#13575
---------
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: xmarre <54859656+xmarre@users.noreply.github.com>
## Summary
Use the selected app locale for queue timestamp language and punctuation
while honoring the browser/system 12-hour or 24-hour clock preference.
## Changes
- **What**: `formatClockTime` resolves the hour cycle from the
browser/runtime default locale, then applies it while formatting with
the app locale.
- **What**: Added behavioral unit coverage for the production
two-argument path and explicit 12-hour and 24-hour preference locales.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
Queue timestamps already use the browser's local timezone. This PR fixes
only the remaining clock-preference mismatch: app language stays in
control of the rendered time string, while the browser/system locale
controls 12-hour versus 24-hour display.
No Playwright regression test was added because browser/system
hour-cycle preference is environment-level Intl state, not deterministic
UI state that the E2E suite can set reliably. Unit tests pin both hour
cycles through BCP-47 locale preferences and assert the rendered
behavior directly.
Fixes
[FE-252](https://linear.app/comfyorg/issue/FE-252/bug-queue-progress-times-ignore-browsersystem-1224-hour-preference)
## Screenshots (if applicable)
Not applicable.
---------
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
## Summary
Backend validation errors (`node_errors`) are reported against the
**flattened** prompt, so an error whose real fix lives on a subgraph
host node gets attached to an interior node the user may never open —
and, after ADR 0009, often *cannot* meaningfully fix there. This PR
re-surfaces a validation error onto the subgraph host node **when — and
only when — the error's subject (the specific input/widget named by
`extra_info.input_name`) is exposed through the subgraph boundary**.
## Why this is needed
Two concrete situations motivated this, both observed in real workflows:
1. **Broken link at the host.** Root node A should feed subgraph host B,
whose boundary input is linked to interior node C. If the A→B link is
missing, the backend flattens the prompt, sees C with no resolved input,
and raises `required_input_missing` **on C** (`"B:C"`). The actual fix —
connect B's input — is one level up, on a node the error never points
at.
2. **Host-owned widget values.** Per ADR 0009 (subgraph promoted widgets
use linked inputs), a promoted widget's value is owned by the host
`SubgraphNode`; the interior widget only supplies schema/defaults. When
the backend raises `value_not_in_list` (or min/max violations) for that
value, attributing it to the interior node is factually wrong — the
value that failed validation *is the host's value*.
This continues the direction of #13059, which moved **missing-model**
detection identity to `{hostExecutionId, hostWidgetName}` with the
interior path kept as diagnostics. That was possible in the FE pre-scan;
this PR applies the same ownership principle to **backend-received**
errors via a receive-side mapping, since the backend cannot know about
subgraph boundaries in a flattened prompt.
## The rule (design)
> Lift an error from interior node N to host H **iff** N's input slot
named by the error is linked to the containing subgraph's boundary
(`SubgraphInput`). Apply the same test again at H (boundary-by-boundary,
matching ADR 0009's chaining principle) and stop at the first level
where the subject is no longer boundary-linked — that node is where the
user can actually fix it.
The predicate is **structural (boundary exposure), not data-flow**.
### In scope — examples
- `required_input_missing` on interior `"12:5"` whose input is fed by
the boundary → surfaces on host `12`'s input slot (red slot ring on the
host, errors-tab card titled/located at the host, message names the
host's `SubgraphInput.name`).
- `value_not_in_list` / `value_smaller_than_min` /
`value_bigger_than_max` on a promoted interior widget → surfaces on the
host's promoted widget. Nested hosts chain: `"1:2:3"` lifts to `"1:2"`,
and further to `"1"` only if `1:2`'s own slot is boundary-linked too.
- Clearing follows the surface: connecting the highlighted host input or
fixing the host widget clears the underlying interior (raw) error —
range-guarded per target, so a still-out-of-range host value does
**not** clear.
### Out of scope — examples
- **No value-flow ancestry.** All in the root graph: A's widget links to
B, B's to C, and C rejects the value that originated at A → the error
**stays on C**. Following same-graph links to a "root cause" node is
explicitly not this feature.
- Errors without an `input_name` subject, node-level types
(`exception_during_validation`, `dependency_cycle`, image-not-loaded),
and unknown validation types — never lifted. Unknown types stay
node-scoped to match how the error catalog renders them (the shared
`isNodeLevelValidationError` in `executionErrorUtil` encodes this, and
the catalog derives its node-level rules from the same set).
- Runtime execution errors (exceptions during a run) — validation
responses only.
- Interior errors whose input is fed by another interior node — fixable
in place, stay in place.
- Fan-out display dedupe: when one boundary input feeds multiple
interior nodes and the host slot is unconnected, each interior error
lifts to the same host slot as a separate panel line. A single fix
(connecting the host input) clears all of them — the clearing
translation already fans out — so the duplication is cosmetic.
Display-level dedupe is a follow-up; deduping inside the lift would
break the one-source-per-error clearing contract.
- Reactive re-lifting on graph topology edits while errors are displayed
(invariant documented on the computed; follow-up), and deriving the
error catalog's full validation rule table from the shared
classification (follow-up; the node-level type set and the
image-not-loaded predicate are already single-sourced in
`executionErrorUtil` and consumed by both the lift and the catalog).
## Changes
- **What**: New pure module
`core/graph/subgraph/liftNodeErrorsToBoundary.ts` — per-error, fail-open
record transform (unresolvable ids/slots/links leave the error where the
backend put it; raw payload is never mutated). `executionErrorStore`
derives `surfacedNodeErrors` from it and display consumers switch over
(errors tab grouping, canvas node/slot flags, Vue node badges,
app-mode/linear hints); raw `lastNodeErrors` remains the source of truth
for mutation. Host-side clearing translates through the lift's
diagnostics fields (`source_execution_id` / `source_input_name`) with a
per-target range guard.
- **Breaking**: None. No persistence/serialization changes; interior
identity survives as diagnostics metadata only (ADR 0009 language).
## Review Focus
- The lift predicate lives entirely on link topology
(`LLink.originIsIoNode` → `SubgraphInput`) — no
`proxyWidgets`/promotion-store style source authority is reintroduced.
- `clearSlotErrorsWithRangeCheck` now resolves clear targets first and
range-checks each target's raw errors; the lifted-path twin of the
existing range-retention test pins this.
- `useProcessedWidgets` deliberately stays on the raw record: host
promoted widgets already map to interior errors via
`widget.sourceExecutionId`, so an interior widget keeps its local red
hint when the user opens the subgraph (hint layer vs surface layer).
- Unit coverage is carried by the pure module (real litegraph subgraph
fixtures, incl. nested recursion, promoted widgets via
`promoteValueWidgetViaSubgraphInput`, ordering, fail-open/no-mutation);
one e2e pins the user-visible contract (host ring + host slot dot +
interior clean).
## Screenshots
### Before
https://github.com/user-attachments/assets/81e5c4db-515d-4f1f-8f8a-e07ac490510f
### After
https://github.com/user-attachments/assets/2949da06-a049-41c1-a480-98ee28333bf2
## Summary
Auth error toasts in the signup/login flow (e.g. "No account found with
this email…") were never tracked, leaving no analytics visibility into
signup dropoff. This adds a PostHog event for them.
## Changes
- **What**: New `app:user_auth_failed` PostHog event with `{ error_code,
auth_action }`, fired from a `reportAuthFlowError` wrapper in
`useAuthActions` before the existing error toast. Wired to the seven
auth-flow actions (`email_sign_in`, `email_sign_up`, `google_sign_in`,
`google_sign_up`, `github_sign_in`, `github_sign_up`, `password_reset`);
covers both the cloud login/signup pages and the in-app sign-in dialog
since all funnel through the same handler. Non-auth actions (logout,
billing) intentionally stay untracked.
## Review Focus
- PostHog provider only — not added to Mixpanel/GTM/CustomerIo/etc.
- No PII: only the Firebase error code is sent (`'unknown'` for
non-Firebase errors), never the message or email.
- The `useAuthActions` test mock of `wrapWithErrorHandlingAsync`
previously rethrew errors; the real implementation swallows them after
handling. The mock now matches the real contract, and one pre-existing
logout assertion that depended on the rethrow was updated.
Fixes
[MAR-451](https://linear.app/comfyorg/issue/MAR-451/add-tracking-for-signup-flow-errors)
---------
Co-authored-by: Deep Mehta <42841935+deepme987@users.noreply.github.com>
## Summary
Switch the meta title separator sitewide from an em dash (—) to a spaced
hyphen ( - ), the follow-up to #13480 tracked as FE-1233, based on the
Screaming Frog crawl that flagged the em dash titles.
## Changes
- **What**: Replaced the em dash separator with a hyphen in every page
meta title, in both `en` and `zh-CN`. Covers ~44 inline page titles, the
shared title keys in `i18n/translations.ts` (`cloudNodes.meta.title`,
`mcp.meta.title`, `affiliate-terms.page.title`,
`enterprise-msa.page.title`, `affiliate.page.title`,
`launches.page.title`), and the dynamic `customers`, `demos`, and
`supported-models` `... - Comfy` suffixes. `BaseLayout` reuses each page
title for `og:title`, `twitter:title`, and the JSON-LD `name`, so those
inherit the change from a single source per page.
- Updated the e2e title assertions to match. `launches` asserts via
`t()` so it follows the translation change automatically.
## Review Focus
- **Scope is titles only.** The crawl and this ticket are about meta
titles. Description / body copy em dashes are left untouched, they are
marketing copy, which we keep em-dashed per the comms-only styling
decision. A full JSON-LD audit across all 57 routes confirms every
`name`/title field is now em-dash-free.
- **zh-CN cloud-nodes title keeps its Chinese double dash `——`**
(`自定义节点包合集——开箱即用`). That is correct CJK punctuation, a dash break, not a
brand/Latin separator, so the Latin-script separator concern does not
apply. Its e2e assertion is left as `——` to match.
- No visual change: titles are `<head>` metadata, not rendered UI, so
there is nothing to screenshot on the page. Rendered `<title>` output
was verified on the dev server across inline, `t()`-based, zh-CN,
dynamic, and 404 routes, and `og:`/`twitter:` inheritance was confirmed.
Ref: FE-1233
## Summary
App mode now uses the same sidenav as the graph view instead of its own
custom navigation, and switching between modes happens through a new
animated graph/app toggle shared by both views.
## Changes
- **What**:
- LinearView hosts the standard `SideToolbar`, filtered to the Assets
and Apps tabs via new `visibleTabIds`/`forceConnected` props. The
bottom-panel and shortcut toggles hide in app mode, and
GraphCanvas/SubgraphBreadcrumb hide their instances so only one toolbar
and toggle render at a time.
- The workflow actions dropdown trigger is now a two-segment graph/app
toggle: the inactive segment switches modes directly, the active one
opens the actions menu as before, with a FLIP animation on switch.
- Each view renders its own toggle, so switching unmounts one and mounts
the other. A `displayLinearMode` ref in canvasStore trails `linearMode`
by two frames so the incoming toggle animates from the old mode instead
of popping in already-switched.
- AppModeToolbar slims down to the toggle plus a "Build an app" button;
the per-icon Assets/Apps/Share buttons are covered by the sidenav.
- The Apps tab gains "Create" header and empty-state actions wired to
`Comfy.NewBlankWorkflow`.
- App-mode feedback moves into `SidebarHelpCenterIcon` as a Typeform
popover with a load-error fallback, replacing `TypeformPopoverButton`
and `LinearFeedback` and simplifying LinearPreview to a single
`OutputHistory`.
- Tests for the toggle, sidebar tabs, help-center fallback, preview, and
view; screenshots regenerated.
## Review Focus
- The two-frame `displayLinearMode` lag — timing-sensitive; rapid
toggling cancels the pending frame chain so the wrong mode never
flashes.
- `SideToolbar` now serves both views: confirm only one instance renders
and the new props behave.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/9ad1eb4f-9370-4975-b3f1-b271482f2bf7
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## ELI-5
Some cloud providers hand you an API key you type in a box; others (like
Google Vertex) hand you a whole JSON file. The provider list already
comes from the server. This teaches each provider to also say *how* its
credential is entered. When a provider says `json_file`, the "Add
Secret" form now shows an **Upload JSON file** button plus a paste box
instead of the single-line password field. Everything else works exactly
as before.
## Summary
Extends the server-driven secret provider schema with optional
`input_type` (`'text' | 'json_file'`) and `label`, and renders a
file-upload + paste textarea for `json_file` providers (e.g. a Vertex
service-account JSON) instead of the password field.
## Changes
- **What**:
- `SecretProvider` ingest schema gains optional `input_type` and `label`
(added to the generated `types.gen.ts` / `zod.gen.ts` — see Review
Focus).
- `listSecretProviders()` now returns the full provider objects (id +
optional `input_type`/`label`) instead of just ids; `availableProviders`
carries them through to the form.
- The credential field switches on the selected provider's `input_type`:
`json_file` → upload button (reads the file into the value) + paste
textarea with light "must be valid JSON" validation; anything else → the
existing password field.
- Provider display label prefers the server `label`, falling back to the
frontend registry label, then the raw id.
- New i18n keys under `secrets.*`.
- **Breaking**: none. Providers that omit `input_type` render exactly as
today (single-line secret). Read/List responses are untouched — the
credential value is still never echoed back.
## Review Focus
- **Judgment call — one provider, credential kind inferred, no toggle.**
Per the design, a single provider is kept and the raw credential value
is sent as-is; the credential kind (AI Studio key vs Vertex SA JSON) is
inferred server-side. The design allowed "inferred from the upload *or*
an explicit toggle"; I chose inference to keep the UI minimal and avoid
introducing a new user-facing gating control. No provider ids are added
or renamed, so the proxy's 1:1 path→provider mapping is unaffected.
- **Generated-file edit (bridge).** The `input_type`/`label` fields were
hand-applied to the generated
`packages/ingest-types/src/{types,zod}.gen.ts`. The upstream
`openapi.yaml` (the generation source) is not in this repo, so this
models the wire contract additively until the backend spec lands the
same optional fields; a future regeneration reproduces them. The fields
are optional, so nothing breaks if the server omits them.
- **No secret ever echoed back.** Only the create request carries the
value; GET/List responses (metadata-only) are unchanged.
Tests: unit coverage added/updated for the provider metadata
passthrough, `selectedInputType`, server label precedence, file loading,
JSON validation, and the form rendering both branches. Targeted vitest
(79 secrets tests), `pnpm typecheck`, eslint, and oxfmt all green.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Adds an accent NEW badge next to top-level header nav items (Products,
Community) on both desktop and mobile.
## Changes
- **What**: New `badge?: 'new'` field on `NavItem`, rendered as an
accent `Badge` in `HeaderMainDesktop.vue` and `HeaderMainMobile.vue`;
adds a `xxs` badge size and drops hardcoded padding/size from the
`accent` variant so it composes with size.
- **Tests**: e2e coverage asserting the badge shows only on
Products/Community, not Company/Pricing.
## Review Focus
- The `accent` badge variant now inherits padding/font-size from `size`
instead of hardcoding them — verify existing `accent` usages still
render correctly.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Follow-up to #12052. The local wheel guard added in FormDropdownMenu was
cancelling native scroll on slow macOS trackpad gestures, leaving image
rows blank in the asset picker until a fast scroll re-pumped samples
through.
## Changes
- **What**: `FormDropdownMenu.onWheel` now only `preventDefault`s on
pinch-zoom (`ctrl/meta + wheel`). The `|deltaX| > |deltaY|` branch of
`isCanvasGestureWheel` is no longer used here.
- **Why**: Slow vertical trackpad scrolls emit small-delta frames with
stray horizontal jitter (e.g. `deltaY=1.2`, `deltaX=1.5`) that satisfied
the horizontal-dominant predicate. Cancelling those frames starved
`VirtualGrid`'s throttled `useScroll`, so `Math.floor(scrollY /
itemHeight)` never advanced and tiles never mounted.
- Horizontal-swipe-to-navigate is still blocked at the page boundary by
`overscroll-behavior: none` on `html, body` (the actual fix from
#12052), so dropping the local horizontal guard does not reintroduce the
back/forward bug.
- The global `isCanvasGestureWheel` is unchanged —
`useCanvasInteractions` still uses it for the canvas wheel handler,
where the semantics are different (forward to canvas pan, not
preventDefault native scroll).
## Review Focus
- Confirm `overscroll-behavior: none` on `html, body` is sufficient on
its own for the horizontal-swipe-to-navigate case inside the teleported
dropdown. (It is — overscroll-behavior is decided per scroll container
at gesture start, independent of where in the page the gesture
originates.)
- Pinch-zoom in the dropdown still preventDefaults correctly.
Tested on macOS trackpad: slow vertical scroll now loads images
progressively; fast scroll unchanged; pinch-zoom in dropdown does not
zoom the page; horizontal swipe in dropdown does not navigate.
Related: this fixes the slow-scroll repro reported in #bug-dump. The
blank-on-reopen class of bug from FE-535 (#11885) is a different issue.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12334-fix-form-dropdown-stop-cancelling-native-scroll-on-slow-trackpad-wheels-3656d73d3650813a987ffa8d6f0eaa9c)
by [Unito](https://www.unito.io)
## Summary
Browsers cap live WebGL contexts per page, so each Load3D node owning
its own renderer broke once enough 3D nodes were added. All 3D views now
share a single offscreen WebGLRenderer: each view renders into the
bottom-left region of the shared drawing buffer and blits the frame into
its own plain 2D canvas, which has no context limit.
- Add sharedWebGLRenderer (refcounted singleton, grow-only buffer) and
RendererView (per-view canvas + per-view renderer state + blit)
- Per-view renderer state (tone mapping, color space, clear color) is
applied before each view renders; HDRIManager and SceneModelManager
write to it instead of the renderer
- OrbitControls/TransformControls bind to the node container explicitly
- ViewHelperManager renders the axes helper itself so it can position
and scale independently of renderer DOM layout
- RecordingManager records from the view canvas; capture renders at
exact size on the shared buffer as before
- On last view release the context is force-lost and webglcontextlost is
dispatched synchronously so context-loss handlers still run
## Screenshots (if applicable)
before
<img width="2155" height="1527" alt="image"
src="https://github.com/user-attachments/assets/35a7ab8a-28dd-41ba-8349-525bd2e97977"
/>
after
<img width="918" height="430" alt="屏幕截图 2026-07-09 135025"
src="https://github.com/user-attachments/assets/67328577-bd82-4494-921a-6d298442d081"
/>
<img width="1636" height="1509" alt="image"
src="https://github.com/user-attachments/assets/a3be9215-7cd0-427c-8264-1fd964d3ace1"
/>
## Summary
Prototype redesign of the embedded 3D viewer controls into a framed
top/bottom-bar chrome. Proof-of-concept for design exploration — not
intended to merge as-is.
## Changes
- **What**: Replaces the floating viewer controls (`Load3DControls`)
with a new `Load3DMenuBar` framed chrome: a black top bar with a
category dropdown (Scene / 3D Model / Camera / Lighting) and the active
category's actions (labels collapse to icons on narrow nodes), plus a
black bottom bar with Record (left) and fit + export (right). Export is
moved out of the menu into the bottom-right button. Adds a "Clay"
material mode that renders meshes with a flat grey material so geometry
is visible without textures.
- **Breaking**: None — `Load3D.vue` swaps the component and hides its
existing right-side button column behind `v-if="false"`; nothing is
deleted.
## Review Focus
- Design prototype: `Load3D.vue` swaps `Load3DControls` →
`Load3DMenuBar` and hides the existing fit/center/expand/record column.
The original components are left untouched.
- The only changes outside the prototype component are for the **Clay**
material mode, which touches shared engine code: `MaterialMode` type
(`interfaces.ts`), the runtime material switch (`SceneModelManager.ts`),
the capability lists (`MeshModelAdapter.ts`, `ModelAdapter.ts`,
`useLoad3d.ts`), and the i18n label.
- Known prototype gaps: **Record** is a visual toggle only (pulsing dot,
not wired to real recording); **fit** reuses the existing
`handleFitToViewer`; **export** reuses the existing format options.
## Screenshots (if applicable)
Prototype walkthrough captured during development (top-bar chrome,
category dropdown, labeled-vs-icon collapse, Clay material, pulsing
Record). Available on request.
---------
Co-authored-by: PabloWiedemann <PabloWiedemann@users.noreply.github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
## Summary
The template picker's search now surfaces the right template for how
people actually type — abbreviations, typos, multi-word intent, and
non-Latin (CJK) titles — and orders results by real popularity instead
of a fuzzy-match score that was being thrown away. Search and ranking
now behave the same here as they do on the workflow hub.
## Changes
**What**
- Searching for the way people phrase things now works: `t2v`, `i2v`,
`cn` expand to their full modality terms, `img2img`/`v2v` expand to
editing (matching how the catalog tags image/video edit templates),
`flux upscale` and `sdxl lora` match across title/model/tag fields
together, prefixes like `vid` match `video`, and typos like `contorlnet`
still find ControlNet. Versioned names tokenize sensibly, so `wan 2.2`
and `wan2.2` both hit, while `2.5` never blurs into `3.5`.
- CJK titles are searchable. Unspaced Han/Hiragana/Katakana runs are
tokenized into character unigrams and bigrams, so a substring a user
types (`放大` inside `图像放大`, or the single trailing `大`) lands on a match.
Korean and other spaced scripts fall to the normal word tokenizer,
unchanged.
- Fuzzy matching is tighter: a term now tolerates edits up to 20% of its
length (down from a flat threshold), so `contorlnet` still finds
ControlNet but `upscale` no longer fuzzy-matches the shorter, unrelated
`scale`. Short (≤3-char) and digit-bearing terms stay exact.
- Results lead with text relevance. Previously the fuzzy match score was
computed and then discarded, and any active sort re-ordered results by
usage — so the best textual match rarely landed on top. Now relevance is
the authoritative order while a query is active, and when two results
match about equally well, the more-used template wins the tie (dampened
so one runaway-popular template can't dominate).
- The ranking is a stable total order. Scores are bucketed before usage
breaks ties, so a cluster of near-equally-relevant results always sorts
the same way — a naive per-pair "within X%" comparison is intransitive
and makes the order depend on internal input order (it can even shuffle
as you type another character).
- "Popular" ranks by raw usage, matching what the hub and the search
index show. It previously blended in a freshness term that pushed newer,
less-used templates above genuinely popular ones.
- The sort dropdown works during search again: it defaults to
"Relevance" but you can switch to Popular/Newest/etc. to re-order the
results, and your browse sort is restored (and never overwritten by a
search-time choice) when you clear the query.
- Alphabetical sort reads correctly: it sorts by the title shown on the
card, trims stray leading whitespace that used to jump templates to the
top, and groups number-prefixed titles after the letters instead of
ahead of them.
- Filter telemetry now reports the sort the user is actually seeing
(relevance while searching) rather than the persisted browse sort, so
analytics reflect the visible ordering.
- Removed the old runtime Fuse-options override path, which is obsolete
under the new engine.
**Breaking**
None. Existing filters (Model / Use Case / Runs On / distribution),
pagination, and persisted sort settings are unchanged; the relevance
mode is search-only and never persisted.
## Review Focus
- The ranking crux is `rankByRelevanceThenUsage` in
`templateSearchConfig.ts`: relevance is primary, usage only re-orders
results in the same score bucket, and bucketing keeps it a stable total
order. That's the one function to review for correctness.
- The CJK tokenizer (`cjkGrams` / `tokenize` in
`templateSearchConfig.ts`): script-matched so only unspaced scripts are
grammed, and a pure-CJK run relies on its grams (no whole-word token).
Splitting by code point is safe here (these scripts are BMP-only; emoji
are excluded by the run regex).
- Deliberately not touched: the "Recommended" sort keeps its curated
blend (usage + editorial rank + freshness) so it stays distinct from
"Popular"; `vram-low-to-high` remains unimplemented exactly as on main.
## Tradeoffs / notes
- Adds `minisearch` (~18 kB gzip). The template selector is where it's
used; accepted for the search-quality gain (a later change could
lazy-load it if bundle size becomes a concern).
- Bucketing means two results just across a bucket boundary don't
tie-break on usage even when their scores are close — the accepted cost
of a transitive, predictable order (this mirrors how the search index
quantizes relevance).
- `img2img` expands to editing (not literal "image to image") because
the catalog labels those templates "Image Edit" — verified against the
real data.
- CJK bigrams roughly double the token count for a pure-CJK title;
negligible at catalog scale (~550 templates, short titles).
## Testing
Behavioral coverage over the real search paths, not the mocks — the
ranking and tokenizer run against actual MiniSearch output; only the
ranking-store math is mocked. Also verified against the full
~550-template catalog end to end (all query types stable, zero
input-order-dependent orderings).
### Behavior matrix (verified on the real catalog)
| Input / action | Now | Previously |
| --- | --- | --- |
| `img2img` | Image-editing templates (Qwen Image Edit, …) | Matched
every "image" template — intent lost |
| `flux upscale` | Flux upscale templates (matches both terms across
fields) | **No results** (single-field fuzzy couldn't span title + tag)
|
| `sdxl lora` | SDXL templates | **No results** |
| `t2v` / `i2v` / `cn` | Expand to text→video / image→video / controlnet
| Only partial slug hits, if any |
| `vid` (prefix) | Matches `video` templates | Unreliable |
| `contorlnet` (typo) | Finds ControlNet | Often dropped by the strict
threshold |
| `upscale` | Matches upscale titles only | Fuzzy-matched the unrelated
substring `scale` |
| `放大` / `大` (CJK) | Matches `图像放大` and other titles containing the run
| No match — CJK titles were unsearchable by substring |
| `wan 2.2` and `wan2.2` | Both match; `2.5` never matches `3.5` | Space
vs no-space degraded the match |
| Near-tied cluster (e.g. `upscale`) | Stable order every time |
Reordered depending on input order (could shuffle as you type) |
| Query active, "Popular" selected | Best textual match still leads;
usage breaks near-ties | Sort re-ordered by usage, burying the best
match |
| Change sort while searching | Re-orders the search results; relevance
is the default | Sort was locked; dropdown had no effect |
| Clear the search | Restores the browse sort you had before | — |
| "Popular" sort | Orders by raw usage (matches hub / index) | Freshness
blend pushed newer low-usage templates up |
| A–Z sort | Letters first (`ACE…`), number-prefixed titles last
(`3x3…`, `360…`); leading whitespace ignored | Leading-space titles
jumped to the top; numbers sorted before letters |
### Unit tests (81 total, all passing)
- `templateSearchConfig.test.ts` (34) — tokenizer identifier/version
splits, CJK unigram/bigram gramming (and Korean left as a spaced word),
per-term fuzziness (`upscale` ≠ `scale`), abbreviation expansion (incl.
`img2img`→edit intent), prefix + typo matching, AND-then-OR,
literal-before-expansion ordering, relevance>tag>description ranking,
and `rankByRelevanceThenUsage` giving a stable order on an intransitive
cluster.
- `useTemplateFiltering.test.ts` (35) — the `img2img` / `flux upscale` /
`sdxl lora` regressions, relevance-default-on-search,
override-sort-while-searching, browse-sort restore on clear, ephemeral
mid-search sort, telemetry reporting the visible sort, Runs-On filter,
empty-result guard, filters preserving relevance order, and alphabetical
trimming + numbers-after-letters.
- `templateRankingStore.test.ts` (12) — freshness and default-score
(recommended) math.
Gate: `pnpm typecheck`, `pnpm lint`, `pnpm knip` clean.
## Screen Recording (if applicable)
https://github.com/user-attachments/assets/6748a3f7-e69d-44ac-826c-71990c8dce90
## Symptom
When the auto-backport workflow (`.github/workflows/pr-backport.yaml`)
hits a cherry-pick **conflict**, it is supposed to comment on the
original PR telling the author to backport manually. On PR #13359
(backport to `cloud/1.45`, cherry-pick of `d6c582c39` conflicting on
`useSubscriptionDialog.test.ts`) **no comment was posted** — the author
(@huntcsg) got no notification and had to find the failure by digging
into Actions logs. See run
[28616756256](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28616756256/job/84862400420).
## Root cause
The "Comment on failures" step actually ran and reached the `conflicts`
branch — the failure reason *was* populated and the `if:` condition
*was* met. The real failure is at the last line of the loop, under the
step's default `bash -e` shell:
```yaml
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
```
The job log ends with:
```
GraphQL: Unable to create comment because issue is locked (addComment)
##[error]Process completed with exit code 1.
```
PR #13359 is `locked: true`, so `gh pr comment` returns non-zero.
Because the call was unguarded under `set -e`, the step aborted on the
spot: the comment was lost and — critically for the general case — any
remaining failed targets in the loop would also be skipped. The step is
then marked failed with no actionable output on the PR.
(Related PR #13167 "attempt each backport target branch independently"
is still open/unmerged; this is a residual gap in the failure-comment
path.)
## Fix
Wrap every `gh pr comment` call in a `post_comment` helper. On failure
it emits a `::warning::` naming the target, the reason, and the
manual-backport branch (`backport-<pr>-to-<target>`) instead of
aborting, so the loop always attempts a comment for each failed target
and surfaces a clear log message when GitHub refuses (e.g. locked
issue). The conflict comment body now also states the manual backport
branch explicitly.
- YAML: `python3 -c 'import yaml; yaml.safe_load(...)'` passes.
- `actionlint`: no new warnings in the changed step (the 2 pre-existing
`SC2016` notes on the intentional single-quoted `envsubst` var lists are
unchanged).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Minimaps unmounted cleanup blindly restored the callbacks that are
originally captured, even if other systems have chained their own
callbacks onto this, breaking other parts of the system (e.g. vue node
graph manager).
Recreation:
1. Ensure minimap open
2. Enter subgraph
3. Exit subgraph
4. Close minimap
5. Try adding a node/unpackign subgraph/etc <--- broken
## Changes
- **What**:
- only replace callbacks that we own
- else function becomes no-op
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12380-fix-dont-remove-unowned-callbacks-when-cleaning-hooks-on-unmount-3666d73d3650817cbfe0d98ab98528b8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Make CLA more robust by including commit authors in the allowlist even
if they have no GitHub account. This to ensure only PR authors are
required to sign.
## Changes
- **What**: `cla.yaml`
## Summary
Keep the onboarding survey's Back/Submit footer visible on small screens
by scrolling only the question area instead of the whole survey.
## Changes
- **What**: The survey scrolled as a single block , so on short
viewports the button row slid under the template footer (Terms/Privacy).
Now the outer is bounded to its slot and the question wrapper in
`DynamicSurveyForm` scrolls internally with a responsive cap , so option
lists scroll while the footer buttons stay pinned. The step height
animation is unchanged.
## Screen Recording
https://github.com/user-attachments/assets/6d7f6d50-59b8-4dc0-b20e-8f4ca08167c6
## What
Browser half of GTM-93 macOS web→desktop identity stitching: when the
desktop app opens the system browser at cloud login with
`?desktop_login_code=dlc_…`, the frontend redeems that code against the
cloud backend once a Firebase session exists — after **explicit user
approval**.
Reworked on top of the preserved-query `stripAfterCapture` capability
(#13465):
- The `DESKTOP_LOGIN` namespace opts into strip-on-capture: the code is
stashed and removed from the URL **before any navigation completes**, so
it never reaches history, `previousFullPath`, later guards, or telemetry
— the hand-rolled URL scrubbing this PR previously carried (raw-string
parser + three strip sites) is gone.
- `desktopLoginRedemption.ts` is a plain module with a single export,
`installDesktopLoginRedemption(router)`, installed once in `router.ts`'s
cloud block (replaces six per-view/composable trigger sites). Redemption
reads the code only from the stash: per-code state (approval + 2-attempt
transient budget, so a second code gets its own approval and budget),
approval dialog, `POST /api/auth/desktop-login-codes/redeem` with the
raw Firebase ID token (backend route is Firebase-JWT-only), 10s fetch
timeout.
- Triggers: `router.afterEach` (the cloud auth guard settles Firebase
init before navigations complete) plus a lazy watcher on
`authStore.currentUser` for sessions that appear without a navigation
(OAuth-resume error branch, dialog sign-in). One bounded in-page retry
(5s) guarantees an approved sign-in always ends in a success or failure
toast.
- Terminal rejections (400/403/404/409/410) drop the code with an error
toast; transient failures (401/5xx/timeout/network) retry once; budget
exhaustion now surfaces a failure toast instead of dying silently.
## Why
Windows stitches web→desktop at download time via installer stamping;
macOS DMGs can't be stamped, so we stitch at login. The browser is where
both halves meet: the existing `posthog.identify(uid)` merges the web
anon person into the Firebase uid, and the backend emits
`comfy.cloud.identity.login_attributed` (uid ↔ installation_id) at
redeem. The desktop app polls the backend and receives a one-time custom
token — no auth material posted to a desktop loopback server (the
concern that stalled #12983, which this supersedes).
## Security
- **Approval dialog before redeem** — redemption mints the desktop a
sign-in token for *your* account, so a lured click must not be enough
(device-code phishing mitigation). Cancel clears the stash and does
nothing.
- Only the opaque single-use code ever appears in a URL; the tracker
strips it on first sight, pre-navigation, and it is never logged.
## Testing
Vitest, driven through a real router (createRouter/createMemoryHistory,
no vue-router mocks) and the real preserved-query manager: capture/stash
lifecycle, approval gate (no fetch before approve; decline/dismiss
clears; per-code approval), Bearer/body shape, terminal-vs-transient
statuses, timeout abort, bounded in-page retry + failure toast on budget
exhaustion, per-code regressions (second code after
success/decline/exhaustion redeems independently), auth-watcher trigger
(session appearing without navigation), unauthenticated no-op, trigger
coalescing. Typecheck/lint/format clean.
Types are hand-written with a `TODO(@comfyorg/ingest-types)` — the
generated types land automatically once the cloud PR merges and the
type-gen workflow runs.
## Landing order
1. Cloud backend: https://github.com/Comfy-Org/cloud/pull/4736 (until it
ships, redemption never triggers — this PR is inert)
2. #13465 preserved-query strip-on-capture (base of this PR)
3. #13466 global-prompt FIFO queue (runtime dependency: the approval
confirm must settle even if another prompt is open)
4. **This PR**
5. Desktop (activates the flow):
https://github.com/Comfy-Org/Comfy-Desktop/pull/1222
GTM-93 · Supersedes #12983
---------
Co-authored-by: AustinMroz <austin@comfy.org>
- Replace the custom two-subtree focus trap with reka FocusScope; the
spotlight overlay is fully modal so a single-subtree trap suffices
- Focus the primary action via a template ref once it's actionable, keyed
off waitingForTarget, and keep a global Escape-to-skip
- Add an e2e test asserting focus lands on the primary and Tab stays trapped
- Auto-open the assets sidebar tab on the assets step instead of
requiring a click on the Assets button (drops advanceOnTargetClick,
skipIfMounted, the click-through blocker, and the idle pulse)
- Make the tour overlay fully modal and add a Back button to steps
- Cache the top-bar inset instead of reading it every animation frame
- Reset waitingForTarget when a deferred step aborts
- Dedupe the assets-panel coachmark binding in LinearView
- Split triggers into holds/autoOpen: only leaving app mode or the
desktop layout dismisses a running tour, not losing outputs
- Trap focus in the spotlight target only on click-to-advance steps,
matching the pointer blocker
- Skip non-laid-out elements in the focus cycle
- Drop unused CoachId[] support
- Recompress the landing image
Removing md:max-w-[800px] let the DialogContent variant's sm:max-w-xl
(576px) cap the 800px landing card; tailwind-merge does not dedupe
max-w across breakpoint modifiers.
- Convert useCoachmarkTour to onboardingTourStore; replace the
coachmarkController event hook with a direct store.replayTour() action
- End an active tour (without the seen-flag) when its trigger stops
holding, e.g. leaving app mode mid-tour
- Toast an error and report skip_reason telemetry (user / target_timeout /
trigger_lost) when a tour is skipped
- Disable primary buttons while waiting on a deferred target; rename
suspendFocusGuard to waitingForTarget
- Rename useFocusTrap to useCoachmarkFocusTrap; while suspended, leave
Tab to the mounting UI but keep the Escape bail-out
- Only auto-open tours on desktop-width (md+) layouts
- Park the registry's rAF poll while no candidate element is registered,
resuming reactively on registration
- Extract TOUR_SEEN_SETTING and use COACH_IDS constants over string
literals; move scrim/ring/pulse styles to design-system tokens
- Port tour tests to the store, cover Done in the e2e spec
- drop the landing's "Skip for now" override so it falls back to "Skip"
- landing skip already ends the tour and marks it seen (no behavior change)
- unit tests build i18n from the real en locale instead of inline messages
- e2e covers the real auto-open path via a pre-seeded empty seen setting
- trim redundant test comments
- a single sync() replaces the per-hook register/unregister logic;
mounted/updated/unmounted all reconcile desired vs current id
- drops the oldValue bookkeeping and its explanatory comment
- derive step translation keys from a step `name`
(onboardingCoachmarks.<tour>.<name>.*), with te()-checked
primary/skip label overrides falling back to Next/Done/Skip
- move all tour i18n into the engine; TourOverlay and TourSpotlight
now receive translated title/body strings
- replace the landing's required open model with a skip emit and
drop the landingOpen writable computed
- simplify TourOverlay.test.ts mocks with fromPartial
- replace SCRIM_COLOR with Tailwind classes (bg-black/60,
spotlight spread shadow)
- narrow the app-run-button anchor so the spotlight excludes the
run error warning
- key the e2e tour fixture's replay button by tour name
- drop the tour spec's template loading; the test server's default
workflow already populates the graph (locally: pnpm dev:test)
- prune comments that restated code
- clear the spotlight's previous ZIndex entry before re-raising, so step
changes stop leaking entries into the shared modal stacking sequence
- report tour telemetry as the user-visible numbering: 1-based step_number
within counted spotlight steps, omitted for the landing
- seed the e2e seen-tours setting from TOURS so future tours can't
auto-open under unrelated suites
- Drop the ?coach= query param: remove the forced-entry/replay-any override
and delayed force-start; tours now start only via auto-open or an explicit
request. E2E replays via the in-app help button after entering app mode
- Flatten coachmarkController to plain requestTour/onTourRequested exports
instead of a useCoachmarkController composable
- Derive the top-bar safe inset from the --comfy-topbar-height token plus
CARD_GAP instead of hardcoding 56
- Remove the landing Start button's fixed width
- Document the real cause of the landing Escape workaround (global keybinding
preventDefaults Escape before Reka's DismissableLayer dismisses)
- Trim verbose comments in coachmarkRegistry and TourOverlay tests
- Report the timed-out deferred step in skip telemetry by advancing the
step index before ending the tour, instead of logging the prior step
- Make the ?coach= override one-shot: named paths force-start only via the
delayed timer (respecting START_DELAY_MS) and re-entry honors the
seen-flag again, rather than bypassing it all session
- Resolve waitForTarget to false immediately when the signal is already
aborted, so it can't hang until timeout
- Reuse the cached dialog locator in Tour.cardForStep
- Drive Floating UI autoUpdate manually so animationFrame polling runs
only while a deferred target is still moving, then fall back to
scroll/resize listeners instead of polling every frame for the whole step
- Set aria-modal to false on interaction steps where the user must click
outside the card
- Ignore unrecognized ?coach= values; keep `any` as the replay keyword
- Align the spotlight scrim with the landing backdrop (0.62 -> 0.6) to
avoid a dim shift on landing -> spotlight
- Export COACH_IDS and import it in the drift guard instead of a hardcoded
in-sync list
- Clarify why the landing needs an explicit Escape listener (Reka's
DismissableLayer doesn't fire update:open here)
- Test that the started telemetry event omits step_index/coach_id while
per-step events include them
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the hand-rolled card-placement math (resolvePlacement /
cardCorner / clampCardPosition) and the manual target tracking (scroll
listener + useElementBounding + rAF settle loop) with @floating-ui/vue
(offset/flip/shift + autoUpdate); promote it from a transitive to a
direct dependency.
- Keep vertically-centred (leftCenter) cards on-screen with
shift({ crossAxis: true }), and centre the card when its target hasn't
laid out, so a step never renders off-screen or invisible.
- Make targetMounted/waitForTarget layout-aware (poll per frame) so a
deferred target that registers before it sizes resolves only once
measurable, matching what the spotlight actually displays.
- Shrink the spotlight glow (pad 8->4) so it no longer spills onto an
adjacent control the user might click.
- Label the spotlight dialog with aria-labelledby pointing at its heading
instead of a duplicated aria-label.
- Drop the unused isActive option from useFocusTrap.
- Add an e2e guard that walks every app-mode spotlight step and asserts
each card sits fully within the viewport; add unit coverage for
last-step Skip hiding, modal z-index reclaim, and all four onboarding
telemetry stages.
Conflict in LinearControls.vue: main refactored the run-button test id into a
named constant (linearRunButtonTestId, same 'linear-run-button' value), while
this branch added the v-coachmark anchor and a static test id to the same
sections. Kept the coachmark directive and adopted main's :data-testid binding,
dropping the now-redundant static id.
Extract TourSpotlight, rendered only while a spotlight step is shown, so the
geometry/scroll, focus-trap and click-to-advance listeners (plus z-index and the
stall pulse) register for the active step rather than the whole graph-view
session. useCoachmarkTour slims to the state machine; the registry-query helpers
(targetMounted/waitForTarget) move to coachmarkRegistry and useCoachmarkTarget
becomes geometry-only and self-measuring.
Also from review feedback:
- read the ?coach= force in setup, before the immediate auto-open watcher, so an
already-populated app no longer drops a ?coach=any replay
- harden isEntryPath against prototype keys (Object.hasOwn)
- decouple each tour's auto-open condition into useTourTriggers, keeping the
engine tour-agnostic
Trimming the graph-mode anchors earlier removed the templates-button
click that loadTemplate relied on to open the browser; open it via the
Comfy.BrowseTemplates command instead.
Reka's built-in Escape dismissal proved unreliable for this dialog in
e2e (Skip, which sets open=false directly, works); close via the same
model path on Escape so the welcome landing reliably dismisses.
- Track the spotlight target with VueUse useElementBounding instead of a
hand-rolled measure/RAF/ResizeObserver loop; keep a capture-phase scroll
listener so it still follows targets inside scrollable panels
- Fold the spotlight ring into the dim element (CSS outline + box-shadow),
dropping the separate SVG; the idle pulse now animates outline-color
- Remove the unused CoachStep `elevated` field and its plumbing
- Open the app-mode tour when entering an app with linear controls
visible (mode === 'app' && hasOutputs), including when the overlay
mounts into one already (immediate watch)
- Respect the seen-flag, so it won't reopen once completed or skipped;
skip the empty/welcome state and arrange (builder) mode
- Move test target DOM cleanup into afterEach so appended nodes are
removed even if a test fails early
- Open the tour only via the help button or ?coach= param (no auto-open
when entering app mode)
- Drop the loadTemplate step tech and the demo card image — the tour runs
on the user's existing app
- Skip the assets-button step at tour start when the assets panel is
already open; step indicator counts 4 instead of 5
- onboardingTours: use 'auto' placement for assets panel so the card follows the sidebar side
- TourOverlay: clamp no-target card left to the viewport margin so it never goes off-screen
- useCoachmarkTour: catch onPrimary action failures, surface a toast, and only advance on success
- useCoachmarkTour: claim the single-instance guard synchronously so a duplicate mount stays inert
- useFocusTrap: send Shift+Tab from outside the trap to the last item instead of skipping it
- telemetry: drop dead run-button imports left over from the rebase (superseded by getRunButtonTelemetryProperties)
- add blank canvas (demo) and app mode (wip) tours
- overlay with target highlight, landing card, step state handling, step card
- v-coachmark directive updating registry for mount/unmount
- frame settling watcher for animated targets (dialogs)
- focus trap for the target plus the coachmark element
- add telemetry for each step
2026-06-29 14:48:18 -07:00
597 changed files with 49311 additions and 6917 deletions
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
post_comment "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist" "missing branch ${target}"
elif [ "${reason}" = "already-exists" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
post_comment "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed." "already-backported ${target}"
elif [ "${reason}" = "branch-create-failed" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` failed: could not create the backport branch. Please retry or backport manually."
elif [ "${reason}" = "push-failed" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` cherry-picked cleanly but the push failed. Please retry or push the backport branch manually."
description: 'Optional:force a specific release branch, e.g. core/1.47 or core/2.0. Overrides the pin-derived target — use to skip a dead minor or do an out-of-cadence / major release.'
en:'Custom-node packs on Comfy Cloud — supported by default',
en:'Custom-node packs on Comfy Cloud - supported by default',
'zh-CN':'Comfy Cloud 自定义节点包合集——开箱即用'
},
'cloudNodes.meta.description':{
@@ -1845,8 +1845,8 @@ const translations = {
// MCP – Meta
'mcp.meta.title':{
en:'Comfy MCP — Drive ComfyUI from any AI agent',
'zh-CN':'Comfy MCP — 让任何 AI 智能体驱动 ComfyUI'
en:'Comfy MCP - Drive ComfyUI from any AI agent',
'zh-CN':'Comfy MCP - 让任何 AI 智能体驱动 ComfyUI'
},
'mcp.meta.description':{
en:'Comfy MCP exposes the full ComfyUI engine over the Model Context Protocol. Generate images, video, audio, and 3D from Claude Code, Claude Desktop, and any MCP-compatible client.',
@@ -1864,10 +1864,26 @@ const translations = {
'zh-CN':
'Comfy MCP 通过模型上下文协议暴露完整的 ComfyUI 引擎——让你的助手能够接入生态系统、构建工作流,并生成图像、视频、音频或 3D 内容。'
},
'mcp.hero.demoPrompt':{
'mcp.hero.demoPromptMoodboard':{
en:'turn the brief in this email into a 6-up moodboard',
'zh-CN':'把这封邮件里的需求做成六宫格情绪板'
},
'mcp.hero.demoPromptConcepts':{
en:'sketch three concept frames for the launch page',
'zh-CN':'为发布页画三张概念稿'
},
'mcp.hero.demoPromptKeyart':{
en:"match this frame's palette, make the hero key art",
'zh-CN':'匹配这一帧的配色,生成主视觉关键画面'
},
'mcp.hero.demoPromptPbr':{
en:'make a tileable asphalt PBR material, all 5 maps',
'zh-CN':'生成可平铺的沥青 PBR 材质,共 5 张贴图'
},
'mcp.hero.demoPromptUpscale':{
en:'upscale the neon kaiju shot to 4K',
'zh-CN':'把霓虹怪兽画面放大到 4K'
},
'mcp.hero.viewDocs':{
en:'VIEW DOCS',
'zh-CN':'查看文档'
@@ -1876,10 +1892,6 @@ const translations = {
en:'INSTALL MCP',
'zh-CN':'安装 MCP'
},
'mcp.hero.runWorkflow':{
en:'RUN A WORKFLOW',
'zh-CN':'运行工作流'
},
'mcp.hero.demoGenerate':{
en:'GENERATE',
'zh-CN':'生成'
@@ -1897,60 +1909,90 @@ const translations = {
'zh-CN':'放大图像'
},
// MCP – SetupStepsSection
// MCP – SetupSection
'mcp.setup.label':{
en:'GET STARTED',
'zh-CN':'快速开始'
},
'mcp.setup.heading':{
en:'Set up Comfy MCP in three steps',
'zh-CN':'三步完成 Comfy MCP 配置'
en:'Set up Comfy MCP',
'zh-CN':'配置 Comfy MCP'
},
'mcp.setup.subtitle':{
en:'Add Comfy Cloud as a custom connector in Claude, Cursor, Codex, or any MCP-compatible client. Sign in once, and the full ComfyUI toolset is available right in your chat.',
en:'Two ways to connect: ask your agent to install it, or add the server yourself. Sign in once, and the full ComfyUI toolset is available right in your chat.',
en:'Using Claude Code? The Comfy skills plugin adds ready-made slash commands. ',
'zh-CN':'在用 Claude Code?Comfy 技能插件提供现成的斜杠命令。'
},
'mcp.setup.skillsLink':{
en:'View on GitHub',
'zh-CN':'在 GitHub 上查看'
},
// MCP – WhyBuildSection
@@ -1971,9 +2013,9 @@ const translations = {
'zh-CN':'开放协议,\n任意客户端。'
},
'mcp.why.1.description':{
en:'MCP is an open standard, so any MCP-compatible client can connect. Today Comfy supports Claude Code and Claude Desktop, with more clients coming.',
en:'MCP is an open standard, so any MCP-compatible client can connect. Claude Code, Claude Desktop, and Codex sign in with OAuth; every other agent connects with an API key.',
'zh-CN':
'MCP 是开放标准,因此任何兼容 MCP 的客户端都能接入。目前 Comfy 支持 Claude Code 和 Claude Desktop,更多客户端即将推出。'
en:'Turn any ComfyUI workflow into a callable tool. The full power of the engine, driven by your agent.',
en:'Submit graphs, track jobs, and pull outputs back. Save and share workflows, reuse a saved one, or open any run on the ComfyUI canvas — the full engine, driven by tool calls.',
en:'Comfy MCP running a ComfyUI workflow as a callable tool from a chat',
'zh-CN':'Comfy MCP 在对话中将 ComfyUI 工作流作为可调用工具运行'
},
'mcp.tools.4.title':{
en:'Direct any model',
'zh-CN':'直接调用任意模型'
},
'mcp.tools.4.description':{
en:'Kling, Veo, Seedance, Flux, GPT-Image, Nano Banana, and ElevenLabs. Closed partner APIs and open-source models, reached through one set of tools.',
'zh-CN':
'Kling、Veo、Seedance、Flux、GPT-Image、Nano Banana 和 ElevenLabs。封闭的合作伙伴 API 与开源模型,通过同一套工具即可调用。'
},
'mcp.tools.4.alt':{
en:'Comfy MCP directing closed partner APIs and open-source models through one set of tools',
'zh-CN':'Comfy MCP 通过同一套工具调用封闭合作伙伴 API 和开源模型'
},
'mcp.tools.5.title':{
en:'Generate in batches',
'zh-CN':'批量生成'
},
'mcp.tools.5.description':{
en:'Stack a batch on the Queue, track it, and pull back every output. Dozens of runs from a single call.',
'zh-CN':
'将一批任务加入队列,跟踪进度,并取回每一个输出。一次调用即可完成数十次运行。'
},
'mcp.tools.5.alt':{
en:'Comfy MCP stacking a batch on the Queue and pulling back every output',
'zh-CN':'Comfy MCP 将一批任务加入队列并取回每个输出'
},
'mcp.tools.6.title':{
en:'Ship it as an app',
'zh-CN':'作为应用发布'
},
'mcp.tools.6.description':{
en:'Turn any workflow into an app with a shareable URL. Collaborators run it in the browser — only the inputs you expose, nothing to install.',
en:'Comfy MCP turning a workflow into a shareable browser app',
'zh-CN':'Comfy MCP 将工作流变成可在浏览器中分享的应用'
},
// MCP – HowItWorksSection
'mcp.howItWorks.heading':{
@@ -2091,71 +2172,81 @@ const translations = {
'zh-CN':'支持哪些客户端?'
},
'mcp.faq.1.a':{
en:'Claude Code and Claude Desktop today, both signing in with OAuth. Support for more clients is coming.',
en:"For Claude Code, Claude Desktop, or Codex, add https://cloud.comfy.org/mcp as a custom connector or remote MCP server in any client, then sign in when prompted.\nFor clients that don't support OAuth, connect with a Comfy API key. Send the docs https://docs.comfy.org/agent-tools/cloud to your agent and it will figure out the installation for you.",
'zh-CN':
'目前支持 Claude Code 和 Claude Desktop,均通过 OAuth 登录。更多客户端的支持即将推出。'
'对于 Claude Code、Claude Desktop 或 Codex,在任意客户端中将 https://cloud.comfy.org/mcp 添加为自定义连接器或远程 MCP 服务器,然后在提示时登录。\n对于不支持 OAuth 的客户端,请使用 Comfy API 密钥连接。将文档 https://docs.comfy.org/agent-tools/cloud 发送给你的智能体,它会为你完成安装。'
},
'mcp.faq.2.q':{
en:"What's the server URL?",
'zh-CN':'服务器 URL 是什么?'
},
'mcp.faq.2.a':{
en:'https://cloud.comfy.org/mcp — add it as a custom connector or remote MCP server in any client, then sign in when prompted.',
en:'Not for Claude Code or Claude Desktop. They use OAuth. An API key is only needed for headless or CI setups with no browser.',
'zh-CN':
'Claude Code 和 Claude Desktop 不需要,它们使用 OAuth。仅在没有浏览器的无头或 CI 环境中才需要 API 密钥。'
},
'mcp.faq.3.q':{
en:'Do the slash commands work in Claude Desktop?',
'zh-CN':'斜杠命令在 Claude Desktop 中可以使用吗?'
},
'mcp.faq.3.a':{
en:'No. They ship in the Claude Code plugin. Desktop connects to the same MCP server, so the tools work; just ask in plain language.',
en:'Not for Claude Code, Claude Desktop, or Codex. You need a Comfy API key for Cursor, Hermes, and OpenClaw for now. Just copy https://docs.comfy.org/agent-tools/cloud and your agent will figure out the installation for you.',
'zh-CN':
'不可以。斜杠命令包含在 Claude Code 插件中。Claude Desktop 连接的是同一个 MCP 服务器,因此工具可以正常使用;直接用自然语言提问即可。'
en:'In Claude Code, run /mcp, select comfy-cloud, and choose Authenticate. In Claude Desktop, reopen the connector from Customize → Connectors.',
en:"Connecting is free with a Comfy account, and searching models, nodes, and templates doesn't cost credits. Running a generation uses Comfy Cloud credits and needs a subscription or credit balance. Your agent confirms with you before it spends.",
'zh-CN':
'在 Claude Code 中,运行 /mcp,选择 comfy-cloud,然后选择 Authenticate(授权)。在 Claude Desktop 中,从“自定义 → 连接器”重新打开该连接器。'
en:'Add the marketplace and install the comfy-cloud plugin, then run /mcp → comfy-cloud → Authenticate. It adds the connection and slash commands in one step.',
en:'Coming soon. Today, to drive a local ComfyUI, you can use comfy-cli: https://github.com/Comfy-Org/comfy-cli',
en:'Generate images, video, audio, and 3D; search models, nodes, and templates; and run ComfyUI workflows, all from a chat.',
'mcp.faq.6.a':{
en:"• Generate images, video, audio, and 3D — including all open-source workflows and partner models like Seedance, GPT-Image, Nano Banana, and Kling\n• Build, edit, and run workflows; save and re-run workflows\n• Run and read in large batches\n• Search models, nodes, and template workflows\n• Read and execute shared workflow URLs\n• Upload and download assets for you\n\nEverything is now in natural language. No nodes, no downloads, no GPU, no node graphs if you don't want them.",
en:'Into your Comfy Cloud asset library, so you can reuse, remix, and share them — and open any run on the canvas to keep editing. You can also ask your agent to download the assets locally for you.',
en:'Comfy.org Affiliate Program Terms and Conditions.',
@@ -3896,8 +3988,8 @@ const translations = {
'This document reproduces the current template of the Enterprise Customer Agreement for reference only. The executed Agreement between Comfy and Customer, together with any signed Order Forms, governs the relationship between the parties. To request an executable copy, please contact <a href="mailto:sales@comfy.org" class="text-white underline">sales@comfy.org</a>.'
},
'enterprise-msa.page.title':{
en:'Enterprise MSA — Comfy',
'zh-CN':'Enterprise MSA — Comfy'
en:'Enterprise MSA - Comfy',
'zh-CN':'Enterprise MSA - Comfy'
},
'enterprise-msa.page.description':{
en:'Comfy Enterprise Customer Agreement — the master services agreement that governs Comfy Enterprise deployments of Comfy Cloud, Comfy API, and related products.',
@@ -4361,8 +4453,8 @@ const translations = {
// Affiliate page (/affiliates) — head metadata
'affiliate.page.title':{
en:'Comfy.org Affiliate Program — Become a Partner',
'zh-CN':'Comfy.org 联盟计划 — 成为合作伙伴'
en:'Comfy.org Affiliate Program - Become a Partner',
'zh-CN':'Comfy.org 联盟计划 - 成为合作伙伴'
},
'affiliate.page.description':{
en:'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.',
@@ -4387,8 +4479,8 @@ const translations = {
// Launches page (/launches) — head metadata
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'launches.page.title':{
en:'ComfyUI Live Demo & Q&A — June 29 Launch Livestream',
'zh-CN':'ComfyUI 直播演示与问答 — 6 月 29 日发布直播'
en:'ComfyUI Live Demo & Q&A - June 29 Launch Livestream',
'zh-CN':'ComfyUI 直播演示与问答 - 6 月 29 日发布直播'
},
'launches.page.description':{
en:'Join the ComfyUI livestream on June 29 for a hands-on product demo and live Q&A. See what’s new across desktop, cloud, and community, and get your questions answered.',
@@ -4446,6 +4538,161 @@ const translations = {
'launches.section.title':{
en:'Latest Launches',
'zh-CN':'最新发布'
},
// Brand Portal page (/brand)
'brand.page.title':{
en:'Brand — Comfy',
'zh-CN':'品牌 — Comfy'
},
'brand.page.description':{
en:'The Comfy brand portal: logos, color, typography, and voice. Everything you need to build something that looks and sounds like Comfy.',
en:'Logo, color, type, and voice. Everything you need to build something that looks and sounds like us.',
'zh-CN':'标志、色彩、字体与语调。打造与我们观感一致、表达一致所需的一切。'
},
'brand.hero.viewGuidelines':{
en:'View brand guidelines',
'zh-CN':'查看品牌规范'
},
'brand.hero.downloadLogos':{
en:'Download logos',
'zh-CN':'下载标志'
},
'brand.logos.heading':{
en:'One mark, many dimensions.',
'zh-CN':'一个标志,多种维度。'
},
'brand.logos.subheading':{
en:'Logos come in light and dark options. Use as provided. Do not distort, recolor, or outline. Make sure the logo is legible against its background.',
'zh-CN':
'标志提供浅色和深色两种版本。请按原样使用,不要变形、改色或描边。确保标志在其背景上清晰可辨。'
},
'brand.colors.heading':{
en:'Every color earns its place.',
'zh-CN':'每种颜色都各得其所。'
},
'brand.colors.subheading':{
en:'Our color palette helps build brand recognition. When people think of Comfy, we want them to associate it with the following colors.',
'zh-CN':
'我们的调色板有助于建立品牌辨识度。当人们想到 Comfy 时,我们希望他们联想到以下这些颜色。'
},
'brand.colors.copy':{
en:'Copy',
'zh-CN':'复制'
},
'brand.colors.copied':{
en:'Copied',
'zh-CN':'已复制'
},
'brand.voice.heading':{
en:'Precise, never cute.',
'zh-CN':'精准,绝不卖弄。'
},
'brand.voice.direct.title':{
en:'Direct',
'zh-CN':'直接'
},
'brand.voice.direct.body':{
en:'We state things. We don’t hedge, qualify, or suggest. Short sentences. Active voice. One idea at a time.',
'zh-CN':
'我们直陈其事。不含糊、不设限、不暗示。短句。主动语态。一次只讲一个观点。'
},
'brand.voice.precise.title':{
en:'Precise',
'zh-CN':'精准'
},
'brand.voice.precise.body':{
en:'We use the real names for things. Nodes, samplers, seeds, checkpoints. We don’t talk around the product or reach for metaphor when the technical term is already good.',
en:'Comfy runs on your hardware. Nothing leaves your machine.',
'zh-CN':'Comfy 在你自己的硬件上运行。任何数据都不会离开你的机器。'
},
'brand.voice.dont.0':{
en:'Simply connect your AI blocks and watch the magic happen!',
'zh-CN':'只需连接你的 AI 模块,见证奇迹的发生!'
},
'brand.voice.dont.1':{
en:'Oops! Something went wrong. Please try again later.',
'zh-CN':'哎呀!出了点问题,请稍后再试。'
},
'brand.trademark.heading':{
en:'Trademark guidelines.',
'zh-CN':'商标使用规范。'
},
'brand.trademark.body1':{
en:'Comfy and ComfyUI are trademarks of Comfy Org. You’re welcome to reference them in content that accurately describes your work with our platform. Tutorials, reviews, integrations, and affiliate content all qualify.',
en:'A few rules: don’t modify the logo, don’t use the Comfy name in your own product or company name, and don’t present your content in a way that implies official endorsement or partnership beyond what’s been agreed.',
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.