## Summary
Add a `COVERAGE_CRITICAL` unit-coverage gate over folder-based critical
runtime areas and wire it into the unit CI job. First PR of a stacked
series that ratchets the gate upward as tests land.
## Changes
- **What**: `vite.config.mts` gains `CRITICAL_COVERAGE_INCLUDE` folder
globs for core runtime areas: `src/base`, `src/composables`, `src/core`,
`src/schemas`, `src/scripts`, `src/services`, `src/stores`, `src/utils`,
selected `src/platform` logic slices, selected
`src/lib/litegraph/src/{node,subgraph,utils}` primitives, and selected
`src/workbench` manager logic; `package.json` gains
`test:coverage:critical` (`COVERAGE_CRITICAL=true vitest run
--coverage`); `ci-tests-unit.yaml` runs the gate. The thresholds are
env-gated, so the normal `test:coverage` run is unaffected.
- **Breaking**: none.
## Review Focus
Establishes the measurement substrate, no tests added yet. Thresholds
are locked to the current baseline over the folder-based critical scope
so CI is green:
| metric | baseline | threshold |
|---|---|---|
| statements | 69.53% (24287/34930) | 69 |
| branches | 60.7% (11497/18940) | 60 |
| functions | 67.34% (4980/7395) | 67 |
| lines | 70.83% (22619/31930) | 70 |
The scope is intentionally not whole `src/platform`, `src/lib`, or
`src/workbench`: UI-heavy and specialized lanes like platform
components, telemetry/surveys, litegraph
canvas/widgets/infrastructure/types, and manager components/types stay
outside this gate for now.
Subsequent stacked PRs add tests and bump these thresholds; a later
refactor series ratchets branches to 90.
Created by Codex
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Changes are limited to test/coverage configuration and CI; no
application runtime behavior is modified.
>
> **Overview**
> Introduces a **critical-path unit coverage gate** that only runs when
`COVERAGE_CRITICAL=true`, leaving the existing `pnpm test:coverage`
behavior unchanged.
>
> **Vitest** (`vite.config.mts`): when the flag is set, coverage is
limited to folder globs for core runtime areas (base, composables, core,
services, stores, utils, selected platform/workspace/auth slices,
litegraph node/subgraph/utils, workbench manager logic, etc.) and
**Vitest thresholds** are enforced (statements 69%, branches 60%,
functions 67%, lines 70%). In that mode, litegraph is no longer
blanket-excluded from coverage the way the full `src` run still excludes
`src/lib/litegraph/**`.
>
> **Tooling & CI**: adds `test:coverage:critical` in `package.json` and
a new unit CI step after Codecov upload that runs the gate so
regressions in those areas fail the job.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
25e73f3844. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
## Summary
Move the five customer stories on `/customers` out of the old
`customerStories.ts` array and the shared i18n file into an Astro
content collection (MDX, English and Chinese). The pages look and read
exactly the same; this just changes where the content lives so it is
easier to edit, and it sets the pattern we will reuse to migrate the
rest of the marketing content.
Linear: FE-1158
## Changes
- **What**:
- Added a `customers` content collection (`src/content.config.ts` +
`src/content/customers.schema.ts`), with one MDX file per story per
locale under `src/content/customers/{en,zh-CN}/`.
- Rebuilt the article rendering as small static components (`Section`,
`Figure`, `Quote`, `Contributors`, `Steps`, plus styled
paragraph/heading/list). The article body is now static HTML; only the
scroll-spy sidebar (`ArticleNav.vue`) ships JavaScript.
- Repointed the `/customers` listing and detail pages (both locales) to
read from the collection.
- Removed the old `customerStories.ts` array and the customer-story keys
from `translations.ts` (about 1,300 lines).
- Dropped the two "Read more" links that just redirected back to the
same page; kept the two that point to the real Substack articles.
- Switched the "Read more" button to the design-system `Button`, which
also fixes its vertical alignment.
- Added a short pattern doc at `apps/website/src/content/README.md` for
reuse.
- **Dependencies**: `@astrojs/mdx` (renders the MDX content).
## Review Focus
This is meant to be a no-visual-change migration. I checked content and
layout against the live site for all five stories in both languages, on
desktop and mobile. The only intended differences are the two removed
self-referential "Read more" links and the read-more button now using
the shared `Button`.
A few small setup changes explain part of the diff:
- `src/env.d.ts` now references `.astro/types.d.ts` so the collection
types resolve (this is the repo's first content collection).
- `astro.config.ts` sets `markdown.smartypants: false` so quotes stay
straight (MDX would otherwise curl them). This option is deprecated in
Astro 7 and moves onto the markdown processor; that belongs with the
eventual Astro 7 upgrade, not here.
- ESLint ignores the `astro:` virtual modules for `apps/website` files
(they are real at build time, but the resolver cannot see them).
- Content MDX is excluded from `oxfmt` in `.oxfmtrc.json`: the formatter
rewraps component slots and changes the rendered output (it broke the
blockquotes), so content files are kept out of it like generated files
and fixtures.
- `components/common/ContentSection.vue` and `config/contentSections.ts`
are untouched; they still power the legal and privacy pages.
The diff is large, but most of it is MDX content, the lockfile, and the
removed i18n keys. The logic to review is small: the collection config
and schema, the components, and the page wiring.
## Screenshots
No visual change is intended, so before and after of the article pages
are identical (verified across both locales and on desktop and mobile).
The one deliberate tweak is the "Read more" button, which now uses the
design-system `Button` for better vertical alignment. Before/after
captures are available if needed.
## Summary
Brand link, reroute, and slot identifiers through LiteGraph, subgraph,
and layout flows so raw numeric workflow data is converted at boundaries
while runtime APIs keep branded IDs.
## Changes
- **What**: Add canonical `LinkId`, `RerouteId`, and `SlotId` types plus
minting helpers, then re-export litegraph/layout ID types from those
modules.
- **What**: Keep `LinkId`, `RerouteId`, and `SlotId` references branded
across graph links, reroutes, node slots, subgraph slots, link
deduplication, link drop handling, layout storage, and tests.
- **What**: Convert raw numeric IDs only at periphery points: serialized
workflow DTOs, legacy graph link proxy access, copied/pasted graph data,
Yjs/string layout keys, and test fixtures.
- **What**: Move slot layout identity onto branded `SlotId` values using
stable `node:direction:index` ordering, while keeping DOM dataset values
stringified at the boundary.
- **What**: Avoid slot-key scans during link drops by carrying the link
segment identity directly through the drop path.
## Review Focus
- Branded IDs should not be widened back to `LinkId | number` /
`RerouteId | number` in runtime APIs.
- Serialized workflow shapes intentionally remain numeric for
compatibility.
- `_subgraphSlot.linkIds` remains `LinkId[]`; call sites should not
treat it as raw `number[]`.
- `MapProxyHandler` is the compatibility boundary for deprecated indexed
`graph.links[id]` access.
## Validation
- `pnpm typecheck`
- `pnpm test:unit src/lib/litegraph/src/LLink.test.ts
src/lib/litegraph/src/LGraph.test.ts
src/lib/litegraph/src/LGraphNode.test.ts
src/lib/litegraph/src/canvas/LinkConnector.core.test.ts
src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts
src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts
src/lib/litegraph/src/LGraphCanvas.drawConnections.test.ts
src/lib/litegraph/src/node/slotUtils.test.ts
src/lib/litegraph/src/subgraph/ExecutableNodeDTO.test.ts
src/core/graph/subgraph/promotionUtils.test.ts
src/core/graph/subgraph/migration/proxyWidgetMigration.test.ts
src/renderer/core/layout/store/layoutStore.test.ts
src/renderer/core/layout/utils/layoutUtils.test.ts
src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
src/scripts/promotedWidgetControl.test.ts`
- Commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`
- Push hook: `knip --cache`
---------
Co-authored-by: AustinMroz <austin@comfy.org>
## Summary
Block background keybindings from firing while a modal dialog (e.g.
Templates) is open, so typing `w` no longer toggles the workflow sidebar
behind the modal.
## Changes
- **What**: In `keybindingService.keybindHandler`, gate command
execution on `dialogStore.dialogStack`. When a dialog is open, only
keybindings whose event target is inside the dialog (`[role="dialog"]`)
fire; all other matches are dropped.
## Review Focus
- The dialog scope check uses `target.closest('[role="dialog"]')` so
dialog-internal shortcuts still work — confirm PrimeVue/Reka dialogs
render with `role="dialog"` on the wrapper (they do; this is the
WAI-ARIA standard the libraries follow).
- Updated `keybindingService.escape.test.ts` "modifiers regardless of
dialog state" case to the new contract (modifiers also blocked),
matching the team consensus in FE-642 that all keybindings should be
disabled when a modal is open.
- New `keybindingService.dialog.test.ts` covers: no-dialog → fires;
dialog open + target outside → blocked; dialog open + target inside →
fires.
Fixes FE-642
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12184-fix-disable-global-keybindings-while-a-modal-dialog-is-open-35e6d73d3650812fbc5dd5490ccde24f)
by [Unito](https://www.unito.io)
Co-authored-by: Dante <bunggl@naver.com>
## Summary
Add an Upload button to the dropdown popover's filter bar so users can
pick a file without closing the dropdown to reach the small upload icon
next to the input.
The upload button in the dropdown menu includes text and uses the same
icon as the external quick upload button. This design ensures that after
using it, users will understand that the icon on the external button
means upload. Even if users didn't understand it before, they will
correctly interpret it next time.
related linear FE-581
## Changes
- **What**:
- Expose `showPicker()` from `FormDropdownInput`; it calls
`HTMLInputElement.showPicker()` on the single existing hidden `<input
type="file">` (falls back to `input.click()` on browsers without
showPicker).
- Add an Upload button in `FormDropdownMenuFilter` that emits
`show-picker`, bubbled up through `FormDropdownMenu` to `FormDropdown`,
which then calls `triggerRef.showPicker()`. The whole chain runs in the
click event's synchronous stack to satisfy the browser's transient
activation requirement, so no extra `<input type="file">` is added to
the DOM.
- Style the button with the project's standard inverted-button tokens
(`bg-base-foreground` / `text-base-background`) so it tracks theme
changes.
## Review Focus
- The `triggerRef!.showPicker()` non-null assertion in
`FormDropdown.vue` is intentional: by the time `show-picker` is emitted
the trigger is guaranteed to be mounted; a null here would indicate a
real bug we want to surface, not swallow.
- Verify the new button reuses the same upload path as the inline icon
button (single `<input type="file">`, single `handleFileChange`).
## Screenshots
<img width="1304" height="1442" alt="CleanShot 2026-06-02 at 14 39
33@2x"
src="https://github.com/user-attachments/assets/b2d1cdd8-e28a-467d-8142-afd707264d0e"
/>
<details><summary>Old Versions</summary>
<p>
https://github.com/user-attachments/assets/2d64873b-6bec-4eca-aa89-a72dd11aa809
</p>
</details>
## Summary
Model/widget dropdowns stayed open until mouseup, detached from their
node when the canvas moved while open, and needed two clicks to dismiss
after the inner scrollbar took focus.
## Changes
- **What**:
- Dismiss the dropdown on `pointerdown` outside the menu/trigger
(capture phase) instead of PrimeVue's `click` (mouseup) dismissal. The
dropdown now closes the instant a press lands, before a drag or
box-select can start, and a focused inner scrollbar no longer swallows
the first outside click.
- Close the dropdown whenever the canvas viewport moves, by watching the
reactive `useTransformState().camera`. This reacts to the canvas
abstraction layer rather than guessing input intent, so it covers
pan/zoom from any device — mouse drag, trackpad pan, wheel scroll/zoom —
where no `pointerdown` ever fires. The popover is teleported to the
document body and cannot follow the viewport, so closing is the correct
behavior.
## Review Focus
- Box-select and node-drag both begin with a `pointerdown` outside the
popover, so they are covered by the immediate dismissal path; the camera
watch handles pointer-less viewport motion.
- `closeOnEscape` and in-menu interactions are unaffected; presses
inside the menu or on the trigger are excluded via `composedPath()`.
Fixes FE-808
---------
Co-authored-by: Dante <bunggl@naver.com>
*PR Created by the Glary-Bot Agent*
---
Updates two sections on https://comfy.org/terms-of-service per legal
copy provided in [the website-and-docs Slack
thread](https://comfy-org.slack.com/archives/C098QHJ8YDR/p1782775899132369).
## Changes
Edits `apps/website/src/i18n/translations.ts` (the source of truth for
the ToS page rendered by
`apps/website/src/pages/terms-of-service.astro`):
- **`tos.payment.block.1` — Plans; Fees; Free Tier.** Adds language
clarifying that a Free Tier user who provides a payment method expressly
authorizes Comfy to charge it for overages (intentional use, third-party
use, or technical factors), and that approach-to-cap notifications are
best-effort, not a precondition to charging.
- **`tos.payment.block.3` — Self-Serve Credit Card Billing.** Clarifies
that the billing authorization applies to paid Plan and Free Tier
overages alike, and that retry rights for failed charges extend to Free
Tier overage charges.
`en` and `zh-CN` values are kept in sync per the existing convention for
these keys (the `/zh-CN/terms-of-service` page is a redirect to the
English page).
## Open question for legal / requester
`tos.effectiveDate` is currently `May 13, 2026` and was **not** bumped
in this PR — the original request did not mention it. If legal wants
this revision to carry a new effective date, that should be a follow-up
commit on this branch before merge.
## Verification
- `pnpm typecheck` (apps/website): 0 errors, 0 warnings.
- `pnpm build` (apps/website): 497 pages built; the rendered
`/terms-of-service` HTML contains both new sentences.
- `pnpm exec eslint` / `oxfmt --check` on the changed file: clean.
- Husky pre-commit (`lint-staged` + `check-unused-i18n-keys`): clean.
- Manual: served the built `dist/` via local HTTP and verified the
rendered Payment section in a real browser (screenshot below).
## Screenshots

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Let the running ComfyUI server decide which backend the web UI talks to
(and which Firebase project it signs you into), so launching with
`--comfy-api-base` just works with the regular bundled frontend.
## Changes
- **What**: At startup the frontend reads `/api/features` on every build
(not just cloud) and treats the server's `comfy_api_base_url` /
`comfy_platform_base_url` as authoritative, falling back to the
build-time defaults.
When that api base is a staging-tier host (staging, or a
`*.testenvs.comfy.org` preview env) and the server hasn't supplied its
own Firebase config, the frontend picks the dev Firebase project,
derived from the api base.
Production is left exactly as it is today.
- `main.ts`: load remote config first thing, before Firebase
initializes, so every module sees the right values from the first render
- `config/comfyApi.ts`: the api/platform getters now read the server's
values on all distributions
- `config/firebase.ts`: `getFirebaseConfig()` resolves in order: a
server-provided config first (cloud), then the dev project for a
staging-tier api base, then the build-time default
- `platform/remoteConfig/refreshRemoteConfig.ts`: the startup fetch now
has a 5s timeout, so a slow or wedged `/features` can never keep the app
from mounting; on failure we fall back to the build-time defaults
- **Breaking**: None. With no `/features` overrides (production and
ordinary self-hosting), behavior is unchanged
## Review Focus
- The precedence in `getFirebaseConfig()` (`config/firebase.ts`): server
config first, then the staging-tier dev project, then the build-time
default. The staging-tier check matches `stagingapi.comfy.org` and any
`*.testenvs.comfy.org` host, and falls back to build-time for anything
it can't parse.
- Running `refreshRemoteConfig()` unconditionally and first in
`main.ts`, with the new fetch timeout as the safety net.
## Testing
I tested every case by hand, locally, on top of the automated checks.
Tested both with `pnpm run build` and `USE_PROD_CONFIG=true pnpm build`
and running Comfy from that folder.
Pointed a local ComfyUI at each backend with `--comfy-api-base` and
signed in with Google each time:
- **Production** (default / `https://api.comfy.org`): stays on
production and signs into the production Firebase project, identical to
today.
- **Staging** (`https://stagingapi.comfy.org`): follows it and signs
into the dev project.
- **Ephemeral preview env** (`https://pr-<n>.testenvs.comfy.org`): the
friendly host is accepted as-is, the frontend follows it, lands in the
dev project, and Google sign-in completes.
The only exception where fronted does not respect the `--comfy-api-base`
is when Comfy runs against `prod` and frontend runs with the `pnpm run
dev` - due to overridden config(this is expected behavior).
Supersedes: https://github.com/Comfy-Org/ComfyUI_frontend/pull/12560
Companion Core PR: https://github.com/Comfy-Org/ComfyUI/pull/14569
## Screenshots (if applicable)
<!-- Add screenshots or video recording to help explain your changes -->
## Summary
This PR enables native PostHog `$pageview` capture for `cloud.comfy.org`
by setting cloud PostHog `capture_pageview` to `history_change`.
This keeps `autocapture` disabled, preserves the existing custom
`app:page_view` event, and lets the PostHog SDK capture the initial
pageview plus SPA history navigation pageviews. The goal is to make
cross-domain funnel tracking cleaner between `comfy.org` and
`cloud.comfy.org`, since `comfy.org` already emits native `$pageview`
events.
## Why
We want to measure the visitor funnel more accurately across:
- `comfy.org` visits
- `cloud.comfy.org` visits
- signup clicks / signup opened
- signup completion
- first cloud workflow run
- first subscription
- first credit purchase
Using native `$pageview` on both website and cloud should make PostHog
and downstream warehouse/Hex analysis cleaner for trackable users, while
leaving custom app pageview telemetry intact for existing consumers.
## Validation
- `pnpm test:unit
src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts`
- `pnpm typecheck`
- `pnpm lint:unstaged`
- pre-commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`
- pre-push hook: `knip --cache`
Note: local validation printed an engine warning because the Codex
runtime has Node `v24.14.0` while this repo declares `>=25 <26`; the
commands above still passed.
Fixes#13175#12931 slimmed groupNode.ts down to migration-only and dropped the
export on GroupNodeHandler.
ComfyUI-Manager still imports it (import { GroupNodeConfig,
GroupNodeHandler } from "../../extensions/core/groupNode.js" in
components-manager.js), so the legacy shim no longer providing that
export throws "does not provide an export named 'GroupNodeHandler'" at
module load. That kills the whole Manager extension before setup() runs
— which is why the Manager button vanished from the toolbar since 1.47.3
(backend loads fine, frontend JS dies).
Just re-adds the export (class is still there, only the keyword was
lost) plus the existing @knipIgnoreUnusedButUsedByCustomNodes tag since
nothing in src imports it.
Tested by loading with ComfyUI-Manager installed: the groupNode.js
import error is gone and the Manager button shows again.
typecheck/knip/lint pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
Update the `EXPLORE` CTA on the Comfy MCP card on
[/launches](https://comfy.org/launches) to link to
[/mcp](https://comfy.org/mcp) instead of the docs
(`docs.comfy.org/agent-tools/cloud`).
## Change
Single line in `apps/website/src/data/drops.ts`:
```diff
cta: {
label: EXPLORE,
- href: { en: externalLinks.docsMcp, 'zh-CN': externalLinks.docsMcp }
+ href: { en: '/mcp', 'zh-CN': '/zh-CN/mcp' }
}
```
Matches the locale-aware pattern used by sibling cards (`/download` /
`/zh-CN/download`, `/api` / `/zh-CN/api`). Both `src/pages/mcp.astro`
and `src/pages/zh-CN/mcp.astro` already exist, so neither link is dead.
The `externalLinks.docsMcp` constant is retained because the MCP page
itself still uses it.
## Verification
- `pnpm typecheck` / `pnpm typecheck:website` clean.
- `oxfmt`, `oxlint`, `eslint` clean (all ran via lint-staged on commit).
- Manually loaded `/launches` and `/zh-CN/launches` in the dev server
and confirmed the Comfy MCP card now points to `/mcp` and `/zh-CN/mcp`
respectively.
- Loaded `/mcp` and confirmed the destination page renders ("Comfy MCP —
Drive ComfyUI from any AI agent").
- Code review by Oracle: no issues.
Screenshot shows the updated MCP card on /launches.
## Screenshots

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Fix Color Palette changes not getting tracked, requested by design team.
Capture theme changes as `app:setting_changed` telemetry. The only
existing hook lived in `SettingItem.vue`, which renders *visible*
settings; `Comfy.ColorPalette` is hidden and changed through bespoke
theme UI, so it was never tracked.
Open to opinions here, we can also remove the hook in SettingItem.vue,
and just make everything that was visible opt in.
Linear:
https://linear.app/comfyorg/issue/GTM-158/track-theme-usage-with-posthog-events
## Summary
Reworks the Comfy MCP page's **"Set up Comfy MCP in three steps"**
section to match the new design, and adds a per-action button `variant`
option to `FeatureGrid01`.
The three steps are now:
| Step | Title | Action |
| --- | --- | --- |
| 1 | Copy the MCP URL | Copy field showing
`https://cloud.comfy.org/mcp` |
| 2 | Add the connector | Filled button **"COMFY CLOUD MCP DOCS" ↗** →
MCP docs |
| 3 | Connect and sign in | Filled button **"COMFY CLOUD SKILLS" ↗** →
comfy-skills repo |
## Changes
- **`FeatureGrid01.vue`** — add `variant?: 'default' | 'outline'` to the
link card action; button now uses `card.action.variant ?? 'outline'`
instead of a hardcoded outline, so callers can opt into the filled
style.
- **`config/routes.ts`** — add `mcpSkills` external link
(`https://github.com/Comfy-Org/comfy-skills`).
- **`i18n/translations.ts`** — refresh the `mcp.setup.*` copy (en +
zh-CN): new subtitle, reworded steps, new `step2.cta` / `step3.cta`,
drop the now-unused `step1.cta`.
- **`SetupSection.vue`** — re-map cards: step 1 → copy field, steps 2 &
3 → filled link buttons.
## Test plan
- [x] `pnpm typecheck` — 0 errors
- [x] Pre-commit hooks (stylelint, oxfmt, oxlint, eslint, typecheck)
pass
- [ ] Visual check on `/mcp` and `/zh-CN/mcp` (copy field on step 1; two
filled yellow CTAs with up-right arrows on steps 2 & 3)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilds the **Comfy MCP** marketing page on the website design-system
stack and adds the missing zh-CN page.
## What's here
- Replaces the bespoke `components/product/mcp/` section silo with thin
`templates/mcp/*` wrappers over reusable `blocks/` + `common/`
components.
- Adds `src/pages/zh-CN/mcp.astro` and threads `locale` through every
section (was English-only).
- New/extended design-system blocks:
- `FeatureGrid01` — setup steps, with a reusable `ui/CopyableField`
(uses `@vueuse/core` `useClipboard`).
- `FeatureGrid02` — how-it-works steps with `NodeUnionIcon` connectors +
a CTA pair via `ui/button`.
- `FeatureRows01` — alternating media rows; `ReasonsSplit01` — "why"
list.
- `HeroSplit01` gained `subtitle`, a `media` slot, and a `class`
passthrough; `SectionHeader` gained `align`.
- Standardized block section spacing on `px-6 py-16 lg:py-24`.
- Refreshed all 8 MCP FAQ answers (en + zh-CN) and hydrated the FAQ
section so the accordion is interactive.
## Notes
- Stacked on the original MCP landing-page commits (previously PR
#13095); those ride along here.
- `typecheck` and `build` are green; `/mcp` and `/zh-CN/mcp` both render
in both locales.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Balpreet Brar <balpreet.brar@growthnatives.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Move the `/launches` nav item from **Company → More** to **Products →
Features** in the main navbar
- Add the workflow link to the **Cleanplate Walkthrough** learning
tutorial (`https://comfy.org/workflows/8f2cf0df5da6-8f2cf0df5da6/`)
## Changes
- `apps/website/src/data/mainNavigation.ts`
- `apps/website/src/data/learningTutorials.ts`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
> **Goal: make updating a published Hub workflow painless in admin
panel.** Today a admin who needs to change a
> published workflow has to reject + reshare + republish, which breaks
the share link, resets stats,
> and blanks the thumbnail — and the rejected backlog can't be cleared.
This is one of 3 PRs that fix it:
>
> | PR | Repo | Fixes |
> |----|------|-------|
> | [#4505](https://github.com/Comfy-Org/cloud/pull/4505) | `cloud` |
Re-publish stops losing data; the editor can read its own published
metadata; reviewer search + admin delete |
> | #13139 | `ComfyUI_frontend` | The publish dialog prefills prior
metadata + thumbnail and updates in place |
> | [#10 ](https://github.com/Comfy-Org/comfy-admin-panels/pull/10)|
`comfy-admin-panels` | Admins can delete rejected/stale workflows |
>
> **Merge order:** `cloud` first (the others depend on its endpoints),
then `ComfyUI_frontend` and
> `comfy-admin-panels` in either order.
## Summary
Editor side of the same effort. Re-opening the publish dialog for an
already-published workflow now
prefills its description, tags, and thumbnail, restores the thumbnail
across step/type changes, keeps
the local workflow name in sync, and labels the action "Update" instead
of "Publish". Depends on the
`cloud` PR — that's what finally returns the `share_id` the dialog reads
from.
## Changes
- **What**:
- **Prefill the thumbnail** — `extractPrefill` dropped `thumbnail_url` /
`thumbnail_comparison_url`,
so only the thumbnail *type* was remembered, never the image. Thread the
URLs into `PublishPrefill`
and restore them; on submit, send the existing URL when no new file is
attached (reuses the
`sampleImageUrls` precedent — an existing URL, not a `File`), so
re-publishing doesn't blank it.
- **Uploads survive navigation** — the thumbnail step kept its `File` in
local component state, so
leaving the step and coming back blanked a fresh upload. The step is now
controlled — files live in
the form data, the single source of truth that survives the remount.
- **Type-gate the prefilled image** — a restored image must only show on
the tab it belongs to;
`existingThumbnailType` keeps an image off the video tab (which was
hiding the upload prompt) while
still restoring it when you toggle back.
- **Refetch on rename** — the dialog is a reused singleton, so
`onMounted` fires once; a watch on the
active workflow path refetches prefill when a rename changes it (the
description was going stale).
- **Name sync** — editing the name field published a new Hub display
name but never renamed the local
workflow, so the editor tab (and a reload) kept the old name. Publish
now renames the local file
when the chosen name differs.
- **"Update" CTA** — the intro panel and footer read "Update" (not
"Publish") when the workflow is
already published, and note that the share link + stats are preserved.
## Review Focus
- `existingThumbnailType` is the load-bearing bit for both the preview
and submit gating — confirm an
image prefill never submits as a video after a type toggle, and that
toggling back restores it.
- Name sync renames *after* a successful publish and is non-fatal on
failure (toast + keep the publish).
The Hub record is keyed by workflow ID, so the rename doesn't orphan it
— worth a sanity check.
## Screenshots
https://github.com/user-attachments/assets/99dd9eff-987f-4ddb-9cf1-e9b40f61e7dc
Current `main` **fails a fresh `knip` run** with 13 unused exported
types (exit 1). They're invisible on main because lint/knip only runs on
`pull_request`/`merge_group`, never on push to main — so merge skew (one
PR adds an export used by file X; a later PR removes X's usage)
accumulates latent failures that ambush backport branches (e.g. #13163,
#13162).
Each of the 13 is `export`ed but referenced only within its own file
(verified 0 importers; ≥2 in-file uses, so not dead code). Fix: drop the
redundant `export`.
Types cleaned: `VideoSource`, `ObjectInfoResponse`,
`PromotedMissingModelWorkflow`, `PixelReadout`, `ResizeDirection`,
`ResizeHandle`, `RunButtonTelemetryOptions`, `ResolvedModelNode`,
`AccountPreconditionContext`, `SubscriptionDialogOptions`,
`MonthlyCreditsUsage`, `MissingMediaReference`, `ResolvedHostWidget`.
Reviewer note: `ResolvedHostWidget` and `ResolvedModelNode` sit under
`renderer/extensions`/`platform/assets`; no in-repo importers, but if
either is intended as published/extension-facing API, prefer a knip
`entry`/`ignore` over un-exporting — flag in review and I'll adjust.
After fresh `knip`: **0 unused exported types**.
Supersedes #13179 (fixed only `AccountPreconditionContext`). Pairs with
the push-gate workflow #13203 — merge this first so that gate is green
on main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Add a hideInPanel widget option so a widget still renders on the node
body but is omitted from the right side panel. Apply it to the Three.js
viewport widgets (Load3D, Preview3D, Load3DAdvanced, SaveGLB), whose
non-syncable scene state would diverge if a second instance rendered in
the panel.
App mode and the subgraph editor are unaffected (they filter on
canvasOnly independently).
Discussed with @alexisrolland and @PabloWiedemann
## Screenshots
before
<img width="2206" height="1181" alt="image"
src="https://github.com/user-attachments/assets/e536871f-65e6-4d6e-aa61-dc981362214f"
/>
after
<img width="2743" height="1295" alt="image"
src="https://github.com/user-attachments/assets/6cc6d252-57ac-464a-a2b7-1ada5ab9e705"
/>
## Summary
A node that fails at runtime is now outlined in the same red as a node
that fails validation, instead of magenta.
## Changes
- **What**: The `executionError` stroke in `litegraphService.ts` was
hardcoded to magenta (`#f0f`); validation errors use
`LiteGraph.NODE_ERROR_COLOUR` (red). Reuse that constant so both error
states render consistently. One-line change; `LiteGraph` is already
imported.
## Review Focus
No test added: asserting a hardcoded stroke colour would be a
change-detector test. The two error paths (validation via `has_errors` /
`NODE_ERROR_COLOUR`, runtime via `lastExecutionError`) now share the
same colour source.
## Summary
Harden E2E coverage HTML generation against non-renderable LCOV source
entries so public assets and stale sourcemap paths no longer abort the
report.
## Changes
- **What**: Removes `assets/images/*` entries from merged E2E LCOV
before upload/report generation.
- **What**: Lets `genhtml` ignore range warnings and synthesize missing
source files when LCOV references stale paths.
- **Dependencies**: None.
## Review Focus
Root cause: Playwright/Monocart can emit LCOV `SF:` records that
`genhtml` cannot read from the checkout. The failed run stopped first on
public assets like `assets/images/hf-logo.svg`; replaying the same
artifact also exposed stale source paths after those assets were
removed.
The filter is intentionally `assets/images/*`, not `assets/*`, because
real `lcov` matching would also remove legitimate source coverage under
`src/platform/assets/...`.
## Validation
- `yamllint --config-file .yamllint
.github/workflows/ci-tests-e2e-coverage.yaml`
- Replayed failed run `28138018468` merged LCOV:
- `assets/images/*` strip leaves `0` `SF:assets/...` entries
- preserves `68` `SF:src/platform/assets/...` entries
- `genhtml` exits `0` with `--ignore-errors source,unmapped,range
--synthesize-missing`
- Commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`
- Push hook: `knip --cache`
## Screenshots (if applicable)
N/A, CI workflow-only.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Changes are limited to the E2E coverage GitHub Actions workflow; no
application runtime or security paths are touched.
>
> **Overview**
> Fixes E2E coverage HTML generation failing when merged LCOV references
paths **genhtml** cannot read (public static assets and stale sourcemap
paths from Playwright/Monocart).
>
> The **Strip non-source entries** step now also drops `assets/images/*`
via `lcov --remove`, scoped narrowly so real source under
`src/platform/assets/...` stays in the report. **Generate HTML coverage
report** passes `--ignore-errors source,unmapped,range` and
`--synthesize-missing` so remaining unmapped or missing sources do not
abort the job.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ede5556644. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
## Summary
Adds a branded local `NodeId` helper and starts separating local node
identity from serialized workflow IDs.
## Changes
- **What**: Adds central `NodeId` parsing/branding helpers, migrates
nearby widget identity types, keeps queue results at the serialized
boundary, and removes misleading workflow `NodeId` usage from execution
error maps.
## Review Focus
Check that the first migration slice keeps serialized/API IDs as raw
`number | string` while local UI/store IDs use the branded string type.
## Caveat
`SUBGRAPH_INPUT_ID` and `SUBGRAPH_OUTPUT_ID` are now branded local
`NodeId` string values internally instead of numeric sentinels.
Reviewers should double-check extension compatibility for callers that
import `Constants` and compare those values numerically.
## Screenshots (if applicable)
N/A
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Localizes the 12 system stats column headers via vue-i18n and fixes the
`Pytorch` → `PyTorch` casing typo.
## Changes
- **src/locales/en/main.json**: Add 11 i18n keys under `g.systemStats*`
namespace
- **src/components/common/SystemStatsPanel.vue**:
- Import `useI18n` and use `t()` for column headers
- Change `header` field to `headerKey` in ColumnDef type
- Fix PyTorch casing (was `Pytorch Version`)
## Context
Follow-up to PR #11816 per review comment.
## Test plan
- [x] Column headers render correctly
- [x] Copy System Info includes localized headers
- [ ] Verify other locales can override (out of scope - EN only for now)
Closes#11870🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12253-refactor-localize-system-stats-headers-and-fix-PyTorch-casing-3606d73d36508134af99f7ca4f9c6593)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Adds full Hebrew (`he` / עברית) localization for the ComfyUI frontend
UI, registered the same additive way as the existing RTL locales (`ar`,
`fa`).
## Changes
- **What**:
- New `src/locales/he/main.json`, `commands.json`, and `settings.json` —
complete, human-reviewed Hebrew translations.
- Exact key parity with `en` (3524 / 125 / 235 keys), verified
programmatically.
- All `{interpolation}` placeholders and `|`-separated plural forms are
preserved (same segment counts as `en`).
- Established technical terms are kept in English (API, GPU, VAE, CLIP,
LoRA, ControlNet, Civitai, Hugging Face, flux, Nodes 2.0, …).
- `dataTypes` and `nodeCategories` are kept as verbatim English
identifiers; option **keys** in
`settings.json`/`menuLabels`/`contextMenu` are left untouched (only
their display values are translated).
- `src/locales/he/nodeDefs.json` is an empty object on purpose, so node
definitions fall back to English and get auto-populated by the release
i18n workflow (per the locale CONTRIBUTING guide).
- Registered `he: { text: 'עברית', loaders: loadersFor('he') }` in
`src/locales/localeConfig.ts` (which automatically adds it to the
Settings → Language dropdown and `SUPPORTED_LOCALE_OPTIONS`).
- Added `he` to `outputLocales` in `.i18nrc.cjs`, plus a Hebrew glossary
block for the CI translator.
## Review Focus
- This follows the approach of #7876 (Persian/Farsi). Like the existing
`ar`/`fa` locales, it translates UI text only and does **not** introduce
RTL layout (`dir="rtl"`) — the app does not currently apply RTL layout
for any locale. I'm happy to follow up with proper RTL layout support in
a separate PR if that's wanted.
- Recurring-term glossary used for consistency: node = צומת, workflow =
תהליך עבודה, queue = תור, widget = פקד, subgraph = תת-גרף, canvas =
קנבס, bypass = עקיפה, prompt = פרומפט.
- Native-speaker review is very welcome. cc translation maintainers
@Yorha4D @KarryCharon @DorotaLuna @shinshin86
## Screenshots
Text-only locale addition — no UI/layout changes. After this change,
**Settings → Language** lists **"עברית"**, and selecting it renders the
UI in Hebrew (untranslated node definitions fall back to English).
---------
Co-authored-by: Yosef Chai <192742853+yosef-chai@users.noreply.github.com>
Co-authored-by: christian-byrne <abolkonsky.rem@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Allow crawler access to `/payment/` routes in robots.txt so search
engines can read the `noindex` tag, and forcefully inject the
`x-robots-tag: noindex` header via `vercel.json`.
## Changes
- **What**: Removed `Disallow: /payment/` from `robots.txt` and added
rules to `vercel.json` applying `x-robots-tag: noindex` to
`/payment/(.*)` and `/zh-CN/payment/(.*)` routes.
## Review Focus
- The configurations in `vercel.json` apply to both English and
localized payment routes (`/zh-CN/payment/(.*)`).
---------
Co-authored-by: nav-tej <36310614+nav-tej@users.noreply.github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
## Summary
The `PR Backport` workflow silently fails for any PR that also modifies
a file under `.github/workflows/**`.
## Root cause
The `backport` job checks out with the default `GITHUB_TOKEN` and reuses
those persisted credentials for `git push`. GitHub refuses to let that
token create or update workflow files:
```
! [remote rejected] backport-12804-to-core-1.45 -> backport-12804-to-core-1.45
(refusing to allow a GitHub App to create or update workflow
`.github/workflows/ci-tests-e2e.yaml` without `workflows` permission)
error: failed to push some refs
```
The cherry-pick itself succeeds — only the push is rejected. And because
the `run:` step inherits `set -e`, the loop aborts before writing the
`failed=` output, so the "Comment on failures" step (`if: failure() &&
steps.backport.outputs.failed`) posts nothing. The result is a red job
with no explanation on the PR.
## History
PR #12804 touched `.github/workflows/ci-tests-e2e.yaml` and
`.github/actions/setup-frontend/action.yaml`. Its backport run
([27788259837](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27788259837/job/82230406910))
failed exactly this way: cherry-pick clean on every target, push
rejected on the workflow file. All four backports (#12966, #12967,
#12968, #12969) had to be created manually.
## Changes
Check out with `PR_GH_TOKEN` (already used by the Create-PR step) so the
push carries `workflow` scope.
> [!IMPORTANT]
> `PR_GH_TOKEN` must have **workflow** write permission for this to take
effect. If it does not, the secret needs that scope added.
## Follow-up (not in this PR)
The push failure aborts the whole job under `set -e` with no PR comment.
Even with the token fixed, a push rejected for another reason (branch
protection, etc.) would still fail silently. Wrapping the push so a
single-target failure is recorded as a `push-failed` reason and reported
via the existing failure-comment step would make the workflow degrade
gracefully.
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Redesign missing-model detection for ADR 0009 promoted subgraph widgets
so candidates are created from the widget value the user can actually
edit, while still using the concrete interior widget as the
schema/options source.
## Why This PR Exists
This PR comes from the follow-up missing-model detection work for the
ADR 0009 / 1.46 subgraph widget changes introduced by
[#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197).
[#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197)
intentionally changed promoted subgraph widgets to be represented
through subgraph input links. After that change, the promoted widget on
the host `SubgraphNode` is the editable value owner, and linked interior
widgets are no longer guaranteed to mirror that value.
The old missing-model contract still treated the concrete interior node
widget as the effective source of truth in subgraph cases:
- recursive scans entered the subgraph and scanned the interior widget
value;
- candidates were keyed by the interior node/widget identity;
- the parent subgraph host mostly received propagated
highlight/navigation behavior;
- subgraph container widgets were not treated as first-class candidate
sources.
That contract breaks after ADR 0009. A user can resolve a missing model
by changing the promoted host widget to an installed model, while the
linked interior widget can still hold the old stale value. If detection
keeps scanning the linked interior value, entering the subgraph or
reloading the workflow can re-create a false missing-model error that no
longer corresponds to the value the user can edit.
ADR 0009 also means the same subgraph definition can be reused by
multiple `SubgraphNode` hosts. Once missing-model detection moves from
the interior definition widget to the promoted host widget, the selected
value is no longer a property of the shared definition alone. It is a
property of a specific host instance. That makes the old interior-node
identity insufficient for mode changes, removal handling, and re-scan
behavior: a single interior leaf definition can be reachable through
multiple host execution paths, and only the affected host path should
add, remove, or restore a candidate.
This PR also builds on
[#12990](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12990),
which narrowed workflow-level `models[]` and embedded model data to
metadata enrichment only. Together, the intended boundary is:
- live widgets create missing-model candidates;
- workflow/root-level `models[]` and node metadata only enrich
candidates that already came from a live widget;
- promoted widget values are read from the editable host widget, not
inferred from stale interior `widgets_values`.
## Changes
- **What**:
- Introduces promoted-widget scan targets that split:
- host promoted widget value and candidate identity;
- concrete leaf widget/node definition data.
- Scans the outermost unlinked promoted widget on a `SubgraphNode` host
as the selected value owner.
- Skips linked interior widgets as candidate sources, preventing stale
linked widget values from producing duplicate or false missing-model
candidates.
- Resolves the concrete leaf widget for combo options, asset-widget
support, node type, directory lookup, and embedded metadata enrichment.
- Keys promoted missing-model candidates by the host execution id and
host promoted widget name.
- Adds `sourceExecutionId` to promoted candidates so liveness still
follows the concrete source execution path, including nested inactive
subgraph containers.
- Uses source-scope activity for pipeline filtering and async
verification, while keeping highlight/store/clearing identity
host-keyed.
- Removes host-keyed promoted candidates when their source execution
scope is removed or bypassed.
- Re-scans ancestor subgraph hosts when an interior source path is
un-bypassed, so host-keyed promoted errors can reappear correctly.
- Handles shared subgraph definitions by deriving promoted source paths
from the concrete host instance path, rather than treating the shared
definition node id as globally unique.
- Shares promoted source resolution between Vue node processing and the
right-side panel to avoid drift.
- Aligns missing-model clearing across Vue node widgets, legacy canvas
widgets, and right-side panel Parameters/Nodes section widgets.
- Adds unit coverage for scan identity, source-scope liveness, dynamic
mode changes, source-scope removal, shared-definition host isolation,
and right-side panel clearing.
- Adds nested promoted-widget E2E coverage for OSS and Cloud flows
across Vue, Parameters tab, and legacy widget surfaces.
- **Breaking**: None expected.
- **Dependencies**: None.
## New Detection Contract
A missing-model candidate is created from an unlinked final editable
value owner.
That value owner can be:
- a normal node model widget; or
- the outermost promoted model widget displayed on a `SubgraphNode`
host.
For promoted widgets:
- the host promoted widget supplies the selected value;
- the host execution id and host widget name are the candidate identity;
- the concrete leaf widget supplies definition data such as combo
options and asset-browser support;
- the concrete source execution path is retained as `sourceExecutionId`
for liveness only;
- linked interior widgets are skipped as candidate sources because their
values are not authoritative when driven by a promoted input.
For a nested chain:
`Outer promoted widget A -> inner promoted widget B -> concrete widget
C`
only `A` creates the candidate. `B` and `C` are linked along the
promoted-input path and are skipped as selected-value sources, while `C`
still provides the concrete widget definition used to evaluate `A`.
## Shared Definition And Source-Scope Liveness
ADR 0009 promoted widgets make subgraphs behave more like reusable
definitions with host-owned inputs. Two host `SubgraphNode`s can point
at the same interior subgraph definition while carrying different
promoted widget values. In that shape, the missing-model candidate must
be keyed to the editable host surface, but the activity check cannot use
the host id alone.
For example, if two outer hosts share the same nested subgraph
definition, one host can select a valid model while the other still
selects a missing model. The result should be one missing-model
reference, not a single definition-level error and not two errors after
one host is fixed. Likewise, bypassing or un-bypassing an interior
nested container should affect only the host execution paths that
actually pass through that container.
This PR therefore separates two concepts:
- **candidate identity**: host execution id + host promoted widget name,
used for storage, highlight, navigation, and clearing;
- **candidate liveness**: concrete source execution path, used for
scan-time activity checks, pipeline filtering, async verification,
source-scope removal, and re-exposure after mode changes.
That separation is the reason this PR updates more than the scan itself.
Moving the detection target to the subgraph host also requires the
mode-change and removal paths to understand that a host-keyed candidate
can be invalidated by a descendant source path, and can need to be
restored by re-scanning an ancestor host when an interior source path
becomes active again.
## Review Focus
Please review the identity split carefully:
- candidate/store/highlight/clearing identity should remain host-keyed
for promoted widgets;
- liveness should use `sourceExecutionId` when present, falling back to
`nodeId` for normal candidates;
- scan-time activity checks should account for the source node itself
and all ancestor subgraph containers;
- source-scope removal should remove host-keyed candidates whose
concrete source path was removed or bypassed;
- un-bypassing an interior source path should re-scan affected ancestor
subgraph hosts so host-keyed candidates can reappear;
- shared subgraph definitions should not merge errors across different
host instances;
- linked interior widgets should not produce their own missing-model
candidates;
- asset-browser eligibility should be resolved from the concrete leaf
node type and widget name, not the synthetic subgraph host type;
- right-side panel edits should clear host missing-model errors and
source validation errors consistently.
The E2E matrix intentionally keeps nested promoted workflows only.
Nested promoted widgets cover the same editable host path as single
promoted widgets while also exercising the `A -> B -> C` chain that can
break source-scope liveness and re-scan behavior. The nested fixture
also includes multiple host instances that share the same subgraph
definition, so it verifies that fixing one host does not accidentally
clear or suppress another host's missing-model candidate. Direct/single
promoted behavior is still covered at the unit level.
## Non-Goals
- This PR does not reintroduce workflow-level `models[]` candidate
creation.
- This PR does not infer selected model values from `widgets_values`.
- This PR does not synchronize linked interior widget values back from
promoted host widgets.
- This PR does not redesign missing-media scanning; missing media still
skips subgraph containers and remains keyed by concrete interior paths.
The shared async post-verification active-scope filter is intentionally
stricter, so a pending missing-media candidate is no longer surfaced if
its own node is bypassed or removed while verification is in flight.
## Validation
- `pnpm exec vitest run
src/components/rightSidePanel/parameters/SectionWidgets.test.ts
src/platform/missingModel/missingModelScan.test.ts
src/composables/graph/useErrorClearingHooks.test.ts
src/platform/missingModel/missingModelPipeline.test.ts
src/platform/missingModel/missingModelStore.test.ts
src/utils/graphTraversalUtil.test.ts
src/composables/graph/useGraphNodeManager.test.ts
src/renderer/extensions/vueNodes/composables/useProcessedWidgets.test.ts
--reporter=dot`
- 8 files passed, 294 tests passed.
- `pnpm exec vitest run
src/platform/missingModel/missingModelScan.test.ts
src/core/graph/subgraph/resolveConcretePromotedWidget.test.ts
src/components/rightSidePanel/parameters/SectionWidgets.test.ts`
- 3 files passed, 71 tests passed.
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm format:check`
- targeted ESLint for changed production/unit/E2E files
- `git diff --check`
- `pnpm build`
- `pnpm build:cloud`
- OSS affected E2E on the 8188 build:
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm
exec playwright test
browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts
--project=chromium --grep "Changing an OSS .*promoted|Refreshing a
resolved promoted|Reloading a resolved nested"`
- 5 passed.
- Cloud affected E2E on the 8188 cloud build:
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm
exec playwright test
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts
--project=cloud --grep "Changing a Cloud .*promoted"`
- 2 passed; the Cloud legacy promoted asset-modal case still fails until
[#13075](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13075) is
merged.
- Full OSS `errorsTabModeAware.spec.ts` on the 8188 build:
- 23 passed; 3 existing paste/clipboard cases failed before the promoted
subgraph section with node count remaining at 1 after
`clipboard.paste()`.
- Commit hooks ran `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`, and
browser typecheck where applicable.
- Pre-push hook ran `pnpm knip --cache`.
## Screenshots
Before
https://github.com/user-attachments/assets/6380c1da-1d92-4b70-888e-3ade572c4b5b
After
https://github.com/user-attachments/assets/4cfc24d6-3dc3-4e36-9b31-72fea6b3d9d5
## Summary
Adds the `/drops` landing page in English and Simplified Chinese, ahead
of the 2026-06-24 livestream. The page is composed of a livestream-gated
hero, a 10-card drops grid, a subscribe banner, and a closing CTA.
Production media for the hero and several cards is wired in; the rest
fall back to placeholders.
## Changes
- **New `/drops` page** (`pages/drops.astro`, `pages/zh-CN/drops.astro`)
plus en/zh-CN translations.
- **`HeroLivestream01` block** — renders the rotating-logo video before
the stream window, swaps to a YouTube nocookie iframe between
`startDateTime` and `endDateTime`. SSR stays deterministic on the logo
and only flips to the embed after client hydration. Takes
`youtubeVideoId` directly (no URL parsing).
- **Drops grid** — `data/drops.ts` defines the 10 cards with `imageFor`
/ `videoFor` media helpers, badges, and per-locale
title/description/href. `DropCard.vue` renders an image or autoplaying
muted video inside the new `Card` primitive set (`Card`, `CardHeader`,
`CardTitle`, `CardDescription`, `CardContent`, `CardFooter`).
- **Supporting blocks** — `SubscribeBanner` uses the Button `link`
variant with an animated hover underline; closing `CtaCenter01` is
extended with a terms link. `resolveRel` is shared from `utils/cta.ts`
instead of duplicated per block.
- **Visual extension to `HeroLivestream01.visual`** — discriminated
`image | video` union so the hero can render a looping video.
- **E2E** — `e2e/drops.spec.ts` covers both locales (hero + grid render,
locale-correct links).
## Review Focus
- `youtubeVideoId` is currently a `nlLZfNIqF8M` placeholder;
`startDateTime`/`endDateTime` are placeholders too — see TODO in
`HeroSection.vue`. These need to be swapped to the production stream +
window before launch.
- Several drop cards still point at placeholder destinations (e.g. Comfy
MCP at `/mcp`) — TODO noted in `drops.ts`.
- Drop card media is a mix of images and short MP4s served from
`media.comfy.org`. The videos autoplay muted/looped with
`preload="metadata"`.
## Test plan
- [ ] `pnpm --filter website dev` → visit `/drops` and `/zh-CN/drops`;
confirm hero video plays, grid renders all 10 cards, video cards loop,
subscribe banner link styled correctly
- [ ] Temporarily set `startDateTime` to the past and `endDateTime` to
the future; confirm hero swaps to the YouTube iframe after hydration
- [ ] `pnpm --filter website test:e2e drops.spec.ts`
- [ ] `pnpm typecheck:website`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Realign the ECS architecture docs and ADR 0008 with the shipped
direction (PR #12617): entity data lives in dedicated Pinia stores keyed
by string IDs, rather than one unified \"World\" registry addressed by
branded entity IDs.
## Changes
- **What**: Docs-only. Retarget the `docs/architecture/` set + ADR 0008
+ agent guidance from the single-World/branded-`*EntityId` model to the
dedicated-store model that PR #12617 actually shipped
(`widgetValueStore` keyed by `WidgetId`, `layoutStore`,
`nodeOutputStore`, `domWidgetStore`, `subgraphNavigationStore`,
`previewExposureStore`).
- `AGENTS.md` + `.agents/checks/adr-compliance.md`: point entity-data
guidance at dedicated stores; fix the inverted `world.getComponent`
compliance check (it flagged correct store-based code).
- `ADR 0008`: dated amendment note (stays Proposed); rewrite the World
section → dedicated stores, Branded ID design (`WidgetId` composite
string), migration strategy, render-loop, consequences, notes.
- `proto-ecs-stores.md`: flip the \"Unified World / branded IDs\"
framing from gap-to-close → target; replace the deleted
`PromotedWidgetViewManager` with the `input.widgetId` store-backed
model; fix key formats and store count.
- `ecs-target-architecture.md` / `ecs-lifecycle-scenarios.md` /
`ecs-migration-plan.md`: reframe all `world.*` APIs and `*EntityId`
brands to per-store APIs + string keys; mark already-shipped migration
phases done.
- `subgraph-boundaries-and-promotion.md` / `entity-interactions.md` /
`entity-problems.md`: scope-tagged store entries; swap removed
`PromotionStore` for `previewExposureStore`.
- `appendix-critical-analysis.md`: post-pivot status banner + resolution
notes on the critiques the pivot vindicated; still-open gaps (extension
callbacks, atomicity, Y.js↔ECS) left live.
- `appendix-ecs-pattern-survey.md`: supersede banner; keep the external
library survey (§1).
- Delete obsolete `ecs-world-command-api.md` (its command-pattern
argument folded into ADR 0008).
- **Breaking**: None (documentation only).
## Review Focus
- ADR 0008 stays **Proposed** with an amendment note rather than a new
superseding ADR — confirm that's the preferred mechanism vs. a fresh
ADR.
- Numeric per-kind brands (`NodeEntityId`, `LinkEntityId`, …) are
retained in ADR 0008 but explicitly marked aspirational/unshipped; only
`WidgetId` (composite string) reflects shipped code.
- `appendix-ecs-pattern-survey.md` §2–§4 are kept under a supersede
banner as historical record (they describe the deleted `src/world/`
substrate) rather than rewritten — confirm that's preferred over
deletion.
- Net −384 lines; no code or test changes.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#12617 introduced a regression in Dynamic Combos. If two options have
child widgets of the same name (such as `bit_depth` on `Save Image
(Advanced)`), then widget state would be incorrectly shared between the
two widgets.
This is resolved by having removed widgets also delete their state.
There was previous interest in having widgets of this type keep state
when valid. This interest remains, but will require a more controlled
intentional implementation in the future.
Since the bit depth options on `Save Image (Advanced)` could potentially
be expanded in the future, this PR specifically adds a new devtools node
for testing with.
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
`selectedItems` was being filtered to nodes and groups. Since no special
behaviour is being performed on groups, the 'move groups' code is
relaxed to instead 'move all non-node selected items'.
Stacks on #12228. Part of the LGraph dead-hook cleanup per AUDIT-LG.9.
## What
- `LGraph.onGetNodeMenuOptions` — **deleted** (field + dispatcher in
`LGraphCanvas.getNodeMenuOptions`). Zero ecosystem consumers.
- `LGraph.onBeforeChange` — **deprecated, not deleted**. The field and
the dispatch in `LGraph.beforeChange()` are kept, but assigning a
handler now emits a one-time `warnDeprecated` nudging migration to
`LGraphCanvas.onBeforeChange`.
## Why onBeforeChange is preserved
The W2F-1 re-audit found `bmad4ever/ComfyUI-Bmad-DirtyUndoRedo` assigns
`app.graph.onBeforeChange = fn` (the listener-assignment pattern).
Deleting the field outright would silently turn those handlers into
no-ops. Keeping it as a deprecated shim preserves backward compatibility
during a grace period while signaling the intended replacement.
`onAfterChange` is untouched (so `BennyKok/comfyui-deploy`'s
`onAfterChange` wrapper keeps working). `LGraphCanvas.onBeforeChange`
remains a separate field, and the canvas dispatch chain
`this.canvasAction((c) => c.onBeforeChange?.(this))` is unchanged.
## Tests
`LGraph.test.ts` covers the shim: the assigned listener is still
invoked, the deprecation warning fires when used, and no warning fires
when no listener is assigned.
## Sequencing
- Stacks on #12228
- Sequences behind Alex's Phase B (#11939, #11811)
---------
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Aligns the Settings ▸ Workspaces **Plan & Credits** panel to DES-186:
state-driven subtitle, team/personal header variants, design perks, and
a shared footer help bar. Stacked on FE-964 (#12734), which owns the
CreditsTile content.
## Changes
- **What**:
- Subtitle per design variants (Figma 3255-21472): Active → "Renews on
{date}", Ending → "Ends on {date}". `subscription_status: 'scheduled'`
falls back to the Active treatment — the facade exposes no
scheduled-plan target/date fields yet, so "Changes to {plan} on {date}"
cannot be rendered (template reserved in i18n-ready form; see Linear
note).
- Team-active header: plan name "Team" + seat-aware workspace total
(`seat_summary.total_cost_cents` from `/billing/plans`) as "$X USD /
mo"; the Next-month-invoice card reads the same computed so the two
can't disagree. Per-member tier price + "USD / mo / member" kept as the
plans-unresolved fallback.
- CTAs per design + designer annotation: "Manage billing" + "Change
plan" on a team plan, "Upgrade plan" on personal.
- Team perks (Figma 2993-14789): "Your plan includes everything in
**Pro**, plus:" (i18n-t, plan name emphasized) + invite members /
concurrent runs / shared credit pool / role-based permissions.
- Personal no-subscription variant (Figma 2993-14604): "Free · $0 USD /
mo" header + primary Subscribe CTA + "What's included:" with the
max-runtime perk → "10 min max runtime" (`subscription.maxDuration.free`
set to 10 min per DES 3253-16079).
- Footer help bar (Learn more / Partner Nodes pricing table / Message
support / Invoice history) extracted into `SubscriptionFooterLinks.vue`,
shared by workspace + legacy panels; new surface-specific key for
"Partner Nodes pricing table".
- **Breaking**: none.
## Review Focus
- Seat-aware price source (`currentPlan.seat_summary.total_cost_cents`)
vs per-member fallback — fixture locks $320 vs $80.
- `'scheduled'` → Active fallback (adapter-level test in
`useWorkspaceBilling.test.ts`).
- Free-state perk copy: `subscription.maxDuration.free` set to **10
min** per DES 3253-16079 (design confirmed; was 30 min).
- Free-state overflow (`⋯`) button intentionally omitted: the only
existing menu item (Cancel Subscription) doesn't apply to a free plan.
Linear:
[FE-768](https://linear.app/comfyorg/issue/FE-768/updates-to-workspaces-tab-of-settings)
(Plan & Credits half; Members invite UI ships in #12759)
## Screenshots
Captured on `local.comfy.org` dev (cloud-prod backend, authenticated
session). Team and free states use client-side API stubs (XHR-level for
`/api/billing/*` + `/api/workspace/*`, fetch-level for legacy
`/customers/*`) since the test workspaces are unsubscribed;
personal-active rows are real account data.
| State | Before (FE-964 base) | After (this PR) |
|---|---|---|
| Team plan — active | <img width="400" alt="before: Pro, $100
USD/mo/member, Renews, Manage Payment/Upgrade Plan"
src="https://github.com/user-attachments/assets/622a9a27-1875-4c08-92b7-9e43a8067c59"
/> | <img width="400" alt="after: Team, $300 USD/mo, Renews on, Manage
billing/Change plan, design perks"
src="https://github.com/user-attachments/assets/adb0f767-d508-4455-ad8e-ee2d6ac419dc"
/> |
| Team plan — ending (cancelled) | <img width="400" alt="before: Expires
Jul 10, 2026"
src="https://github.com/user-attachments/assets/cb4bb978-4e8b-4372-8ecc-7265f477a828"
/> | <img width="400" alt="after: Ends on Jul 10, 2026"
src="https://github.com/user-attachments/assets/5466b99a-d016-4eba-a60f-99b87bd2693e"
/> |
| Personal — active (real data) | <img width="400" alt="before: Renews
Mar 10 2027, Manage Payment"
src="https://github.com/user-attachments/assets/34018400-930b-4147-bd0d-398fb4d159ee"
/> | <img width="400" alt="after: Renews on Mar 10 2027, Manage billing"
src="https://github.com/user-attachments/assets/87af7fa7-28cf-4cdf-b9a2-158b2a6eb979"
/> |
| Personal — no subscription | <img width="400" alt="before: generic
not-on-a-subscription prompt"
src="https://github.com/user-attachments/assets/7ee5b36e-1e07-4630-a9b7-680f4fad349b"
/> | <img width="400" alt="after: Free $0 USD/mo header + Subscribe +
What's included"
src="https://github.com/user-attachments/assets/1eda1791-e3de-46c3-bb69-df0365545211"
/> |
---
## Perk descriptions — designer QA (DES `3253-16079`)
CDP-verified the Plan & Credits **perk list** per current plan against
Figma. Captured at the full 1280px settings layout (width fix: #12849;
the perk text/content is identical at the current 960px).
| Plan | "Includes" header | Perks shown |
|---|---|---|
| Free (no subscription) | What's included: | 10 min max runtime |
| Personal — Pro | Your plan includes: | 1 hr max run duration · RTX
6000 Pro (96GB VRAM) · Add more credits whenever · Import your own LoRAs
|
| Team | Your plan includes everything in **Pro**, plus: | Invite
members · Members can run workflows concurrently · Shared credit pool
for all members · Role-based permissions |
**Free — Figma `3253-16079` (left) vs implementation (right):**
| Figma | Implementation |
|---|---|
| <img width="480" alt="Figma DES 3253-16079 free settings"
src="https://github.com/user-attachments/assets/cfe79570-f3ba-4627-a8eb-348b9158f6ac"
/> | <img width="480" alt="App free settings — What's included: 10 min
max runtime"
src="https://github.com/user-attachments/assets/d89898e4-d819-486c-9b4f-c2fd61916783"
/> |
**Personal — Pro:**
<img width="640" alt="App personal Pro — Your plan includes"
src="https://github.com/user-attachments/assets/adc2fd9f-d249-469f-b947-1ec8f674cbb0"
/>
**Team:**
<img width="640" alt="App team — Your plan includes everything in Pro,
plus"
src="https://github.com/user-attachments/assets/e7378067-11a2-411b-b37b-98c8aecb82b1"
/>
Open items (design):
- Free perk now reads **"10 min max runtime"**
(`subscription.maxDuration.free` set to 10 min) per Figma `3253-16079` —
✅ applied in this PR.
- Personal-plan perk **stacking** (show lower-tier perks under the
current tier) is an unresolved Figma thread on this node — not
implemented.
---------
Co-authored-by: GitHub Action <action@github.com>
Adds a Cloudflare Turnstile widget to the email/password signup form
(web + desktop). The frontend renders the widget and attaches its token
to the signup request; the verification decision is made server-side.
## Design — config-driven, no origin sniffing
* The widget renders **iff** the `signup_turnstile` mode is `shadow` or
`enforce` **and** a `turnstile_sitekey` is present — both delivered via
cloud remote config. OSS / local builds receive no remote config, so it
never renders. Gating is a pure `isTurnstileEnabled(mode, siteKey)`; an
unknown mode normalizes to `off`.
* Submit is blocked only in **enforce**; **shadow** never blocks.
* The token is sent as `turnstile_token` (snake_case, optional) on the
customer-creation request.
* **OAuth** never renders the widget or sends a token (federated
providers are exempt).
## Behavior
* **Decision is server-side** — the frontend only renders the widget and
attaches the token; the backend verifies it and decides allow/block.
* **Mode-driven** — `off` (no-op) / `shadow` (render + attach, never
blocks) / `enforce` (blocks submit until solved).
* **Config-gated** — no `isCloud`/origin check in the client; the widget
is driven purely by the presence of the mode flag + sitekey in remote
config.
* **Fail-safe to off** — an unknown/missing mode or a missing sitekey
resolves to "don't render", so the feature is a no-op until both are
configured.
* The sitekey is a public, client-side value delivered per environment
via remote config; in dev it falls back to Cloudflare's always-pass test
sitekey.
## Files
New: `config/turnstile.ts`, `composables/auth/useTurnstile.ts` (+ test),
`composables/auth/turnstileScript.ts`,
`components/dialog/content/signin/TurnstileWidget.vue`. Edited:
`SignUpForm.vue`, `SignInContent.vue`, `useAuthActions.ts`,
`authStore.ts` (+ test), `remoteConfig/types.ts`,
`locales/en/main.json`.
## Flow
```mermaid
sequenceDiagram
actor U as User
participant FE as Signup form
participant CF as Cloudflare Turnstile
participant API as Backend signup API
Note over FE: renders only when mode is shadow or enforce<br/>and a sitekey is present
U->>FE: open email/password signup
FE->>CF: load widget with sitekey
CF-->>U: challenge (usually invisible)
U-->>CF: solve
CF-->>FE: token (single-use, short-lived)
U->>FE: submit
FE->>API: signup request with turnstile_token
Note over API: verifies the token server-side and<br/>decides allow/block (shadow never blocks)
API-->>FE: allowed, or blocked in enforce
```
## Rollout
Config-driven and a no-op until enabled:
1. **Merge + deploy** the FE — no visible change while the mode is `off`
/ no sitekey.
2. **Set** the `turnstile_sitekey` in remote config per environment.
3. **`signup_turnstile=shadow`** — the widget renders and attaches the
token; the server observes and never blocks.
4. → **`enforce`** — the FE blocks submit until the challenge is solved.
Kill switch: set the mode back to `off` and the widget stops rendering.
## Refactor: shared script loader
The Turnstile script loader was extracted to
`utils/loadExternalScript.ts` (`createScriptLoader`) and now also backs
the existing Typeform embed loader, removing duplicated
singleton/timeout/cleanup logic. Minor behavioral change: when a
matching `<script>` tag already exists in the DOM, the loader polls for
the global to become ready instead of attaching a `load` listener (which
may have already fired).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Add `Glary Bot` to the CLA Assistant allowlist.
## Context
PR #13146 is blocked because its commit author is `Glary Bot
<bot@glary.dev>`, which GitHub does not resolve to a GitHub user. The
CLA action checks the unresolved commit author name after failing to
find a linked GitHub account, so the existing `*[bot]` GitHub App
allowlist does not apply.
## Validation
- Ran `git diff --check`.
It's `github-actions` without the `[bot]`. This was blocking every PR
that contained updated browser test expectations.
Additionally, the action already included an allow list for every
account ending in `[bot]`. This made half the entries redundant.
## Summary
Remove the redundant **Enter Subgraph** action from Errors tab node
cards. This button should not be part of the updated Errors tab design;
it remained from the previous implementation and its removal was missed
when the new interaction model was introduced.
## Changes
- **What**: Removed the Errors tab `Enter Subgraph` button from
`ErrorNodeCard`, along with the `enterSubgraph` event plumbing in
`TabErrors`.
- Removed the now-unused `useFocusNode().enterSubgraph()` helper path,
since the Errors tab no longer has a separate subgraph-only action.
- Removed the `ErrorCardData.isSubgraphNode` flag and its population in
`useErrorGroups`, because it only existed to decide whether to show this
button.
- Removed the Storybook story and unit-test expectations that were
specifically tied to the removed button/flag.
- Removed the now-unused English `rightSidePanel.enterSubgraph` i18n
entry. Non-English locale files are intentionally left untouched per the
repo's localization update policy.
## Why
The Errors tab already has a **Locate node on canvas** action. For
errors inside subgraphs, that action navigates into the relevant
subgraph and centers the target node on the canvas. The removed **Enter
Subgraph** action was therefore a weaker duplicate: it entered the
subgraph and fit the view, but did not provide the same direct
target-node positioning.
Keeping both actions made the card UI more crowded and exposed two very
similar navigation paths with overlapping intent. The updated design
should only keep the more useful locate action, so this PR removes the
stale duplicate surface rather than adding another hidden/negative
assertion around it.
## Review Focus
Please verify that this only removes the Errors tab-specific action. The
normal node footer/canvas subgraph navigation behavior remains
untouched.
Validation run locally:
- `pnpm exec vitest run
src/components/rightSidePanel/errors/ErrorNodeCard.test.ts
src/components/rightSidePanel/errors/TabErrors.test.ts
src/components/rightSidePanel/errors/useErrorGroups.test.ts`
- `pnpm typecheck`
- `pnpm lint`
- `pnpm format:check`
- `pnpm knip`
## Screenshot
Before
<img width="335" height="595" alt="스크린샷 2026-06-25 오후 5 33 37"
src="https://github.com/user-attachments/assets/545f80e9-68bb-45ef-a4da-0a41012269f6"
/>
After
<img width="344" height="591" alt="스크린샷 2026-06-25 오후 5 34 24"
src="https://github.com/user-attachments/assets/7c1f1bf6-c5fd-4a43-9b5c-1392246070a8"
/>
## What this does
Collapses the personal-vs-team billing dispatch so it keys ONLY on the
build/flag (`teamWorkspacesEnabled ? 'workspace' : 'legacy'`). Personal
now flows through `useWorkspaceBilling` (`/api/billing/*`), same as team
("personal plan = single-seat workspace"). This converges status /
balance / subscribe / preview / cancel / portal in one change.
Dispatch sites collapsed:
- `useBillingContext.ts` — `type` computed: dropped the
`store.isInPersonalWorkspace` branch → flag-only.
- `useBillingContext.ts` — D3 subscription→store mirror watch: dropped
the `isInPersonalWorkspace` early-return guard so personal also mirrors
into the workspace store.
- `useSubscriptionDialog.ts` — `useWorkspaceVariant` compound predicate
→ flag-only (personal + flag-on now uses the workspace required-dialog
variant).
- `SubscriptionPanel.vue` — already flag-only
(`v-if="teamWorkspacesEnabled"`); no change needed.
## Kept (Risk #6)
- The ~11 raw `workspace.type === 'personal'` checks in
`teamWorkspaceStore.ts` — workspace-TYPE membership logic
(can-delete/leave, fetch-members, switcher), NOT billing dispatch.
Untouched.
- `useLegacyBilling` / `useSubscription` / authStore billing methods
kept intact for the flag-OFF (OSS/Desktop) path.
## Flag-off unchanged
Flag-OFF (OSS/Desktop) still selects `legacy` (`/customers/*`). Verified
by unit test.
## Tests
- `useBillingContext`: flag-ON → personal selects `workspace`; flag-OFF
→ `legacy`; D3 mirror now fires for personal under flag-on.
- `useSubscriptionDialog`: flag-ON → workspace required-dialog variant
for personal; flag-OFF → legacy personal variant.
## Follow-up (deferred, not in this PR)
Post-flip cutover deletion of `useLegacyBilling`-only components:
`PricingTable.vue`, `SubscriptionPanelContentLegacy.vue`,
`TopUpCreditsDialogContentLegacy.vue`, `CurrentUserPopoverLegacy.vue`,
`subscriptionCheckoutUtil.ts`, `useSubscriptionCancellationWatcher.ts`.
- Fixes part of FE-903 (B1)
Renders the inline **"Invite your team"** block in the team variant of
FE-934's "You're all set" success card
(`SubscriptionSuccessWorkspace.vue`), so a buyer can invite teammates
right after a team-plan upgrade (DES-394, Figma 3084-18651).
- New shared `InviteMembersForm.vue` (chips / `TagsInput` multi-email
form); seats capped via `useBillingContext.getMaxSeats(tierKey)`; submit
via `workspaceApi.createInvite`.
- Team upgrades only — personal / single-seat plans show the plain
success card; gated on `teamWorkspacesEnabled` + a team plan.
- `workspace_invite_sent` telemetry distinguishing a post-upgrade invite
from a Settings invite; success-card i18n + Storybook story.
**Stacked on FE-934 #12975** (`jaewon/fe-934-team-subscribe-wire`, the
success-card host). The PR base is that branch, so this diff is FE-965's
delta only. Re-using the same form in the Settings invite dialog is out
of scope here (belongs with FE-768 / a follow-up).
## Screenshots
| Team upgrade — invite block | Personal / non-team — no invite block |
|---|---|
| <img
src="https://github.com/user-attachments/assets/be5450fe-2b83-46bd-afbc-00e6d33590b7"
width="420" /> | <img
src="https://github.com/user-attachments/assets/a91909c7-7629-42ef-80b6-45fdb070a0e8"
width="420" /> |
Storybook: `Components/SubscriptionCheckoutSteps` →
`TeamSuccessWithInvite` (with block) / `SuccessAllSet` (without).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Promote / demote workspace members ↔ owners in Settings ▸ Members, per
[DES-222 / Figma
2993-15512](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=2993-15512)
and the [permissions section
3343-22966](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3343-22966).
- Fixes
[FE-770](https://linear.app/comfyorg/issue/FE-770/promote-demote-workspace-members-owners-settings-members)
- Stacked on #12759 (`jaewon/fe-768-members-invite-ui`)
## Changes
- Per-member row (…) menu → **Change role** submenu (Owner / Member,
current role check-marked) + existing **Remove member**, replacing the
shared PrimeVue `Menu` with the Reka `DropdownMenu`/`DropdownItem`
(submenu opens right of parent, flips on collision; scalable for future
roles).
- **Make [name] an owner?** / **Demote [name] to member?** confirm
dialogs (single `ChangeMemberRoleDialogContent`, copy 1:1 from Figma).
- `workspaceApi.updateMemberRole` → `PATCH
/api/workspace/members/:userId {role}` +
`teamWorkspaceStore.changeMemberRole` (local role map update; Role
column re-sorts).
- **Original-owner guards** (Figma annotations): creator pinned to the
top of the list, no row actions for anyone on that row; own row also has
no actions. Creator inferred as earliest `joined_at` until BE exposes an
explicit flag (tracked as the FE-770 BE blocker — same applies to the
endpoint itself, which does not exist yet; UI is wired to the proposed
contract).
- `DropdownMenu` raised to `z-3000` so the row menu sits above the
Settings modal (the Reka popper wrapper copies the content's computed
z-index; static `z-1700` lost to dialogs in the `@primeuix` modal
sequence). Also drops the always-rendered icon slot in `DropdownItem` so
icon-less items (Change role / Remove member) align flush-left.
## User stories verified
Viewer = an **owner** (promoted, not the workspace creator), so the
creator guard and the self guard are exercised separately.
| # | Click → action → expected |
| --- | --- |
| US1 | Member row (…) → menu shows **Change role ›** + **Remove
member** |
| US2 | Hover **Change role** → Owner / Member submenu, **current role
check-marked** |
| US3 | Click the current role (✓) → no dialog, no PATCH (no-op) |
| US4 | Member row → **Owner** → "Make {name} an owner?" + "They'll have
the same access as you — managing members, billing, and workspace
settings." + Cancel / **Make owner** |
| US5 | **Cancel** (or ✕) → dialog closes, role unchanged, no PATCH |
| US6 | **Make owner** → `PATCH /api/workspace/members/:id
{role:'owner'}` → Role column → Owner, row **re-sorts under the
creator**, "Role updated" toast, the promoted row keeps its (…) menu |
| US7 | Promoted owner row → **Member** → "Demote {name} to member?" +
"They'll lose admin access." → **Demote to member** → Role column back
to Member |
| US8 | **Creator row (earliest joined) has no (…) button** — even for
another owner |
| US9 | **Own (You) row has no (…) button** — even when not the creator
|
| US10 | PATCH 500 → "Failed to update role" toast, **dialog stays
open**, role unchanged |
| US11 | Viewer with `member` role → no row actions anywhere |
| US12 | **Remove member** → existing FE-768 "Remove this member?"
dialog |
## Tests
Each user story is covered by automated tests and confirmed by a manual
CDP pass driving the real cloud app (mocked auth + boot +
workspace/billing API).
| Story | Unit / Component | E2E (Playwright) | CDP (live app) |
| --- | :---: | :---: | :---: |
| US1 row menu shows Change role + Remove member | ✅ | ✅ | ✅ |
| US2 submenu checkmark follows current role | ✅ | ✅ | ✅ |
| US3 picking the current role is a no-op | ✅ | ✅ | ✅ |
| US4 promote dialog copy (Make owner) | ✅ | ✅ | ✅ |
| US5 Cancel leaves role unchanged, no PATCH | ✅ | ✅ | ✅ |
| US6 Make owner → PATCH, re-sort under creator, toast, stays demotable
| ✅ | ✅ | ✅ |
| US7 demote dialog (Demote to member) → role reverts | ✅ | ✅ | ✅ |
| US8 creator row has no (…) menu | ✅ | ✅ | ✅ |
| US9 own (You) row has no (…) menu | ✅ | ✅ | ✅ |
| US10 PATCH 500 → error toast, dialog stays open | ✅ | ✅ | ✅ |
| US11 member-role viewer sees no row actions | ✅ | — | — |
| US12 Remove member → FE-768 remove dialog | ✅ | ✅ | ✅ |
| Layer | File | What it covers | Result |
| --- | --- | --- | --- |
| E2E (`@cloud`) |
`browser_tests/tests/dialogs/memberRoleChange.spec.ts` | 3 tests — guard
rows (US1/US8/US9/US12), promote→re-sort→demote round trip (US3–US7),
failed PATCH (US10). FE-964 boot pattern: `CloudAuthHelper` +
remote-config flag mock + stateful route mocks capturing PATCH args.
Reka submenu driven via `ArrowRight` (synthetic hover doesn't open it).
| 3 / 3 green |
| Component | `ChangeMemberRoleDialogContent.test.ts` | promote/demote
copy, confirm → store + success toast + close, error keeps dialog open,
cancel | green |
| Component | `MembersPanelContent.test.ts` | creator/self rows hide the
menu (US8/US9), member-viewer gating (US11) | green |
| Composable | `useMembersPanel.test.ts` | menu factory
labels/checkmarks/commands, same-role no-op, creator pin in
`sortMembers`, `isOriginalOwner` | green |
| Store | `teamWorkspaceStore.test.ts` | `changeMemberRole`
success/failure, `originalOwnerId` inference | green |
| CDP live | full cloud app on `local.comfy.org` (mocked auth + boot) |
promote→re-sort→demote round trip with PATCH applied to mock state,
guard rows, submenu checkmark, dialog copy, menu/dialog z-index above
Settings, forced PATCH 500 → error toast | verified |
⚠️ Merge-gated on the BE role-change endpoint (no `PATCH
/workspace/members/:userId` in cloud OpenAPI as of 2026-06-10; see
FE-770 BE-blocker comment).
## Screenshots (local dev, workspace/billing API stubbed; vs Figma
2993-15512)
| Members (before) | Change role submenu |
| --- | --- |
| <img alt="members"
src="https://github.com/user-attachments/assets/686fec86-fcb5-4942-a745-50f367022ab0"
/> | <img alt="submenu"
src="https://github.com/user-attachments/assets/d6adeea8-7001-4c8d-91b7-f5bfc47a50d6"
/> |
| Promote dialog | After promote (Jane → Owner, still demotable) |
Demote dialog |
| --- | --- | --- |
| <img alt="promote"
src="https://github.com/user-attachments/assets/af638cde-2fd6-4c37-b203-78801eeb2785"
/> | <img alt="after"
src="https://github.com/user-attachments/assets/f47dc7af-6b1b-422c-8a9a-5ec889b9af11"
/> | <img alt="demote"
src="https://github.com/user-attachments/assets/9a861d04-a23b-4cd4-bc54-1ed3a66c6429"
/> |
## Summary
### AS IS
<img width="1340" height="798" alt="Screenshot 2026-06-10 at 12 22 36
AM"
src="https://github.com/user-attachments/assets/61636fa3-e80c-427b-855b-499e1eca67da"
/>
### TO BE
<img width="1301" height="793" alt="Screenshot 2026-06-10 at 12 22 39
AM"
src="https://github.com/user-attachments/assets/62d9f5a6-da92-45df-94e7-cd3c244249f9"
/>
### Empty states ([added to DES-247 on
2026-06-11](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3349-29750))
| 0 monthly credits | 0 credits |
| --- | --- |
| <img alt="credits-empty-monthly"
src="https://github.com/user-attachments/assets/b3c55d3b-79b0-47b1-9795-c8bf69d5efe2"
/> | <img alt="credits-empty-all"
src="https://github.com/user-attachments/assets/919081d6-64e1-483b-9c04-6b085243ebc1"
/> |
Consolidate the divergent Settings credits surfaces into one
facade-driven **CreditsTile**, implementing the DES-247 redesign so
personal and team modes always render the same balance from
`useBillingContext`.
## Changes
- **What**:
- New `CreditsTile.vue` — total + `remaining`, a stacked
monthly/additional progress bar, colored breakdown rows (`Monthly
(refills …)` / `Additional`), refresh, and a permission-gated *Add
credits* / *Upgrade to add credits* action. Owns the post-checkout
(`focus` / `pending_topup`) balance refresh.
- Extracted the duplicated inline credits card out of
`SubscriptionPanelContentWorkspace.vue` **and**
`SubscriptionPanelContentLegacy.vue` onto the shared tile.
- Replaced `LegacyCreditsPanel.vue` (read `authStore.balance` directly)
with `CreditsPanel.vue` routed through the tile; repointed
`useSettingUI` and deleted the legacy panel.
- `creditsProgress.ts` pure helper for the bar math + numeric credit
getters on `useSubscriptionCredits`.
- i18n keys for the unified tile labels.
- DES-247 responsive variants via CSS container queries: below ~350px
tile width the `{used} used` label, `remaining` suffix, and breakdown
subtitle drop and the additional-credits value stacks under its label;
below ~230px the monthly summary compacts (`105K left of 200K`).
Additional-credits tooltip copy aligned with the updated design (per
design feedback).
- Empty states (added to DES-247 via Slack on 2026-06-11, low priority):
once the monthly allowance is depleted, an info notice renders under the
total (`Monthly credits are used up. Refills {date}` / `You're now
spending additional credits.`), the monthly bar section dims to 30%
opacity, and an `IN USE` pill marks *Additional credits*; once
everything is depleted the notice switches to `You're out of credits.
Credits refill {date}` and *Add credits* swaps to the `inverted`
(filled-white) Button variant. Gated on a loaded balance so the notice
never flashes while fetching.
- **Dependencies**: Stacked on **#12622 (FE-904 / B2)** for the facade
`tier` / `renewalDate` fields — base this PR against that branch;
retarget to `main` once FE-904 merges.
## Review Focus
- The tile reads everything from the facade (`balance.*Micros` as cents
→ credits, `subscription.tier`/`renewalDate`), so legacy and workspace
modes share one source.
- Monthly allowance still comes from `getTierCredits` (hardcoded tier
nominal). With real data the monthly *remaining* can exceed the nominal
(rolled-over credits), so the bar clamps to a full segment — same
semantics as the prior `{monthly} / {planTotal}` display; the canonical
allowance is a BE-1047 follow-up.
- `LegacyCreditsPanel` deletion: `CreditsPanel.vue` retains the
usage-history table + help links and reads the facade.
## Testing
- Unit/component (36 green): `CreditsTile.test.ts` (render, zero-state,
free-tier, permission gating, add-credits, mount+manual refresh, plus
the empty states: depletion notice copy, `IN USE` badge, `inverted`
button when fully out, no-flash-while-loading guard),
`creditsProgress.test.ts` (clamping/stacking math),
`useSubscriptionCredits.test.ts` (`*_micros`-as-cents), and
`SubscriptionPanel.test.ts` updated for the extracted tile.
- E2E (`@cloud`): `browser_tests/tests/dialogs/creditsTile.spec.ts`
boots the cloud app against mocked Firebase auth + stubbed boot
endpoints (no backend) and asserts the tile's total / progress bar /
monthly+additional breakdown / add-credits in Settings ▸ Workspace ▸
Plan & Credits, then resizes to a narrow viewport and asserts the
responsive variant (labels hidden, compacted `11K left of 21K`). A
second test boots with a drained monthly balance (0-monthly notice + `IN
USE` badge), then re-mocks a fully drained balance and refreshes the
tile in place to assert the out-of-credits state. Both pass locally
against a cloud dev server; runs in the `cloud` CI project. Drives a raw
page because the shared `comfyPage` fixture expects the OSS devtools
backend.
- Screenshot-verified the tile at the three DES-247 reference widths
(448 / 235 / 204px) against the Figma Responsiveness section — 1:1.
- Verified live in the running app (Settings ▸ Workspace ▸ Plan &
Credits) against the authenticated backend — renders 1:1 with DES-247.
The empty-state screenshots above were captured the same way
(authenticated app, real Pro subscription, balance endpoint stubbed to
the depleted values via CDP).
- `pnpm typecheck` / `typecheck:browser` / `lint` / `knip` green.
Implements FE-964 (DES-247).
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Two related streams of FE-934 checkout/billing work on this branch:
https://github.com/user-attachments/assets/af629def-543e-4bcd-894d-b35aa032fe0a
1. **Team-plan subscribe** wired to the BE-1254 credit-stop contract
(replaces the "coming soon" toast stub).
2. **Checkout / confirm screen redesign** aligned to the updated DES
mockup — yearly pricing display, dialog navigation, and the plan-change
confirm.
<img width="1078" height="427" alt="team subscribe"
src="https://github.com/user-attachments/assets/2b5f7192-3c91-4e2d-b495-832ca5a26657"
/>
## Changes
### Team-plan subscribe (credit-stop contract)
- Team subscribe sends `{ plan_slug: team_per_credit_monthly |
team_per_credit_annual, team_credit_stop_id, billing_cycle }` to `POST
/api/billing/subscribe` and handles the response like the personal path
(`subscribed` → success, `needs_payment_method` → payment URL,
`pending_payment` → poll). Slider stops come from `GET
/api/billing/plans → team_credit_stops` via `mapApiTeamCreditStops`,
falling back to the hardcoded DES-197 stops so OSS / pre-deploy still
render. `preview-subscribe` is unchanged — the team confirm step is
display-only.
- **Internal API change**: `workspaceApi.subscribe(planSlug, returnUrl?,
cancelUrl?)` → `subscribe(planSlug, options?: SubscribeOptions)`; the
billing facade (`useBillingContext` / `useWorkspaceBilling` /
`useLegacyBilling`) and callers were updated to match.
### Checkout / confirm screen redesign (DES mockup)
- **Yearly confirm**: headline is now the ÷12 monthly-equivalent with a
`{total} Billed yearly` (yearly) / `Billed monthly` (monthly) subtitle;
credits show `Each year credits refill to` (×12) for yearly; `Starting
today` → `Starts today`. The team confirm now receives the active
billing cycle (the subtitle was missing because it wasn't passed), and
the redundant header credits/month line was dropped.
- **Navigation**: the pricing table stays mounted (`v-show`, not `v-if`)
so the plan / billing-cycle / credit-stop selection survives a round
trip to the confirm step and back; **Backspace** mirrors the back arrow
(ignored while an input/textarea/contenteditable is focused).
- **Plan-change confirm**: rewritten from the 2-column current→new
comparison to the **single-plan layout**, branching on
`previewData.is_immediate` — immediate upgrades show prorated line items
(`new_plan.price_cents − cost_today_cents` = credit) + upfront (yearly)
/ monthly credit refill + a prorated total ("Confirm upgrade");
scheduled downgrades show `Starts {date}`, $0 due today, and an "After
that" block ("Confirm change").
- **Storybook**: `SubscriptionCheckoutSteps` stories for each new-sub /
upgrade / downgrade variation (props-driven, no API).
## Review Focus
- **Merge gate (team subscribe)** — ✅ **resolved**: BE-1254 is now
merged in cloud `main`. The live `GET /api/billing/plans` returns
`team_credit_stops` (`TeamCreditStop` struct + validation in
`common/billing/catalog/catalog.go`, served for every workspace,
asserted by the `billing_credit_stops_contract` smoke test). The gate is
lifted.
- **Fallback safety**: with the contract live, the hardcoded DES-197
stops are now purely the OSS / pre-deploy fallback — they render only
when the API doesn't supply `team_credit_stops`. In that window a real
subscribe is still impossible (no stop `id`) and surfaces a
`teamPlan.unavailable` toast. Open question retained: hide/disable the
team CTA in that window instead of toasting?
- **Plan-change scope**: the single-plan confirm redesign covers
**personal** changes (the real `previewSubscribe` path). **Team** plan
changes route through the display-only team confirm and aren't wired to
`previewSubscribe` (BE left `PreviewSubscribeRequest` as `plan_slug`
only) — team-change proration is a follow-up.
- **Dead locale keys**: the old 2-column transition keys
(`everyMonthStarting`, `youllBeCharged`, `proratedRefund`,
`proratedCharge`, `creditsRefillTo`, `switchToPlan`, `starting`, `ends`,
`confirmPlanChange`) are now unused — can be removed in a follow-up
cleanup.
- **Out of scope**: the success "Your change is scheduled" variant from
the mockup.
- Based on `fe-934-unified-pricing-table`; base + `main` merged in to
resolve conflicts (PR is now mergeable).
## Verification
- Confirm / transition component tests green
(`SubscriptionAddPaymentPreviewWorkspace`,
`SubscriptionTransitionPreviewWorkspace`); oxlint / oxfmt clean locally;
full typecheck runs in CI.
- Each variation viewable in Storybook →
`Components/SubscriptionCheckoutSteps`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Behind `unified_cloud_auth` (default OFF), flip the two token accessors
so every cloud request rides the single Cloud JWT minted in PR2. This is
the consumer-flip phase of FE-950: PR2 built the dormant `unifiedToken`
slot; this PR makes consumers read it — and surfaces the
permanent-auth-failure path that the flip turns from graceful
degradation into a hard stop.
Stacked on #12704 (PR2), now merged; base is `main`.
## Changes
- **What**:
- `getAuthHeader()` — flag ON returns `{ Authorization: Bearer
<unifiedToken> }` (or `null` if unminted), with **no** Firebase/API-key
fallback. Flag OFF keeps the exact workspace → Firebase → API-key
cascade.
- `getAuthToken()` — flag ON returns the unified Cloud JWT (or
`undefined`); flag OFF keeps workspace → Firebase.
- Both accessors are the single seam every cloud consumer already routes
through, so the flip propagates automatically with **no edits** to
`fetchApi` (`scripts/api.ts`), `/customers/*` (authStore),
`workspaceApi`, the WebSocket (`api.ts:568`), or backend-node auth
(`app.ts:1593`).
- **Surface permanent auth failures** (answers @pythongosssss's review
on PR2). Under the flag there is no Firebase fallback, so a silent
`clearUnifiedContext()` wipe would strand every cloud request until
manual re-login — unlike the legacy path, which degrades to the Firebase
token. `refreshUnified()` and `mintAtLogin()` now emit a user-facing
error toast (keyed by error code off the existing `workspaceAuth.errors`
i18n) on the permanent codes (`ACCESS_DENIED` / `WORKSPACE_NOT_FOUND` /
`INVALID_FIREBASE_TOKEN` / `NOT_AUTHENTICATED`). `mintAtLogin()` now
resolves `false` on a permanent failure instead of rejecting an
unhandled `void`ed promise. Transient failures stay silent (proactive
refresh still retries). Also trims the verbose unified-lifecycle
comments flagged in review.
- **Breaking**: None. Flag OFF is byte-for-byte the current cascade.
## Review Focus
- **Single token, no fallback under the flag.** Tests assert
`getAuthHeader`/`getAuthToken` return only the unified token and never
call `getIdToken` or the API-key store; they return `null`/`undefined`
(not a fallback) when the token is unminted.
- **Surfacing, not recovery.** This PR makes the terminal state
*visible* (toast); the existing router auth-guard still redirects to
login on the next navigation. Active recovery (automatic re-mint on 401)
stays in the deferred safety-net PR so the toast is never a dead-end
"please re-login" with the fix one PR away.
- **Flag-OFF parity.** The full existing cascade suite runs with
`unifiedCloudAuthEnabled = false` (the `beforeEach` default) and stays
green.
## Deferred (intentional)
- **`acceptInvite` is left unchanged — still Firebase-authed.** It is
the one cloud call that intentionally keeps the raw Firebase token,
because the invite is accepted *before* the user is a member of the
target workspace. Promoting it to the unified Cloud JWT first needs a
quick check that `POST /invites/:token/accept` accepts a personal-scoped
Cloud JWT for a not-yet-member; deferred until that is verified.
`getFirebaseAuthHeader` / `getFirebaseAuthHeaderOrThrow` stay defined
(their removal belongs to the later cleanup ticket, FE-951). No
`workspaceApi.ts` change in this PR.
- **The reactive 401 re-mint + retry safety net is a follow-up.** A
clean place to intercept a `401` and re-mint once does not exist yet:
cloud requests use raw `fetch` (`/customers/*`, `/auth/token`) plus
several independent axios clients (`workspaceApi`,
`customerEventsService`, registry, manager), with no shared response
interceptor. PR2's `remintUnifiedOnce()` primitive is ready, and the
proactive buffer-based refresh (`refreshUnified`) already covers the
common token-*expiry* case, so this cross-cutting safety net (plus
deciding whether the surfacing toast escalates to a guided re-login CTA
once remint exists) lands in its own focused PR before any production
rollout. Note this is orthogonal to the surfacing above: proactive
refresh prevents expiry; it cannot prevent *revocation*, which is
exactly what triggers the now-surfaced permanent-error path.
## Tests
- Extended `authTokenPriority.test.ts`: flag-ON `getAuthHeader` returns
only the unified JWT (Firebase + API-key + workspace untouched) and
`null` when unminted; flag-ON `getAuthToken` returns the unified JWT
(not Firebase) and `undefined` when unminted. Existing cascade tests
prove flag-OFF parity.
- Added to `useWorkspaceAuth.test.ts` (red-green + regression lock): a
permanent refresh error toasts the **correct i18n key for each of the
four permanent codes** (`it.for` over 403/404/401 + a
lost-Firebase-token `NOT_AUTHENTICATED` case) and clears the slot; a
permanent login-mint error toasts and resolves `false`. Negative guards
prove the surfacing is **error-only and flag-scoped**: a transient (5xx)
refresh does **not** toast and keeps the slot, a **successful** re-mint
does not toast, and the unified lifecycle **never toasts when the flag
is OFF** (even against a rejecting backend).
## Red-Green Verification
| Commit | CI | Purpose |
|--------|-----|---------|
| `test: cover permanent unified-auth error surfacing` | 🔴
Red
([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27455949404))
| Proves the tests catch the silent-failure gap |
| `fix: surface permanent unified-auth errors instead of failing
silently` | 🟢 Green
([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27456200098))
| Proves the surfacing resolves it |
Part of FE-950 (single Cloud-JWT provider at login, Phase 1).
## Summary
Adds an in-app deep link that opens the pricing table directly, for
driving pilot users straight to subscribe (request from nav/Alex).
Resolves [FE-1104](https://linear.app/comfyorg/issue/FE-1104).
- `/?pricing=1` — on app load, open the pricing table.
- `/?pricing=team` / `/?pricing=personal` — open it on the Team /
Personal plan tab (via the existing `UnifiedPricingTable`
`initialPlanMode`).
- Gated to the **original owner** via
`useWorkspaceUI().permissions.canManageSubscriptionLifecycle` (personal
user, or a team workspace's original owner). A member or a promoted
owner is a **silent no-op**: the app loads normally, the param is
stripped, no 404 / error / toast.
- Off-cloud (OSS): the loader isn't instantiated, so the param is
ignored.
- Survives the login redirect via the preserved-query system, same as
`?invite` / `?create_workspace`.
## How
Mirrors the established URL-loader pattern (`useInviteUrlLoader` /
`useCreateWorkspaceUrlLoader`):
- `preservedQueryNamespaces.ts` / `router.ts` — register the `pricing`
namespace + tracker key.
- New `usePricingTableUrlLoader.ts` — hydrate preserved query, read
`pricing`, strip the param + `clearPreservedQuery` in a single replace
before any await, then `await fetchMembers()` (resolves the
original-owner gate; no-ops for personal) and open the table only when
the gate allows.
- `GraphCanvas.vue` — call the loader in `onMounted` after the
create-workspace loader (cloud only; not gated on the team-workspaces
flag so it also drives personal/legacy users).
- `useSubscriptionDialog.ts` — new `'deep_link'` value on
`SubscriptionDialogReason`.
## Telemetry
Eligible opens emit the existing `subscription_required_modal_opened`
PostHog event with the new `reason: 'deep_link'`. Ineligible-click
bounce rate is derivable from the autocaptured pageview URL
(`?pricing=…`), so no new event plumbing.
## Stacking / dependencies
This feature needs two sibling stacks off `main`:
- **FE-934** (`#12666`, base of this PR) — the `UnifiedPricingTable` +
`showPricingTable({ planMode })`.
- **FE-770** (`#12829`) — the `canManageSubscriptionLifecycle` gate.
**Merged into this branch**, so the diff against the FE-934 base
includes FE-770's changes until it lands. Review the single
`feat(billing): deep link…` commit. Once both land on `main`, rebase
onto `main` and the diff collapses to just this feature.
Do not merge before FE-770 and FE-934. Post-Billing-V1 follow-up.
End-state: swap the FE original-owner heuristic for the BE
workspace-level `is_original_owner` flag when it lands (removes the
members-fetch).
## Tests
- Unit (`usePricingTableUrlLoader.test.ts`, 12 cases): opens for an
original owner; `team`/`personal` tab preselect; silent no-op +
param-strip for a member/promoted owner; proves the gate is read only
after `fetchMembers` resolves; preserved-query restore;
empty/non-string/absent/unrecognized param; members-fetch failure
strips+clears without opening.
- E2E (`browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts`,
`@cloud`, 4 cases, verified locally): personal owner opens + URL
stripped; `?pricing=team` lands on the active Team tab; team original
owner opens (real `is_original_owner` + email gate); team member is a
silent no-op + URL stripped.
- Typecheck + related unit suites (`useSubscriptionDialog`,
`useWorkspaceUI`, `teamWorkspaceStore`) green.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Redesigns the Settings ▸ Workspaces Members invite flow to DES-186:
email-chips invite dialog, Resend/Cancel pending-invite actions, and
explicit team-plan gating.
## Changes
- **What**:
- `InviteMemberDialogContent`: single-email link-generation flow →
comma/Enter/paste-separated email chips (`TagsInput`); batch
`createInvite` via `Promise.allSettled` — on partial failure, failed
emails stay as chips with an error toast; success shows an Invited
state.
- `PendingInvitesList`: inline Copy-link/Revoke buttons → `⋯` overflow
menu with **Resend invite** / **Cancel invite**. New
`teamWorkspaceStore.resendInvite` issues the fresh invite *before*
revoking the old one, so a failed resend never destroys the original.
- FE-built invite links removed
(`getInviteLink`/`createInviteLink`/`copyInviteLink`,
`PendingInvite.token`) — invite delivery is BE email per DES-186.
- New `useTeamPlan` composable: explicit `isOnTeamPlan` (seat-based
`maxSeats > 1` proxy until BE-1254 exposes the plan signal) replaces
`isSingleSeatPlan` branching in
`WorkspacePanelContent`/`useMembersPanel`.
- Personal/no-team-plan state per Figma 2993-14604: "To add teammates,
upgrade your plan." banner + **Upgrade to Team**; "Need more members?
Contact us" footer; upsell dialog copy → Team-plan framing; members sort
by Role column (was Join date).
- **Header layout per design (2nd commit)**: `[Search][Invite +][⋯
workspace menu]` moved from the dialog tab row into the members card
header row (the "N of M members" row) — the tab row right side is empty
in every DES-186 frame. Workspace menu extracted to
`WorkspaceMenuButton.vue`.
- **Single-member gating per design annotations**: when the owner is
alone — search hidden ("Show if more than 1 members"), Active/Pending
segmented tabs hidden, role column hidden, and the Members tab label
drops its count ("Remove '(0)' as well if it's just 1 member"). Tabs
reappear if pending invites exist so they stay manageable.
- **Breaking**: `teamWorkspaceStore` invite-link API removed (no
external consumers found in-repo).
## Review Focus
- Resend semantics (create-first, then revoke) and pending-invites state
update in `teamWorkspaceStore.resendInvite`.
- Invite-link removal assumes BE sends invite emails (DES-186 annotation
"Resends the invite email"). If BE email delivery is not yet live, this
PR should wait on that confirmation.
- Gating matrix: team-active owner → enabled; team at seat cap →
disabled+tooltip; non-team plan → upsell dialog; personal workspace →
disabled.
- Workspace `⋯` menu (Edit/Delete/Leave) now lives only on the Members
tab card header, matching DES-186 — the Plan & Credits tab no longer
exposes it (design shows the plan-card `⋯` there instead, shipped via
FE-964/FE-768b).
## Screenshots
Captured on `local.comfy.org` dev (cloud-prod backend, authenticated
session; team-plan states use client-side XHR-level API stubs for
subscription/members/invites since the test workspaces are
unsubscribed).
| Flow | Before (main) | After (this PR) |
|---|---|---|
| Members list — team plan | <img width="400" alt="before members"
src="https://github.com/user-attachments/assets/be214b95-4783-47ff-9539-59c8a33b5eb9"
/> | <img width="400" alt="after: Search/Invite/menu in the card header
row, Role column, design demo data"
src="https://github.com/user-attachments/assets/841c89d5-2a29-4eed-9c72-4a5ee8bee9f4"
/> |
| Pending invites — row actions | <img width="400" alt="before pending:
inline copy-link/revoke icons"
src="https://github.com/user-attachments/assets/1703850e-86bc-4735-81b3-7530c01ad46f"
/> | <img width="400" alt="after pending: overflow menu with
Resend/Cancel invite"
src="https://github.com/user-attachments/assets/9b1bbe03-d82b-4cf6-86f2-e01d7b898ae7"
/> |
| Team workspace with 1 member | (same chrome as members list: search +
tabs always shown) | <img width="400" alt="after: no search/tabs/role,
tab label without count, Invite + menu in card row"
src="https://github.com/user-attachments/assets/22c7f68a-eb0f-49a9-a203-516cd9b7e02d"
/> |
| Invite dialog | <img width="400" alt="before: single email, Create
link"
src="https://github.com/user-attachments/assets/c19d8bbb-feb5-4fe4-8541-6d52e6ab6600"
/> | <img width="400" alt="after: comma-separated email chips"
src="https://github.com/user-attachments/assets/101fdc7b-d6e0-4f7d-8966-894bbf16b4aa"
/> |
| Invite dialog — submit result | (copies a link) | <img width="400"
alt="after: Invited success state"
src="https://github.com/user-attachments/assets/f539a88c-0250-434c-bf4a-6ce714b30398"
/> |
| Upsell — invite without team plan | <img width="400" alt="before:
subscription required, Creator plan copy"
src="https://github.com/user-attachments/assets/bb2cb9fd-f298-4cb0-b39a-6d59061dcea1"
/> | <img width="400" alt="after: Team plan required, Upgrade to Team"
src="https://github.com/user-attachments/assets/45170ed5-63bd-469a-af12-197a8b7e09ee"
/> |
| Personal workspace — Members tab | (create-workspace hint text) | <img
width="400" alt="after: upgrade banner + disabled Invite"
src="https://github.com/user-attachments/assets/a0ae664b-2d20-4d87-900d-7a36872ecde3"
/> |
Linear:
[FE-768](https://linear.app/comfyorg/issue/FE-768/updates-to-workspaces-tab-of-settings)
(Members half; Plan & Credits panel ships separately stacked on FE-964)
## Summary
- Point the website Windows desktop download URL at
`https://comfy.org/download/windows/nsis/x64`.
- Keep macOS on the existing ToDesktop URL.
- Update the download page smoke test to expect the new Windows href.
## Context
This is the frontend leg of the GTM-93 Windows MVP. ToDesktop still
controls `download.comfy.org`; instead of changing DNS, the website
sends Windows users to a controlled `comfy.org` proxy path that the
router PR handles. The proxy forwards to ToDesktop and adds a tokenized
`Content-Disposition` filename for Desktop to consume on Windows.
Linear:
https://linear.app/comfyorg/issue/GTM-93/fix-posthog-identify-call-unblock-funnel-attribution-desktop-funnel
Router PR: https://github.com/Comfy-Org/comfy-router/pull/33
Desktop PR: https://github.com/Comfy-Org/Comfy-Desktop/pull/1149
## Validation
- `pnpm --filter @comfyorg/website run typecheck`
- `pnpm --filter @comfyorg/website run build`
- `pnpm --filter @comfyorg/website exec playwright test
e2e/download.spec.ts`
- pre-commit: `pnpm typecheck`, `pnpm typecheck:website`
## What
**B3 — Repoint direct-bypass billing consumers to the facade.** Billing
data was read from the legacy `useSubscription` store / `authStore`
directly (empty or personal-only for team workspaces) instead of the
workspace-aware `useBillingContext` facade.
FE-933 (parent FE-903).
> **Stacked on #12622 (B2 / FE-904)** — depends on the facade `tier` /
`renewalDate` fields added there. Base is the B2 branch; retarget to
`main` once B2 merges.
## Repointed consumers
- **T3 — `SubscribeButton.vue`**: `subscribe_clicked` telemetry
`current_tier` ← facade `tier` (was wrong/empty for team users)
- **T4 — `PostHogTelemetryProvider.ts`**: PostHog `subscription_tier`
person property ← facade `tier` watch (tier-segmented analytics was
polluted for team users)
- **T5 — `FreeTierDialogContent.vue`**: next-refresh date ← facade raw
ISO `renewalDate`, formatted at the display site (the line silently
disappeared for team users)
- **`useSubscriptionActions.handleRefresh` + `SettingDialog`
credits-nav**: balance refresh ← facade `fetchBalance()` (was legacy
`/customers`-only `authActions.fetchBalance`)
- **`CurrentUserPopoverLegacy.vue`**: tier badge / balance / skeleton /
refreshes ← facade (`tier`, `balance`, `isLoading`, `fetchStatus`,
`fetchBalance`); tier name via shared `useWorkspaceTierLabel` instead of
a duplicated mapping
- **`PricingTable.vue`**: `isActiveSubscription` / `isFreeTier` / `tier`
/ yearly-vs-monthly ← facade; the billing-portal flow
(`accessBillingPortal` deep-links + proration) is intentionally
unchanged — facade `manageSubscription` is not behavior-identical
## Out of scope (triaged)
- `TopUpCreditsDialogContentLegacy` / `SubscriptionPanelContentLegacy` /
`useSubscriptionDialog` / cancellation watcher — legacy-mode-only
surfaces decommissioned by B1 (FE-966); repointing is churn, and
`useSubscriptionDialog` would create a legacy↔facade cycle
- `LegacyCreditsPanel` / `UserCredit` — deleted/orphaned by FE-964
(#12734); its successor `CreditsPanel.vue` keeps an
`authStore.lastBalanceUpdateTime` watch (no facade equivalent yet) —
follow-up after FE-964 lands
## Known semantic deltas (intentional, match shipped facade consumers)
- Balance-refresh failures no longer toast: legacy
`authActions.fetchBalance` wrapped errors with a toast; facade
`fetchBalance` rejections are void-ed, same as
`CurrentUserPopoverWorkspace` / `SubscriptionPanelContentWorkspace`.
Facade-level error surfacing is a follow-up.
- Popover skeleton keys on facade `isLoading` (init-time) rather than
per-fetch `isFetchingBalance`, matching the workspace popover.
## Tests
- New behavioral coverage: FreeTier renewal-date render/disappear,
popover tier badge + balance from facade, current-plan highlight from
facade tier+duration, facade-vs-legacy fetchBalance tripwire, PostHog
`subscription_tier` from facade tier.
- Local gates clean (typecheck / lint / format / dead-code); touched
unit files 71/71 pass.
## E2E coverage
Browser regression tests live in the stacked #12760
(`billingFacadeConsumers.spec.ts`, `@cloud`): avatar popover tier badge
+ balance, and the free-tier dialog renewal-date line (T5) rendered from
the facade. The team-user telemetry fixes (PostHog person property,
telemetry payload) are non-UI observables covered by unit tests that
mock only the facade and fail on revert.
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Fix the two current blockers that prevented `pnpm test:coverage` from
completing on `main`.
Stack order: 1/x
## Changes
- Mock `load3dAdvanced` in the lazy-loader test so coverage does not
import the real Load3DAdvanced UI graph.
- Track the active workflow status in `useWorkflowStatusDismissal` so
terminal statuses arriving after activation are cleared.
## Test Results
| | before | after |
| -- | -- | -- |
| `pnpm test:coverage` | ❌ failed, so the stack had no usable coverage
baseline | ✅ passed with 877 test files passed; 11,772 passed / 8
skipped |
| focused tests | `load3dLazy` timed out; `useWorkflowStatusDismissal`
failed its active-workflow status case | ✅ `load3dLazy`: 13 passed;
`useWorkflowStatusDismissal`: 4 passed |
## Coverage
| | before | after |
| -- | -- | -- |
| statements | unavailable | 62.84% |
| branches | unavailable | 53.03% |
| functions | unavailable | 56.94% |
| lines | unavailable | 64.05% |
Screenshots: N/A, no UI change.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit 94c4c9bac1. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
## Summary
Restore the unified "Choose a Plan" pricing dialog width — it was
collapsing to the default `md` (576px) frame, so the 1280px table
overflowed and rendered off-center with the right card clipped.
## Changes
- **What**: `showPricingTable` opens the unified dialog
(`SubscriptionRequiredDialogContentUnified`) with PrimeVue-path props
for sizing (`style: 'max-width: 95vw'` + `pt`). Since #12593 (FE-578
Phase 6a) made **Reka the default dialog renderer**, those props are
ignored — Reka sizes via `size`/`contentClass`, so the dialog fell back
to `size: 'md'` (`max-w-xl` = 576px). The content root's
`xl:w-[min(1280px,95vw)]` then overflowed the 576px box and shifted
off-center. Moved the width onto a Reka `contentClass` (`w-fit
max-w-[min(1280px,95vw)]`), matching the sibling subscription dialogs in
the same file.
## Review Focus
- **Regression origin**: the broken config landed when #12666 (FE-934,
UnifiedPricingTable) merged on top of #12593's reka-default flip while
still using the PrimeVue config. No merge conflict — the `style` line is
valid but dead, so it broke silently. FE-991 (#12792) predates #12593,
so it still rendered via PrimeVue and looked correct (matching the
report that it was fine there).
- **`w-fit` vs fixed width**: `w-fit` preserves the original "dialog
hugs its content per step" intent — the content root only sets the
1280px width on the pricing step, so confirm/success steps still shrink
instead of floating in a 1280px box.
- Out of scope: the legacy-team / flag-off paths share a PrimeVue
`style` shell and are likely affected the same way under Reka; left for
a follow-up (flag-off is the lower-priority OSS path).
## Verification
- Unit test `useSubscriptionDialog.test.ts` — red without the fix
(dialog has no `contentClass`), green with it.
- Verified live (cloud dev, viewport 1301px): box centered at 1236px
(95vw), no overflow, all three personal cards visible.
## Screenshots
Personal tab, viewport 1301px:
| Before | After |
| --- | --- |
| <img width="480" alt="before"
src="https://github.com/user-attachments/assets/e233fe00-f754-4e34-837f-cf6630ccbfb9"
/> | <img width="480" alt="after"
src="https://github.com/user-attachments/assets/dedd92b7-8707-4865-b7f3-289919043b48"
/> |
## Summary
Allows engineers to run their localhost frontend while choosing which
backend to point. This PR adds staging and prod as targets.
## Changes
- **What**: New NPM scripts: `dev:cloud:test`, `dev:cloud:staging`, and
`dev:cloud:prod`. `dev:cloud` points at `dev:cloud:test`
- **Breaking**: None
## Why
Currently, the testcloud environment is broken (backend config issue)
and doesn't allow going through the subscription registration process.
This also allows testing frontend code against backend changes being
staged for release, as well as against actual backend production code.
Under some circumstances, (particularly with pointerCancel events) a
drag operation could end without properly being cleaned up. When this
occurs, the bugged state would manifest in comical ways
- Nodes would 'run away' from the cursor
<img width="1024" height="1024" alt="AnimateDiff_00001"
src="https://github.com/user-attachments/assets/accfeac0-ce4c-4d8a-b3b8-6b243e8d5f8d"
/>
- Resizing the window could cause the zombie drag to move into the
autopan region which would result in nodes rapidly scrolling away.
<img width="1024" height="1024" alt="AnimateDiff_00002"
src="https://github.com/user-attachments/assets/e30629f4-ddea-4981-83d8-0037b3010ad5"
/>
This is resolved by adding more robust cleanup for canceled drag events.
This PR also cleanups a sizeable chunk of dead TransformPane code which
was unused.
## Summary
Various race conditions can cause `NullGraphError` to be thrown after
removing/converting a subgraph. This fix guards at call sites and
refactors to add a pre-removal phase before the graph is nulled.
## Changes
- **What**:
- add pre-detach event (node:before-removed) so reactive consumers can
drop references before node.graph is nulled
- move selection and Vue node-manager teardown to this event to
eliminate stale panel/render evaluations against detached nodes
- guard SubgraphNode promoted-widget paths resilient on detached access
and add regression coverage
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->
## Review Focus
Alternative considered approach:
- Guards: Guards were treating the symptom at every caller, and new
callers may appear that won't know about this edge case. Adding a new
hook for consumers to drop refs is safer than trying to guard every call
site - the ones that are left in are safetynets and not the primary fix.
- Large scale refactor (towards ADR0008) - requires additional
scaffolding to already be in place to implement effectively, this fix
simply adds a new hook and isnt incompatible with the projects future
goals
- Defer/remove/reorder graph null - The detach was explicitly added in
#8180 to ensure GC - delaying is fragile and may not resolve the issue,
difficult to prove and may surface a new race condition
- Make rootGraph nullable - would require 100s of references to be
updated, when `NullGraphError` was added in #8180 to throw a clear
message when the graph for a removed subgraph node was referenced,
potentially leading to other harder to track bugs without the exception
Tests:
- e2e test complexity is required to prove the issue happens, patching
calls to add artificial delays. This isn't great, but I could not find a
reliable way to recreate otherwise, unless we are happy to drop e2e and
keep only unit tests.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11804-fix-prevent-NullGraphError-on-subgraph-node-removal-3536d73d3650814e9183e17067cc0992)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: DrJKL <DrJKL0424@gmail.com>
## Summary
- Brand `NodeExecutionId` and `NodeLocatorId` as distinct required
string types.
- Route execution/locator ID construction through existing helper
functions instead of minting raw strings at call sites.
- Update tests and boundary parsing to use branded IDs without
conflating them with local `NodeId` values.
## Validation
- `pnpm typecheck`
- `pnpm test:unit src/types/nodeIdentification.test.ts
src/stores/executionStore.test.ts
src/renderer/extensions/vueNodes/components/NodeSlots.test.ts
src/composables/graph/useErrorClearingHooks.test.ts
src/platform/nodeReplacement/missingNodeScan.test.ts -- --runInBand`
- `pnpm exec eslint src/types/nodeIdentification.ts
src/utils/graphTraversalUtil.ts
src/platform/workflow/management/stores/workflowStore.ts
src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts
src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts
src/stores/workspace/favoritedWidgetsStore.ts
src/stores/nodeOutputStore.ts
src/utils/__tests__/executionErrorTestUtils.ts
src/platform/nodeReplacement/missingNodeScan.test.ts
src/stores/executionStore.test.ts --cache`
Note: full `pnpm lint` timed out after 5 minutes while still in
stylelint startup, so targeted lint was run on changed files.
## Open Question
- Should root-level node IDs like `1` be considered valid
`NodeExecutionId` values, or should `isNodeExecutionId()` require a
colon and callers use a separate type/helper for root execution IDs?
## Summary
Opening the Keybinding panel from the **Manage Shortcuts** button now
focuses the **Search Keybindings** field instead of the **Search
Settings** field.
## Changes
- **What**: The Settings dialog's "Search Settings" input had an
unconditional `autofocus`, so opening directly to the keybinding panel
always stole focus to the wrong field. Made it conditional
(`:autofocus="activeCategoryKey !== 'keybinding'"`) and added
`autofocus` to the keybinding panel's own search input.
## Review Focus
- `autofocus` maps to the native attribute, which only fires on DOM
insertion — flipping the reactive `:autofocus` while navigating between
categories inside the dialog will not re-steal focus, so there is no
regression for in-dialog navigation.
- Added an E2E test verified in both directions: it fails on the
original code (Search Settings focused) and passes with the fix (Search
Keybindings focused).
Fixes FE-845
Co-authored-by: Dante <bunggl@naver.com>
## Summary
Add two reusable node widgets backed by native (non-string) values:
- Bounding boxes editor (BOUNDING_BOXES): draw, select, resize, and
label regions over an optional background image. Value is a native list
of `{ x, y, width, height, metadata }` pixel boxes; the editor works in
normalized space internally and converts at the value boundary,
rescaling when the node's width/height change.
- Colors palette (COLORS): native `string[]` of hex colors, sharing the
PaletteSwatchRow component (usePaletteSwatchRow composable).
Both reactively hide the width/height widgets while a background image
is connected by writing through the widget value store so the Vue node
re-renders.
Some design refer to KJ's node
BE: https://github.com/Comfy-Org/ComfyUI/pull/14537
Screenshot
<img width="3019" height="1470" alt="image"
src="https://github.com/user-attachments/assets/06795772-97e6-4084-9205-e370f955fb28"
/>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
## Summary
Browsers cannot render EXR/HDR in <img>, so these outputs showed as
broken images. Add a full-screen three.js viewer holding a single WebGL
context created on open and released on close, opened via an 'Open in
HDR Viewer' action on EXR/HDR outputs in ImagePreview. The layout
mirrors the 3D viewer: canvas on the left, grouped controls in a
right-hand sidebar.
The display pipeline (gamut -> exposure -> linear-to-sRGB -> dither ->
clamp, plus clip warnings) is adapted from
[HDRView](https://github.com/wkjarosz/hdrview). Source gamut is
auto-detected from the EXR chromaticities attribute (Rec.709/Rec.2020)
with a manual override.
Inspection tools operate on the EXR float data kept on the CPU by
EXRLoader:
- pixel inspector: hover to read raw RGBA values and coordinates
- statistics: min/max/mean/std-dev plus NaN and Inf counts
- auto-exposure: set exposure so the max value maps to 1
- channel isolation: view R/G/B/A or luminance individually
## Screenshots (if applicable)
https://github.com/user-attachments/assets/22b80718-4b15-41ee-86b5-8fe38a6a82e2
## Summary
Sidebar tab labels no longer overflow the rail: they wrap up to 2 lines
max, then truncate with an ellipsis, with the full name always
recoverable via the hover tooltip (per design spec from Alex Tov in
FE-698).
## Changes
- **What**:
- Labels in `SidebarIcon.vue` now use `line-clamp-2` + `overflow-wrap:
break-word` + `whitespace-normal`, contained within the rail width minus
`--sidebar-padding` so text keeps breathing room from the rail border
(the base Button's `whitespace-nowrap` previously prevented any
wrapping, causing labels like "Input & Output" to be clipped on both
sides)
- Near-fit built-in labels ("Workflows", "Templates", "Shortcuts" —
wider than the floating-mode line) get soft hyphens (``) in their en
label strings, so they break cleanly as "Work-/flows" in floating mode
and render as a single unhyphenated line in connected mode (56px).
`hyphens: auto` can't do this because Chromium skips hyphenation for
capitalized words. Title/tooltip strings are untouched
- Tooltip falls back to the label when an extension registers a sidebar
tab without a tooltip, so clamped text is always recoverable on hover
## Review Focus
- Labels never bleed past the rail or get clipped by the rail's
`overflow-hidden`; long unbroken extension names (e.g.
`WASNodeSuitePreprocessors`) break mid-token across 2 lines + ellipsis,
matching the design mockup
- Soft hyphens live only in `sideToolbar.labels.*`, not in the
title/tooltip keys, so command palette / tooltip text stays clean
- No E2E regression test: the fix is pure CSS layout (line
wrapping/clamping), and per `AGENTS.md` testing guidelines we don't
write tests that depend on non-behavioral styling. The one behavioral
change (tooltip falls back to label) is covered by a unit test in
`SidebarIcon.test.ts`
Fixes
[FE-698](https://linear.app/comfyorg/issue/FE-698/bug-input-and-outputs-text-not-wrapping-in-left-sidebar)
---------
Co-authored-by: Dante <bunggl@naver.com>
## Summary
Bind asset-browser modal selections to the widget that actually opened
the modal, so promoted subgraph asset widgets commit through the host
promoted widget instead of the internal source widget closure.
## Changes
- **What**: Makes the asset-browser modal commit path widget-owned:
after a valid selection, `openModal` writes to the widget passed into
the modal and notifies that widget's callback.
- **What**: Captures workflow state after a successful value-changing
asset selection, because the async modal `Use` action can run after the
global mouseup-based change capture has already fired.
- **What**: Preserves existing asset-browser filtering by keeping
`nodeTypeForBrowser` and `inputNameForBrowser` captured in the asset
widget's existing modal options closure.
- **What**: Avoids adding promoted-widget-specific rebinding code to
`litegraphService` and avoids changing LiteGraph core widget option
types.
- **What**: Only runs the source widget's `onValueChange` callback when
the selected widget is the original owner widget created by
`createAssetWidget`.
- **What**: For cloned/transient host widgets, such as promoted subgraph
asset widgets, dispatches `onWidgetChanged` through the widget's owning
node instead of the internal source node.
- **What**: Removes the duplicate PrimitiveNode callback dispatch
because the asset modal commit path now centrally notifies the selected
widget callback.
- **What**: Adds stable asset-browser `data-testid`s and a cloud E2E
regression for legacy promoted subgraph asset selection.
- **What**: Adds unit coverage for both regular asset widget commits and
cloned promoted-host asset modal commits, including workflow change
capture.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
This PR supersedes #13074. The earlier direction treated the bug as a
missing callback bridge in the async asset-browser commit path, but the
ownership issue is more specific: promoted subgraph asset widgets reuse
modal options that were created from the deepest concrete source widget.
Those options still need to carry source metadata for filtering the
asset browser, but the modal's `Use` action must commit to the widget
that actually opened the modal.
This matters after the History ADR 0009 subgraph widget changes shipped
through #12197. In the 1.46 subgraph model, promoted widget values live
on the subgraph host node and are not synchronized back into the
internal widget. The internal source widget remains useful as the
provider of asset-browser metadata, because `SubgraphNode` already
resolves nested promotions down to the final concrete widget, but it
should not own the edit commit.
The final patch keeps that boundary narrow:
- no `IWidgetOptions` or LiteGraph core type changes;
- no asset-specific promoted-widget rebinding in `litegraphService`;
- no new promoted-widget traversal logic, because the existing subgraph
promotion path already resolves the final concrete source widget;
- the modal commit path uses the widget passed to `openModal` as the
value owner;
- successful async modal commits explicitly capture workflow state when
the selected value changes.
Please focus review on whether `createAssetWidget` now preserves regular
asset widget behavior while correctly handling cloned/transient host
widgets. The key distinction is that the source `onValueChange` path
only runs for the original owner widget; promoted host wrappers instead
rely on their callback bridge and owning node's `onWidgetChanged` hook.
A review pass also found that this PR makes an existing async modal
weakness more visible: asset-browser selection happens from the modal
button's `click` handler, while the global change tracker also captures
on `mouseup`. Depending on event ordering, the automatic capture can
occur before the selection mutates the widget. This PR now captures
workflow state immediately after a successful value-changing asset
selection so undo/modified tracking follows the same user-visible edit.
Local verification:
- `pnpm exec vitest run
src/platform/assets/utils/createAssetWidget.test.ts --reporter=dot`
- `pnpm exec vitest run
src/platform/assets/utils/createAssetWidget.test.ts --coverage
--reporter=dot --coverage.reporter=text
--coverage.include=src/platform/assets/utils/createAssetWidget.ts`
- `pnpm exec eslint src/platform/assets/utils/createAssetWidget.ts
src/platform/assets/utils/createAssetWidget.test.ts`
- `pnpm typecheck`
- `pnpm format:check`
- `pnpm build:cloud`
---------
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
## Summary
Remove redundant `if (isCloud)` guards around `useTelemetry()?.x()`
calls. `useTelemetry()` already returns `null` in OSS builds, so the
optional-chain calls no-op there — the guards only duplicated that
central contract.
## Changes
- **What**: Drop the `isCloud` guard wrapping telemetry calls across 9
files and remove the 5 now-unused `isCloud` imports (pure dedent —
implementations unchanged). Add two-path (cloud + OSS) characterization
tests for the two previously-uncovered composables
(`useTemplateWorkflows`, `useSubscriptionActions`).
## e2e
In local/OSS mode, useTelemetry() returns null, so no telemetry-related
behavior occurs, and the workflow loads as expected. There are no
local/OSS flow regressions for the exact template workflow paths touched
by the branch.
| before | after |
| -- | -- |
| <img width="1280" height="800" alt="before-01-templates-open"
src="https://github.com/user-attachments/assets/1cccc686-4e3a-4cf0-a578-a653a1383e3c"
/> | <img width="1280" height="800" alt="after-01-templates-open"
src="https://github.com/user-attachments/assets/ff834a58-4375-432a-8cc1-6e04ceeece77"
/> |
| <img width="1280" height="800" alt="before-02-template-loaded"
src="https://github.com/user-attachments/assets/1abd301b-d66d-4819-a0f3-9dff1a1e23b5"
/> | <img width="1280" height="800" alt="after-02-template-loaded"
src="https://github.com/user-attachments/assets/9fbb6903-c085-4744-b683-39b01680c654"
/> |
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Behavior is intended to be unchanged: OSS still no-ops via null
telemetry. Router page-view tracking may run in more build contexts but
remains guarded by optional chaining.
>
> **Overview**
> Removes duplicate **`if (isCloud)`** wrappers around
**`useTelemetry()?.…()`** across onboarding, auth, templates,
subscription UI, and routing. Call sites now rely on
**`useTelemetry()`** returning **`null`** in OSS (optional chaining
stays a no-op there), and several unused **`isCloud`** imports are
dropped.
>
> **`trackPageView`** in the router no longer bails early on cloud-only
or **`window`** checks; it always invokes
**`useTelemetry()?.trackPageView(...)`** on navigation.
>
> Adds characterization tests for **`useTemplateWorkflows`** and
**`useSubscriptionActions`** that assert telemetry fires when the mock
dispatcher is registered and does not when the mock simulates OSS
(**`useTelemetry()` → null**).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
fd6c9a56bd. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: ShihChi Huang <shh@theonlyperson.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
When using the ComfyUI (non-cloud) backend, selecting a grouped asset
(`outputCount > 1`) and clicking Download only downloaded the
cover/preview image instead of every output in the group.
The cloud backend already handles this correctly by exporting a ZIP
server-side. The OSS path called `downloadFile(asset.preview_url, ...)`
once per selected `AssetItem` and never expanded grouped assets into
their individual outputs.
## Fix
In `useMediaAssetActions.downloadAssets`, when any selected asset has
`outputCount > 1` and we're on the OSS path, resolve each grouped asset
into its individual outputs via the existing `resolveOutputAssetItems`
utility and trigger one direct download per file. Non-grouped selections
keep the original single-shot behaviour. After expansion the file list
is deduplicated by `AssetItem.id` so a user who selects both an expanded
stack parent and one of its children does not download the child twice.
The success toast now reflects the actual number of files downloaded.
- Single asset, single output → unchanged (1 download).
- Multi-select of single-output assets → unchanged (N downloads).
- Any selection containing a grouped asset → expanded via
`resolveOutputAssetItems` (same code path the cloud ZIP and
stack-expansion UI use). If resolution returns nothing, falls back to
the preview download so the user still gets something.
- Grouped parent + one of its expanded children selected → deduped, no
double download.
## Tests
Added unit tests in `useMediaAssetActions.test.ts` for the OSS path:
- Expands a grouped asset into individual downloads.
- Mixes grouped and single-output assets in one selection.
- Falls back to the original asset when `resolveOutputAssetItems`
returns empty.
- Does not call `resolveOutputAssetItems` when no grouped assets are
selected.
- Deduplicates downloads when an expanded child is also selected
alongside its parent.
- Shows an error toast when resolution rejects.
All 40 tests in the file pass; all 508 tests under `src/platform/assets`
pass. `pnpm typecheck`, `pnpm exec eslint`, `pnpm exec oxfmt --check`
all clean.
## Manual verification
Tested against a `master` ComfyUI instance with default settings.
Not tested against cloud - feature is gated to non cloud
Performed by @synap5e
<img width="448" height="618" alt="image"
src="https://github.com/user-attachments/assets/daf32fa0-c5ec-47ca-bab3-d5ea3fb3d7cc"
/>
https://github.com/user-attachments/assets/a87ae1aa-836f-4cbc-9ef7-a35ed4f94ee7https://github.com/user-attachments/assets/49d833bf-7b4e-4c53-b0d5-f16ff2108185
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Regenerates `packages/registry-types/src/comfyRegistryTypes.ts` from the
current comfy-api OpenAPI spec via `openapi-typescript`.
The FE registry types were last regenerated in May; this brings them up
to date. Notable addition consumed by #12924: the `createCustomer`
request body (`CreateCustomerRequest` with an optional
`turnstile_token`).
`pnpm typecheck` passes against the regenerated types.
## Summary
Reskins the Media Assets bulk-selection bar into a prominent floating
pill so bulk-download actions are no longer easy to miss (Linear
FE-989).
## Changes
- **What**: The selection bar now floats over the bottom of the panel as
an inverted rounded pill — close · "{count} selected" · download ·
divider · delete — matching the Figma/prototype spacing, radius, and
shadow. Actions are icon-only with `v-tooltip` hints; the count uses
`tabular-nums` and reflects selected assets (not total outputs).
Extracted into a presentational `MediaAssetSelectionBar.vue` with a
Storybook story, a unit test, and an e2e guard that the count is
per-asset.
## Review Focus
- The pill floats via `position: absolute` (centered, `bottom-6`,
`z-40`) in the sidebar footer slot and overlays the bottom of the grid
by design.
- Count semantics changed from total outputs to selected-asset count
(`selectedAssets.length`).
- Out of scope, deferred per FE-989: marquee / select-all, pagination,
and the favorites / tags / label controls.
## Screenshots (if applicable)
<img width="1003" height="1767" alt="image"
src="https://github.com/user-attachments/assets/3a9ef884-e7f4-4d0b-a495-194ce0860db2"
/>
<img width="643" height="581" alt="image"
src="https://github.com/user-attachments/assets/1161884f-a9c2-4a2b-a20e-33ee3f189935"
/>
<img width="664" height="222" alt="image"
src="https://github.com/user-attachments/assets/b16b083c-bfd9-452d-b508-86b3cbfa9842"
/>
<img width="649" height="265" alt="image"
src="https://github.com/user-attachments/assets/a1076e34-58c9-4e7f-89c4-b21bb3281883"
/>
<img width="559" height="205" alt="image"
src="https://github.com/user-attachments/assets/09c24140-33ce-4629-b681-233c59916043"
/>
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Wire the previously-dead `workspaceApi.getBillingEvents` (`GET
/api/billing/events`) into the existing usage/activity table behind
`teamWorkspacesEnabled`, so billing history can converge onto the
unified cloud feed (FE-969, "wire `GET /api/billing/events`" half).
## Changes
- **What**: `UsageLogsTable` sources events from
`workspaceApi.getBillingEvents` when `teamWorkspacesEnabled` is on, else
the legacy `customerEventsService` (`/customers/events`) — no change for
flag-off (legacy/personal). The two feeds share an identical response
envelope (`{total, events, page, limit, totalPages}`) and event object
(`{event_type, event_id, params, createdAt}`), so the table view is
reused unchanged (no design change). `topupTracker` now also recognizes
`topup_completed` so credit top-up telemetry works on the unified feed.
- **Breaking**: none (flag-gated; legacy path retained).
## Review Focus
This is a **draft** — it intentionally does NOT retire
`customerEventsService` yet. Two BE confirmations gate the rest:
**1. Does the store actually accumulate events?** For a personal
workspace today, `GET /api/billing/events` appears to hold only
`gpu_usage` (written directly by inference per `workspace_id`).
Webhook-sourced events (topup/subscription/invoice/credit) are dropped
unless the workspace is provisioned in the cloud billing system —
`GetWorkspaceBy{Stripe,Metronome}CustomerID` → "Not our customer,
ignoring". That provisioning is the BE-1047 cutover. Please confirm
which `event_type`s land in the store per env (e.g. pr-4359), personal
vs team.
**2. Are the incompatible fields compatible (or can they be made so)?**
The feeds use different vocabularies / `params` shapes:
- legacy: `credit_added` / `account_created` / `api_usage_*`, params
`{amount, api_name, model}`
- unified: `topup_completed` / `gpu_usage` / `api_node_usage` /
`invoice_*` / `subscription_activated` / `seat_*`, params e.g.
`gpu_usage {gpu_seconds, gpu_type, ...}`
The table degrades gracefully (badge falls back to the raw `event_type`;
the generic params tooltip still renders), but per-type labels/severity
and the details column need the confirmed `params` shapes before full
convergence + `customerEventsService` retirement.
Notes:
- Surfacing: on cloud with `subscription_required`, the Credits panel
hosting `UsageLogsTable` is hidden when `teamWorkspacesEnabled` is on
(team users see the workspace Plan & Credits panel, which has no
activity table). So this is plumbing; an in-app team history surface is
a separate design item.
- Full retirement of `customerEventsService` should land with the
BE-1047 cutover.
Local gates: unit tests (topupTracker + UsageLogsTable) pass, typecheck
/ oxlint / oxfmt clean.
---------
Co-authored-by: dante01yoon <dante01yoon@naver.com>
## Summary
Right-clicking a frame (group) now opens the new Vue context menu
instead of
the legacy litegraph menu, matching the three-dot menu and node
right-click.
## Changes
- **What**: In Nodes 2.0 mode, a group right-click is routed to the
existing
`showNodeOptions` flow (the same menu the three-dot button and node
right-click use) instead of litegraph's `processContextMenu`. The group
is
selected (unless already in the selection) so the menu targets it.
Nodes, the
canvas background, reroutes, and legacy rendering are unchanged.
## Review Focus
- App-layer wrap of `LGraphCanvas.prototype.processContextMenu`,
mirroring the
existing `useContextMenuTranslation` pattern: no new methods on
`LGraphCanvas` (ADR 0008).
- Gated on `LiteGraph.vueNodesMode`; legacy rendering keeps the old
menu,
consistent with legacy node right-click.
- Reroute guard: right-clicking a reroute inside a group still gets the
legacy
"Delete Reroute" menu, not the group menu.
Fixes FE-1090
## Screenshots (if applicable)
<img width="719" height="788" alt="image"
src="https://github.com/user-attachments/assets/8d514c6d-b7d0-4ec1-841e-677793daf3c7"
/>
---------
Co-authored-by: GitHub Action <action@github.com>
2026-06-23 21:32:36 +00:00
920 changed files with 78217 additions and 12149 deletions
- **New circular entity dependencies** — New circular imports between `LGraph` ↔ `Subgraph`, `LGraphNode` ↔ `LGraphCanvas`, or similar entity classes.
- **Direct `graph._version++`** — Mutating the private version counter directly instead of through a public API. Extensions already depend on this side-channel; it must become a proper API.
### Centralized Registries and ECS-Style Access
### Dedicated Stores and Data/Behavior Separation
All entity data access should move toward centralized query patterns, not instance property access.
Entity data lives in dedicated Pinia stores keyed by string IDs (`widgetValueStore`, `domWidgetStore`, `layoutStore`, `nodeOutputStore`, `subgraphNavigationStore`, `previewExposureStore`), not on entity instances.
Flag:
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that should be a component in the World, queried via `world.getComponent(entityId, ComponentType)`.
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that belongs in a dedicated store (e.g. widget values → `widgetValueStore` keyed by `WidgetId`).
- **OOP inheritance for entity modeling** — Extending entity classes with new subclasses instead of composing behavior through components and systems.
- **Scattered state** — New entity state stored in multiple locations (class properties, stores, local variables) instead of being consolidated in the World or in a single store.
- **Duplicated authority** — Storing the same entity state in both a class property and a store, or across two stores, so ownership becomes ambiguous. Each piece of state should have one owning store.
🎉 Thank you for your contribution, we really appreciate it! 🎉
Like many open source projects, we require contributors to sign our [Contributor License Agreement (CLA)](https://github.com/Comfy-Org/comfy-cla/blob/main/comfyui_icla.md). A CLA makes the ownership of contributions explicit, so contributors and the project share a clear understanding of how the code can be used. By signing, you:
- Confirm that you own your contribution.
- Keep the right to reuse your own code.
- Grant us a copyright license to include and share it within our projects.
CLAs are standard practice across major open source projects including those under the Apache Software Foundation and the Linux Foundation. Ours is based on the Apache Software Foundation's CLA. Most importantly, it would enable us to relicense the project under a more permissive license in the future, giving the project and its community greater flexibility.
✍ **To sign, please post a new comment on this PR with exactly the following text:** ✍
custom-pr-sign-comment:I have read and agree to the Contributor License Agreement
custom-allsigned-prcomment:|
✅ All contributors have signed the CLA. Thank you! This PR is ready to be merged.
- Keep commonly used technical terms in English when they are standard in Persian software (e.g., node, workflow).
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
- Maintain consistency with terminology used in Persian software and design applications.
IMPORTANT Hebrew Translation Guidelines:
- For 'he' locale: Use modern, formal Hebrew (עברית תקנית) for a professional tone throughout the UI.
- Hebrew is a right-to-left (RTL) language. Keep all interpolation placeholders ({name}, {count}), pipe-separated plural forms, and English technical terms intact and in their original positions.
- Use the real function directly, or introduce a named helper only when it adds validation, branching, domain meaning, or shared behavior beyond renaming
## Design Standards
@@ -246,7 +249,7 @@ All architectural decisions are documented in `docs/adr/`. Code changes must be
1. **Command pattern for all mutations**: Every entity state change must be a serializable, idempotent, deterministic command — replayable, undoable, and transmittable over CRDT. No imperative fire-and-forget mutation APIs. Systems produce command batches, not direct side effects.
2. **Centralized registries and ECS-style access**: Entity data lives in the World (centralized registry), queried via `world.getComponent(entityId, ComponentType)`. Do not add new instance properties/methods to entity classes. Do not use OOP inheritance for entity modeling.
2. **Dedicated stores over instance state**: Entity data lives in dedicated Pinia stores keyed by string IDs — widget values in `widgetValueStore` keyed by `WidgetId` (`graphId:nodeId:name`, see `src/types/widgetId.ts`), plus `domWidgetStore`, `layoutStore`, `nodeOutputStore`, `subgraphNavigationStore`, and `previewExposureStore`. Prefer a focused store to a single unified registry. Do not add new instance properties/methods to entity classes for data that belongs in a store. Do not use OOP inheritance for entity modeling.
3. **No god-object growth**: Do not add methods to `LGraphNode`, `LGraphCanvas`, `LGraph`, or `Subgraph`. Extract to systems, stores, or composables.
4. **Plain data components**: ECS components are plain data objects — no methods, no back-references to parent entities. Behavior belongs in systems (pure functions).
5. **Extension ecosystem impact**: Changes to entity callbacks (`onConnectionsChange`, `onRemoved`, `onAdded`, `onConnectInput/Output`, `onConfigure`, `onWidgetChanged`), `node.widgets` access, `node.serialize`, or `graph._version++` affect 40+ custom node repos and require migration guidance.
title: "How Groove Jones Delivered a Holiday FOOH Campaign for Dick's Sporting Goods with Comfy"
category: "CASE STUDY"
description: "Groove Jones, a Dallas-based creative studio, used Comfy to deliver a hyper-realistic FOOH holiday campaign for the Crocs x NFL collection on a fast-approaching deadline."
Groove Jones, a Dallas-based creative studio, builds AI-driven campaigns and immersive experiences for major brands where photoreal polish, creative ambition, and social-ready speed all have to land together. As their work expanded across AI Video, AR, VR, and WebGL for clients like Crocs, the NFL, and Dick’s Sporting Goods, they faced a recurring challenge: delivering feature-film-quality VFX on commercial timelines and budgets.
For the Crocs x NFL collection holiday launch, that challenge came to a head. The brief called for hyper-realistic video of giant NFL-licensed Crocs parachuting into real Dick’s Sporting Goods parking lots, across multiple locations, delivered on a fast-approaching holiday deadline. A live-action shoot plus a traditional CG pipeline was off the table.
</Section>
<Section id="topic-2" title="The Output Groove Jones Achieved Using Comfy">
- A full FOOH (faux out-of-home) social campaign delivered on a tight holiday deadline
- Vertical 9:16 deliverables at 2K for Instagram Reels, TikTok, and YouTube Shorts
- Same-day iteration on client notes instead of week-long asset updates
- Winner, Aaron Awards 2024: Best AI Workflow for Production
</Section>
<Section id="topic-3" title="The Problem Groove Jones Was Trying to Solve">
A traditional pipeline for this creative meant a live-action shoot at multiple store locations plus a full CG build: high-res modeling of every team’s clog, look development, lighting, rendering, compositing, and a new render every time the client wanted a variation. It also meant a large crew (modelers, texture artists, lighting artists, compositors) and a schedule measured in months. Neither the budget nor the holiday window supported that path.
</Section>
<Section id="topic-4" title="How Groove Jones Used Comfy to Solve the Problem">
Groove Jones’s Senior Creative Technologist, Doug Hogan, rebuilt the production process around Comfy’s node-based workflow system, using their proprietary GrooveTech GenVFX pipeline. Custom LoRAs handled brand accuracy, a single Comfy graph orchestrated multiple generative models, and Nuke handled final polish. For a team with feature-film and commercial roots, the environment was immediately familiar.
<Quote name="Doug Hogan | Senior Creative Technologist @ Groove Jones">Comfy felt very similar to working inside a traditional CG and compositing pipeline. Node-based logic, clear data flow, modular builds. It felt natural to our artists already.</Quote>
</Section>
<Section id="topic-5" title="Brand-Trained LoRAs for Hero Assets">
Groove Jones trained custom LoRAs on the Crocs NFL Team Clogs and on Dick’s Sporting Goods storefronts, so every generation came out anchored in brand-accurate references. Real team colorways, real product silhouettes, and real store exteriors stayed consistent across shots without per-frame correction, replacing what would normally take weeks of manual look development.
<Figure src="https://media.comfy.org/website/customers/groove-jones/nfl-crocs-team-lineup.webp" alt="Grid of brand-accurate NFL team Crocs generated via custom LoRAs" caption="Brand-accurate NFL team colorways generated through custom LoRAs." />
</Section>
<Section id="topic-6" title="Multi-Model Orchestration in a Single Graph">
The creative required different generative models at different stages: Flux for key-frame still development, Gemini Flash 2.5 (Nano Banana) for fast ideation and variants, and Veo 3.1 plus Moonvalley’s Marey for final video generation. Comfy routed between all four inside one graph, so outputs from one model fed directly into the next without ever leaving the environment.
<Quote name="Dale Carman | Co-founder @ Groove Jones">The Comfy community develops at an almost exponential curve, and we were able to leverage their existing nodes and tools to solve very specific production challenges instead of reinventing the wheel ourselves.</Quote>
</Section>
<Section id="topic-7" title="Storyboards to Previz to Final Shot in One Pipeline">
The workflow opened with traditional storyboards for narrative approval, then moved into CGI blocking to lock composition, camera framing, and story beats. Comfy drove generation from there: the shoe drop, the parking lot reactions, the crowd coverage, and the environmental conversions that turned static summer storefronts into snow-covered holiday scenes, all inside the same graph.
<Figure src="https://media.comfy.org/website/customers/groove-jones/nfl-crocs-dicks-storyboards.webp" alt="Storyboard grid for the Crocs x NFL holiday campaign" caption="Grayscale storyboards used to lock narrative beats before generation." />
<Figure src="https://media.comfy.org/website/customers/groove-jones/nfl-crocs-fooh-sequence.webp" alt="Composition progression from blocking to mid-render to final shot" caption="Composition progression: wireframe blocking, mid-render, and final shot." />
</Section>
<Section id="topic-8" title="Workflow Files as Version Control">
Every variant of every shot lived as a Comfy workflow file, which doubled as version control. When notes came in requesting a different team colorway, store exterior, or time of day, the team duplicated a branch instead of rebuilding, which made same-day iteration possible. GPU usage and API credit burn were trackable inside the same environment as the work itself, giving Production real-time visibility into compute cost per iteration.
</Section>
<Section id="topic-9" title="Finishing in Nuke">
Generated shots moved into Nuke for final compositing: falling snow, camera shake, crowd ambience, holiday audio, and 2K mastering in 9:16 for Instagram Reels, TikTok, and YouTube Shorts. Because Comfy handled generation cleanly, Nuke focused on polish and motion enhancement rather than patching generative artifacts.
</Section>
<Section id="topic-10" title="Conclusion">
By building the FOOH pipeline inside Comfy, Groove Jones turned a brief that would have required an expensive live-action shoot plus months of CG into a fast, iterative, single-environment workflow the client could direct in real time. The project recently won the Aaron Award for Best AI Workflow for Production.
<Quote name="Dale Carman | Co-founder @ Groove Jones">At Groove Jones, we care deeply about delivering work that makes people say WOW! But we also care about delivering on time and on budget. VFX projects used to operate at razor thin margins. Comfy solved that for us.</Quote>
title: "How Moment Factory Reimagined 3D Projection Mapping at Architectural Scale with ComfyUI"
category: "CASE STUDY"
description: "Moment Factory used ComfyUI to reimagine their 3D projection mapping pipeline, enabling architectural-scale visual experiences with AI-driven content generation and real-time iteration."
How do you make generative AI work at architectural scale? Moment Factory used ComfyUI to fundamentally transform how they handle early concept, look development, and design exploration for architectural projection mapping.
Before ComfyUI, this phase was slower, more abstract, and carried greater risk. After ComfyUI, it became faster, more concrete, and spatially grounded from the start.
<Figure src="https://media.comfy.org/website/customers/moment-factory/hero.webp" alt="Moment Factory architectural projection mapping" caption="Arched interior architectural projection by Moment Factory." />
</Section>
<Section id="topic-2" title="Before ComfyUI: Slow Iteration, Abstract Decisions, Late Risk">
Early concept and look development traditionally relied on:
- Static sketches
- Reference decks
- Moodboards
- Abstract discussions about intent
For architectural projection mapping, this creates a problem. You do not really know if something works until it is projected at scale. Seams, pixel density, spatial drift, and composition issues usually reveal themselves later in the process, when changes have a massive impact on production.
Traditionally, this means:
- Fewer directions explored
- Longer back-and-forth cycles
- Creative decisions made without spatial proof
- Risk pushed downstream into production
</Section>
<Section id="topic-3" title="What Changed with ComfyUI">
Moment Factory built a custom ComfyUI workflow and used it to enhance and accelerate large parts of early concept sketching, look-dev exploration, and part of the design phase.
They did not just generate images. They changed how decisions were made.
### 1. Iteration stopped being the bottleneck
ComfyUI transformed the iteration process, making it faster, sharper, and more intentional. Grounded in real production parameters, they explored:
- Over 20 main artistic directions
- 20 to 40 iterations per direction
- Styles ranging from hyper-realism to illustrative engraving
<Figure src="https://media.comfy.org/website/customers/moment-factory/variations.webp" alt="Grid of generated artistic variations" caption="A grid of generated variations exploring different artistic directions." />
The studio used batching and parameter tweaks to move quickly, while intentionally stress-testing the system to understand its limits.
<Quote name="Guillaume Borgomano | Senior Multimedia Director & Innovation Creative Lead @ Moment Factory">With any GenAI tool, it's easy to over-iterate, to believe the best result is always one click away. Imposing real production constraints, whether financial or time-based, was essential to ensure these explorations remained meaningful and truly impacted our pipelines.</Quote>
That volume of exploration would not have been realistic in their previous workflow.
### 2. Concept work moved from days to hours
The biggest acceleration happened early. What would normally involve days of back-and-forth between static concepts and reference decks could happen within a few hours.
They generated intentionally low-resolution outputs around 2K, reviewed them quickly, and even generated new variations live on site. Those outputs could be checked directly in the media server timeline minutes later.
This low-resolution stage was not about polish. It was about validation and decision-making. That shift alone changed the pace of the entire project.
### 3. Spatial credibility came first, not last
A major reason this worked is that every generation was already spatially constrained. Moment Factory built the entire workflow around architectural surface templates, so outputs were pre-mapped from the start. The pipeline supported multiple template types in parallel, including flat UVs, 360 layouts, and camera-projection setups.
ControlNet injected structural information from those templates directly into the diffusion process, enforcing scale, layout, and spatial logic early.
Because of this, visuals were already spatially credible during the concept phase. Abstract intent turned into shared reference points. The team could react to something grounded instead of imagining how it might look later.
### 4. Approval no longer meant starting over
Once a direction was approved, the workflow did not reset. They could:
- Inpaint specific regions
- Preserve composition
- Upscale selected outputs to 18K in ~20 minutes
This completely changed how fast ideas moved from concept to projection-ready content. Previously, approval often meant rebuilding work. With ComfyUI, approval meant pushing forward.
### 5. Fewer people, better collaboration
Once the system was stable, one main artist operated inside ComfyUI. Around that setup, two additional team members were continuously involved in art direction, prompt tuning, selection, and alignment discussions.
They had to define a new working methodology to keep creative intent at the center, but in practice, ComfyUI functioned as a shared exploration tool, not a solo technical setup.
### 6. The moment it became undeniable
Within Moment Factory's innovation team, it felt like a breakthrough early on — the level of malleability and control simply wasn't achievable with more rigid tools. But the real turning point came during an in-situ live demo, held at 25 Broadway. Late in the process, Moment Factory swapped the surface template and reran the entire pipeline without re-authoring a single asset. The composition held and the spatial logic remained intact. The content dropped straight into the media server timeline.
The room went quiet.
In that moment, it stopped being a promising experiment and became a shared realization. People weren't asking "what if" anymore — they were asking how to prompt, and in what other context it could apply.
That's when it became undeniable: this wasn't just a powerful tool for R&D. It was a shift in how teams across Moment Factory could think, iterate, and produce.
<Figure src="https://media.comfy.org/website/customers/moment-factory/demo.webp" alt="Moment Factory live projection mapping demo" caption="Interior crowd view with projection mapping at architectural scale." />
</Section>
<Section id="topic-4" title="Why ComfyUI Was Critical at Architectural Scale">
Moment Factory had been exploring diffusion-based workflows for projection mapping for years. The ambition was clear: use generative systems not just for images, but as structured spatial material within complex, large-scale environments.
What architectural scale demanded, however, was not just image generation. It required:
- Precise control over spatial conditioning
- The ability to inject UV layouts and depth constraints directly into inference
- Rapid template switching without breaking composition
- Iterative refinement without rebuilding from scratch
- A pipeline that could evolve as constraints changed
This level of structural malleability was essential.
ComfyUI's node-based architecture allowed the team to design and reshape the workflow itself, not just the outputs. Conditioning logic, batching strategies, template inputs, and upscaling stages could be reconfigured as the project evolved.
Rather than adapting the project to fit a tool, the tool could be adapted to fit the architecture.
At that point, it became clear: achieving reliable architectural-scale generative workflows required a system flexible enough to be re-authored alongside the creative process. ComfyUI provided that flexibility.
<Figure src="https://media.comfy.org/website/customers/moment-factory/workflow.webp" alt="ComfyUI node-based workflow" caption="Screenshot of the ComfyUI node-based workflow used by Moment Factory." />
</Section>
<Section id="topic-5" title="The Takeaway">
ComfyUI did not make the creative decisions. The vision stayed human. The constraints were architectural, and the expectations were production-level from the start.
What ComfyUI brought to the table was structural flexibility. It allowed the workflow itself to be shaped and reshaped as the project evolved. Spatial inputs could be injected directly into inference. Templates could be swapped without collapsing the composition. Refinements could happen without rebuilding entire directions.
Generative systems stopped behaving like black boxes and started behaving like controllable material. Spatial logic was embedded early, and scaling to architectural resolution became a managed step rather than a gamble.
The impact was not just speed. Decisions could be validated earlier, directly against geometry and projection conditions. Spatial alignment became part of concept development instead of a late-stage correction. That shift reduced uncertainty before entering production.
In that sense, ComfyUI did more than accelerate exploration. It made architectural-scale generative workflows structurally viable within real production constraints.
<Contributors label="MOMENT FACTORY CONTRIBUTORS" people={[{"name":"Guillaume Borgomano","role":"Senior Multimedia Director & Innovation Creative Lead"},{"name":"Conner Tozier","role":"Lead Motion Designer & Generative AI Lead"}]} />
title: "How Doodles, SYSTMS, and Open-Source Tools Like ComfyUI Are Rewriting the Rules for Artists"
category: "OPEN SOURCE × BRAND"
description: "Doodles and SYSTMS built Doodles AI — a generative platform powered by PRISM 1.0 — on open-source infrastructure including ComfyUI, proving that open-source workflows can power brand-quality, commercially successful products."
Doodles, the entertainment brand built around the iconic pastel-palette artwork of Canadian illustrator Scott Martin (known as Burnt Toast), is about to launch **Doodles AI** — a generative platform powered by **PRISM 1.0**, a generative image model trained on Doodles' extensive body of work that can reimagine people and objects in the unmistakable Doodles visual language.
Behind the scenes, the engineering is being handled by **SYSTMS**, an AI studio whose tagline — "Engineering the Impossible" — reflects their approach to building bespoke creative pipelines using open-source infrastructure, including node-based workflow tools like ComfyUI.
<Figure src="https://media.comfy.org/website/customers/open-story-movement/cover.webp" alt="Doodles AI generative platform powered by PRISM 1.0" caption="The Doodles AI platform reimagines people and objects in the Doodles visual language." />
The story of how these pieces came together offers a compelling blueprint for anyone watching the intersection of open-source, AI, artist-driven brands, and the emerging concept the Doodles team is calling "open story."
</Section>
<Section id="topic-2" title="IP Without Walls">
Artists have traditionally been protective of their IP, and for good reason. But the Doodles team is exploring a new model where the community doesn't just consume the brand — they co-create it. Every generation a user produces on the Doodles AI platform makes the model stronger.
Through reinforcement learning, user-generated content becomes part of the training data for future iterations of the PRISM. Users aren't just customers; they're collaborators shaping the brand's visual DNA.
<Figure src="https://media.comfy.org/website/customers/open-story-movement/walls.webp" alt="Doodles community co-creation" caption="Users become collaborators, co-creating the Doodles brand through AI-generated content." />
As Scott Martin put it when he returned as CEO in early 2025, the goal is to recalibrate — creativity first, community at the center, art driving everything. Martin, who built his career as an illustrator working with Google, Snapchat, Dropbox, and Adobe before co-founding Doodles in 2021 alongside Evan Keast and Jordan Castro, understands both the commercial and artistic sides of this equation.
</Section>
<Section id="topic-3" title="The Last Mile Is the Whole Game">
Doodles AI represents something powerful: proof that open-source tools can power commercially successful, brand-quality products.
The SYSTMS team uses open-source tools in their rawest form, prioritizing control and innovation at the bleeding edge of the space. The fact that these same tools are now producing output with the kind of brand fidelity that differentiates Doodles from generalized platforms like MidJourney or Sora is significant. It's the "last mile" problem in creative AI — getting from 85% to 100% fidelity — and it's where the real value lies.
Doodles AI is a showcase of what's possible when open-source workflows meet professional creative direction. ComfyUI's powerful node-based platform allows users to package complex systems of open-source models, APIs, and other tools into consumer-facing applications, making it a natural fit for projects like this.
Doodles AI launches with PRISM 1.0 as an image-to-image model, but the roadmap is ambitious: 2D and 3D output generation, video with sound, real-time AR, and gaming applications. Original Doodles holders receive 100 free generations on launch day — a deliberate move to seed the community and let them flood every timeline with the platform's output.
<Figure src="https://media.comfy.org/website/customers/open-story-movement/dna.webp" alt="Doodles AI output examples" caption="Doodles AI output demonstrating brand-fidelity generative results." />
The deeper play is alignment with the speed and scale of the entire AI industry. By building on open-source infrastructure and fostering a community of co-creators, Doodles has positioned itself to plug its "coded DNA" into future technologies that don't yet exist. It's a bet that openness — open source, open story, open creation — isn't just philosophically appealing but strategically sound.
</Section>
<Section id="topic-5" title="What It Means for Artists">
For artists watching from the sidelines, the message is clear: the building blocks are here, the community is building, and the line between creator and consumer is disappearing. The question isn't whether open source will reshape creative industries. It's whether you'll be building with it when it does.
<Figure src="https://media.comfy.org/website/customers/open-story-movement/output.webp" alt="Doodles AI creative output" caption="Open-source tools powering brand-quality creative output at scale." />
Series Entertainment builds story-driven games and short-form video experiences where characters, emotion, and visual consistency matter. As the scope of their work expanded across internal projects, partner collaborations, and Netflix titles, the team faced a growing challenge: they needed to produce more content, across more projects, without slowing down or losing consistency.
To meet that challenge, Series leveraged ComfyUI to scale their workflows. By building custom, repeatable workflows on top of ComfyUI, Series changed how they create characters, emotions, and video. The result was a scalable production system that supported over 100,000 assets, shipped Netflix games, and continues to power multiple projects in active development.
<Figure src="https://media.comfy.org/website/customers/series-entertainment/series.webp" alt="Series Entertainment game titles including Olympus Rising, Gilded Scales, Evergrove, and The Wandering Teahouse" caption="Series Entertainment produces story-driven games and video experiences across multiple titles and visual styles." />
</Section>
<Section id="topic-2" title="The Output Series Achieved Using ComfyUI">
With ComfyUI integrated into its production workflows, Series achieved:
- 100,000+ assets generated across games and video
- 180× faster production speed
- Six distinct character emotions generated in seconds
- 15 minutes of final video per creator per week
- Multiple Netflix titles shipped, with many more experiences in active development
These outputs span character assets, emotional variations, background consistency, and short-form video — all created through repeatable ComfyUI-powered workflows.
</Section>
<Section id="topic-3" title="The Problem Series Was Trying to Solve">
Series' work depends on expressive characters and consistent visual identity. As projects grew in size and complexity, the team needed a way to scale content creation without breaking timelines.
Traditional animation workflows rely on manual keyframing, multiple disconnected tools, and long production cycles that can stretch into weeks per video. Producing variations often means redoing work from scratch, and experimentation can be slow and expensive.
Series needed workflows that could be reused across teams and projects, while still supporting emotional storytelling, character consistency, and fast iteration.
</Section>
<Section id="topic-4" title="How Series Used ComfyUI to Solve the Problem">
Series rebuilt their production process around ComfyUI's node-based workflow system. Instead of treating generation as a one-off step, they treated workflows as long-term production assets. ComfyUI became the place where creative structure lived — from character creation to emotion generation to video output.
### Emotion Generation at Scale
Series built a custom avatar system using ComfyUI that generates six distinct emotions in seconds: Happy, Sad, Serious, Snarky, Thinking, and Surprised. This made it possible to create expressive characters with multiple emotional states without manually recreating each variation.
<Figure src="https://media.comfy.org/website/customers/series-entertainment/panel.webp" alt="ComfyUI Expression Editor node for facial expression manipulation" caption="The Expression Editor node in ComfyUI enables fine-grained control over character emotions." />
### Replicable Pipelines from Test to Production
Using ComfyUI's modular node system, Series built four streamlined pipelines that support the full production cycle — from early exploration to final output. These workflows deliver results up to **180× faster** than traditional manual processes that can take six hours or more per asset, while maintaining production quality.
The pipelines range from quick 512×512 single-emotion tests to high-resolution batch generation, allowing teams to experiment quickly and move directly into production using the same workflows.
<Figure src="https://media.comfy.org/website/customers/series-entertainment/workflows.webp" alt="ComfyUI workflow for facial expression manipulation and upscaling pipeline" caption="A ComfyUI workflow showing parallel expression editing, upscaling, and face detailing pipelines." />
### Consistency Across Games and Branching Stories
For multiple Netflix titles, Series used ComfyUI to build workflows that keep characters and backgrounds consistent across complex, branching narratives. Styling and consistency pipelines help ensure that characters stay visually aligned across scenes, emotions, and story paths — even as asset counts grow.
<Figure src="https://media.comfy.org/website/customers/series-entertainment/consistency.webp" alt="Consistent character across multiple scenes and emotional states" caption="A single character maintained across six different scenes and emotional states using ComfyUI consistency pipelines." />
### Production at Scale with ComfyUI
Series also uses ComfyUI as part of an AI-assisted animation pipeline that connects story development directly to image and video generation. This pipeline includes bot-assisted video generation, allowing creators to repeatedly run the same workflows to produce video efficiently. Using this approach, each creator can generate Lorespark videos at scale, delivering over **15 minutes of final video per week**.
<Figure src="https://media.comfy.org/website/customers/series-entertainment/batch.webp" alt="ComfyUI batch processing workflow using Nano Banana and Google Gemini" caption="A batch processing workflow connecting multiple character images to Nano Banana for style-consistent generation." />
</Section>
<Section id="topic-5" title="Why ComfyUI Worked for Series">
ComfyUI worked well because its node-based structure makes workflows explicit and reusable — once a workflow is built, it can be refined and shared across projects. This allowed Series to turn video generation into a repeatable system rather than a one-off process.
Batch execution and bot integration allow those workflows to run at scale. Because the same workflows support both low-resolution testing and high-resolution final output, teams can move from exploration to delivery without switching tools or rebuilding pipelines.
Most importantly, ComfyUI let Series focus on building structure instead of relying on trial-and-error prompting. Emotions, consistency, and production logic live inside the workflows themselves.
<Figure src="https://media.comfy.org/website/customers/series-entertainment/scale.webp" alt="Six variations of the same character generated with consistent style" caption="Multiple pose and expression variations of a single character, generated at scale while maintaining visual consistency." />
</Section>
<Section id="topic-6" title="Conclusion">
By making ComfyUI a core creative platform, Series Entertainment transformed how it produces games and video. What started as a need for scale and consistency became a workflow-driven production system that supports emotional storytelling, large asset volumes, and ongoing development across multiple teams.
<Quote name="Series Entertainment">For Series, ComfyUI is not an experiment. It is how entertainment gets made.</Quote>
title: "Ubisoft Open-Sources the CHORD Model with ComfyUI for AAA PBR Material Generation"
category: "AAA GAME PRODUCTION"
description: "Ubisoft La Forge open-sourced its CHORD PBR material estimation model with ComfyUI custom nodes, enabling end-to-end texture generation workflows for AAA game production."
Ubisoft La Forge has open-sourced its PBR material estimation model, **CHORD (Chain of Rendering Decomposition)**, together with **ComfyUI-Chord** custom node implementation to build an end-to-end material generation workflow with AI.
The model weights and code are released with a Research-Only license. Beyond research, this is a significant step toward integrating ComfyUI into AAA-scale video game production workflows.
<Figure src="https://media.comfy.org/website/customers/ubisoft/cover.webp" alt="CHORD PBR material generation in ComfyUI" caption="PBR materials generated using the CHORD model in ComfyUI." />
</Section>
<Section id="topic-2" title="PBR Material Production in AAA Games Today">
In AAA game development, PBR materials are the foundation of visual realism. Large-scale titles require hundreds of reusable materials, each with full Base Color, Normal, Height, Roughness, and Metalness maps that meet strict svBRDF standards.
Traditionally, these assets are crafted by texture artists using photogrammetry, procedural tools, and extensive manual tuning — making the process time-consuming and highly expertise-dependent.
Ubisoft's Generative Base Material prototype directly targets this production bottleneck. The ComfyUI workflow outputs PBR texture sets that integrate directly into DCC tools and game engines for prototyping and placeholder assets.
</Section>
<Section id="topic-3" title="Why Ubisoft Chose ComfyUI as The Workflow Platform">
Ubisoft's choice of ComfyUI is rooted in production realities. For large studios, the requirement is not another image generator — it is a controllable and integratable AI workflow platform that can meet the bespoke requirements of game development.
<Quote name="Ubisoft La Forge Blog">Considering the multi-stage nature of our prototype, ComfyUI provides us with an efficient framework to build integrated workflows doing texture image synthesis, material estimation and material upscaling. This also enables us to leverage state-of-the-art generative models and the powerful features of ComfyUI that provide fine-grain control to creators with ControlNets, image guidance, inpainting, and countless other options.</Quote>
</Section>
<Section id="topic-4" title="3 Stages of The Generative Base Material Pipeline">
The CHORD model is integrated into a broader pipeline consisting of 3 core stages.
<Figure src="https://media.comfy.org/website/customers/ubisoft/pipeline.webp" alt="The 3-stage generative base material pipeline" caption="The 3-stage generative base material pipeline: texture generation, CHORD estimation, and upscaling." />
### Stage 1 — Texture Image Generation
The first stage generates seamless, tileable 2D textures from text prompts or reference inputs such as lineart and height maps using a custom diffusion model with full conditional control.
### Stage 2 — CHORD Image-to-Material Estimation
A single texture is converted into a full set of PBR maps — including Base Color, Normal, Height, Roughness, and Metalness — using chained decomposition, unified multi-modal prediction, and efficient single-step diffusion inference for controllable and scalable results.
### Stage 3 — Material Upscaling
Since CHORD operates optimally at 1024 resolution, the third stage applies industrial-grade PBR upscaling. All channels are upscaled by 2x or 4x to produce 2K and 4K texture assets for real-time game production.
This complete pipeline enables artists to rapidly iterate on ideas and mix and match AI-generated outputs within their existing workflows, lowering the barrier to industrial-grade PBR material creation.
</Section>
<Section id="topic-5" title="How to Try CHORD in ComfyUI">
Ubisoft has open-sourced the CHORD model weights, ComfyUI custom nodes, and example workflows covering the texture image generation stage and the image-to-material estimation stage of the pipeline.
<Figure src="https://media.comfy.org/website/customers/ubisoft/workflow.webp" alt="CHORD example workflow in ComfyUI" caption="The CHORD example workflow in ComfyUI for end-to-end PBR material generation." />
<Steps items={["Install or update ComfyUI to the latest version","Install the CHORD ComfyUI custom node from Ubisoft","Download the CHORD model and place it in ./ComfyUI/models/checkpoints","Load the CHORD example workflow in ComfyUI"]} />
You can switch the texture image generation model to any other image model, and use the workflow modules for each stage separately.
</Section>
<Section id="topic-6" title="Example Outputs">
<Figure src="https://media.comfy.org/website/customers/ubisoft/example1.webp" alt="CHORD PBR material example output 1" caption="Generated PBR material set showing Base Color, Normal, Height, Roughness, and Metalness maps." />
<Figure src="https://media.comfy.org/website/customers/ubisoft/example2.webp" alt="CHORD PBR material example output 2" caption="Another generated PBR material set demonstrating the variety of textures achievable with CHORD." />
<Figure src="https://media.comfy.org/website/customers/ubisoft/example3.webp" alt="CHORD PBR material example output 3" caption="Material generation output with full PBR channel decomposition." />
<Figure src="https://media.comfy.org/website/customers/ubisoft/example4.webp" alt="CHORD PBR material example output 4" caption="High-quality PBR texture set generated from a single input texture." />
<Figure src="https://media.comfy.org/website/customers/ubisoft/example5.webp" alt="CHORD PBR material example output 5" caption="Final rendered PBR material demonstrating production-ready quality." />
The release of CHORD demonstrates how ComfyUI has grown from a community-driven tool into a platform for real production. Studio users can build end-to-end pipelines from prompt or reference input through texture generation, material estimation, PBR upscaling, and finally export to DCC tools or game engines. Each stage can also operate independently and be embedded into an existing production system.
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.