*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>
## Problem
The **CI: E2E Coverage** `merge` job fails in the *Generate HTML
coverage report* step ([example
run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28051468752)):
```
genhtml: ERROR: localhost-8188/assets/nodeDefs-BNhq_6cm.js is not readable or doesn't exist.
##[error]Process completed with exit code 1.
```
V8/Playwright coverage records scripts the test server serves at
`localhost-8188/assets/*.js` (built bundles), which are not source files
on disk. `genhtml` aborts on the missing source even with
`--ignore-errors source`, so the whole job fails.
## Change
Add a *Strip non-source entries from coverage* step that runs `lcov
--remove '*localhost-8188*'` on the merged lcov. It is placed **after**
the data-loss validation (so the merged-vs-shard integrity check stays
consistent) and **before** the Codecov upload and genhtml — which also
makes the Codecov report more accurate, since those served bundles
aren't repo source.
## Validation
- YAML parses cleanly; pre-commit lint/format/typecheck pass.
- The removed paths are non-source served assets only; real `src/**`
entries (absolute paths under the repo workspace) are untouched by the
`*localhost-8188*` pattern.
- `--ignore-errors unused` guards against runs where no such entries
exist.
## Summary
The **Docstring Coverage** pre-merge check is currently blocking merges.
It is not defined in our `.coderabbit.yaml` — it is enabled via
CodeRabbit **organization-level settings** (this repo has "use
organization settings" toggled on, but the repo config file overrides
specific keys).
This PR explicitly opts the repo out of that check by setting
`reviews.pre_merge_checks.docstrings.mode: 'off'`. A repo-level
`.coderabbit.yaml` takes precedence over org settings, so this disables
the check **only for this repo** — other org repos that want it keep it.
```yaml
reviews:
pre_merge_checks:
docstrings:
mode: 'off'
```
## Why
- Docstring coverage is a poor fit for this TS/Vue frontend codebase and
was added at the org level, not chosen here.
- It's a blocking gate with no repo-level config, so it can't currently
be tuned from the dashboard without affecting other repos.
## References
- [Built-in pre-merge
checks](https://docs.coderabbit.ai/pr-reviews/pre-merge-checks)
- [Configuration
reference](https://docs.coderabbit.ai/reference/configuration)
- Prior art:
[crossplane/.coderabbit.yaml](https://github.com/crossplane/crossplane/blob/main/.coderabbit.yaml)
disables docstrings the same way
## Summary
Logged-out users who open a share link and then sign up/in were not
attributed to the share. The `share_id` capture lived in
`useSharedWorkflowUrlLoader`, which only runs after `GraphView` mounts —
i.e. after the cloud auth guard has already redirected the logged-out
user to login. The capture never happened, so `trackAuth` fired without
a `share_id`.
This moves the capture into the cloud auth guard (`router.beforeEach`),
so it runs on the initial navigation before any login redirect. The
`share_id` is preserved across the auth round-trip and consumed on auth
completion as before.
## Changes
- Capture logged-out share attribution in the router guard instead of
the share loader, via a new `preserveLoggedOutShareAuthAttribution` util
- Extract `isValidShareId` into the shared util and reuse it in the
loader (removes the duplicated regex)
- Gate capture on `isCloud` (matching the cloud-only consumption); drop
the now-dead capture branch from the loader
- Make the accepted share-id shape explicit: ASCII alphanumeric start,
ASCII alphanumeric/`_.-` after that, max 128 chars
## Notes
- Capture no-ops when `share` is absent, so param-less redirects do not
clear attribution
- If another valid share link is visited before auth completes, the
latest valid share replaces the previous attribution
- `SHARE` and `SHARE_AUTH` stay separate intentionally: `SHARE`
preserves the workflow-loading query, while `SHARE_AUTH` is consumed
once by auth telemetry attribution
- No behavior change for logged-in users or for share-dialog open/cancel
## Testing
- New unit tests for `isValidShareId` and
`preserveLoggedOutShareAuthAttribution`
(valid/invalid/array/logged-in/boundary cases)
- Auth store tests cover `share_id` propagation + consumption across
email signup/login, Google, and GitHub
- Updated loader and telemetry tests for the relocated capture and
`share_id` passthrough
- Cloud E2E regression covers logged-out `/?share=abc` redirecting to
login after capturing share auth attribution
## Problem
When the auth backend rejects a sign-up, the Firebase client SDK
surfaces the rejection wrapped in a generic error code (e.g.
`auth/internal-error`). `reportError` maps only by `error.code` →
`auth.errors.${code}`, so this case falls through to the generic
*"Something went wrong while signing you in. Please try again."* toast —
giving the user no actionable guidance.
## Change
- `useAuthActions.ts` — add a branch in `reportError` that matches a
stable `signup_blocked` token in `error.message` (matched
**case-insensitively** on the message, not `error.code`, which the SDK
wraps inconsistently across versions) and shows a dedicated message.
- `locales/en/main.json` — new `auth.errors.signupBlocked` string. Other
locales inherit it via `fallbackLocale: 'en'`.
- `useAuthActions.test.ts` — unit tests asserting the token path wins
over the generic fallback, and that the match is case-insensitive.
## Validation
- `vitest run src/composables/auth` — 12/12 pass (incl. token +
case-insensitive tests)
- `vue-tsc --noEmit` — clean
## Why no E2E (Playwright) test
This branch is reachable only when the **auth backend actively rejects a
Google-Workspace-SSO sign-up** (a server-side blocking decision) and the
Firebase SDK returns an error whose message carries the `signup_blocked`
token. Reproducing that in the browser E2E harness would require driving
a real federated-SSO sign-up against a backend configured to reject the
specific account — state the FE test environment cannot set up
deterministically. The logic under test is pure error-mapping with no
UI/navigation surface beyond the existing toast, so it is fully covered
by the unit tests above. The end-to-end path will instead be confirmed
via a one-time staging repro (see below).
## Draft — pending confirmation
Kept as **draft** until we confirm, via a staging repro, the exact error
shape the client receives for this rejection (the message-token match is
the robust hedge regardless of `error.code`, but a live repro makes it
deterministic). The backend that emits the `signup_blocked` token is a
separate change.
## ELI-5
Custom nodes can hook into app events (like "this node finished
running"). If
one of those hooks crashes, the browser shouts the crash into our global
error
channel — and our monitoring logs it as an app error, on every single
execution.
One sloppy third-party node can bury the real errors in thousands of
copies of
its own bug. This wraps each hook so if it crashes, we log a quiet
warning for
the node author instead of screaming it into the error dashboard.
## What
`ComfyApi` extends `EventTarget`. When a listener throws, native
`dispatchEvent`
reports the exception to the **global** error handler (`window.onerror`
/
`reportError`) and continues. Our error monitoring (Datadog RUM)
collects those
as unhandled errors — so a single misbehaving custom-node listener (e.g.
`comfyui-enricos-nodes` reading `.type` on an undefined message inside
an
`executed` handler) produces thousands of non-actionable error events.
This wraps listeners registered via `addEventListener` /
`addCustomEventListener` in a try/catch:
- The wrapper is stored in a `WeakMap<original, wrapped>`, so
`removeEventListener`
/ `removeCustomEventListener` still match the installed wrapper. (GC'd
weakly.)
- Caught errors are logged at **`console.warn`**, not `console.error` —
RUM
collects `console.error` by default, which would just relabel the same
noise.
Native `EventTarget` already isolates listeners from one another, so
this does
**not** change dispatch/continuation behaviour — it only changes where a
thrown
error goes (a warn in the console, visible to node authors, instead of
an
unhandled error in global telemetry).
## Test plan
- [x] New `api.eventListeners.test.ts`: a throwing listener doesn't
abort
dispatch to other listeners; errors log at `warn` not `error`;
`removeEventListener` still removes a guarded listener. (3 tests)
- [x] Existing `api.featureFlags.test.ts` (15) and
`changeTracker.test.ts` (16)
still pass — 34 total green.
- [x] oxfmt + oxlint clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Collapse the OSS vs Cloud branching in the mask editor and painter
uploads so both runtimes use the same contract — POST to `/upload/image`
with `type: input` and no `subfolder`, then reference the result by
filename only.
## Related
- Companion spec change (Comfy-Org/ComfyUI):
https://github.com/Comfy-Org/ComfyUI/pull/13968 deprecates
`/api/upload/mask` and documents `/api/upload/image` as the unified
upload contract. No runtime behavior changes on the server.
## Changes
- **What**:
- `useMaskEditorSaver.ts`: replace the separate `uploadMask` +
`uploadImage` helpers with a single `uploadLayer`. All four layers
(masked, paint, painted, paintedMasked) now go through `/upload/image`.
Dropped the `original_ref` form field and the `clipspace` subfolder.
`uploadLayer` throws on a non-ok status, on a non-JSON response body,
and on a 200 response missing `data.name` (no more silent fallback to
the pre-upload ref).
- `usePainter.ts`: removed the runtime branch on upload type/subfolder
and on the returned widget value. Always uploads as `type: input` with
no subfolder; widget value is `${filename} [input]`. Added a `data.name`
guard alongside the existing non-ok and JSON-parse guards.
- `usePainter.test.ts`: assert the upload FormData carries `type=input`
with no `subfolder`, plus new coverage for the missing-name and
JSON-parse-failure error branches.
- `browser_tests/tests/maskEditor.spec.ts`: drop the now-dead
`**/upload/mask` Playwright route interceptors and tighten the
save-success assertion to confirm exactly four `/upload/image` calls
(one per layer).
- **Breaking**: yes for downstream consumers — the mask editor no longer
calls `/upload/mask`, and saved widget values for both editors no longer
contain a subfolder prefix. Existing nodes will continue to load their
referenced inputs because the filename is preserved; new saves emit the
unified shape.
## Review Focus
- Confirm the mask editor's four-layer upload (masked, paint, painted,
paintedMasked) is still correct without `original_ref` — the layers are
now independent uploads rather than alpha-composited server-side. The
four blobs are composited client-side in `prepareOutputData` before
upload, so the previous `original_ref` chain was vestigial — but worth a
second look.
- OSS-side painter widget back-compat: previously stored as
`painter/foo.png [temp]`, new saves emit `foo.png [input]`. The old
format remains valid in saved workflows; only the *new save* shape
changes. Loading a pre-existing OSS workflow with a `painter/foo.png
[temp]` widget value still resolves via the existing parser path.
## Test plan
Unit:
- [x] `usePainter.test.ts` — 30 tests pass, including the three new ones
covering FormData payload shape, missing `data.name`, and bad JSON.
E2E:
- [x] `browser_tests/tests/maskEditor.spec.ts` — `save uploads all
layers and closes dialog` and `save failure keeps dialog open` updated
to the unified contract.
Smoke-tested manually against a Cloud staging instance:
- [x] **Painter node**: paint strokes → save → reload workflow → widget
value resolves and the canvas re-renders cleanly. Upload responds with
`subfolder: ""`, `type: "input"`.
- [x] **Mask editor**: open editor with a base image, paint + mask,
save. All four layers (`clipspace-mask-*`, `clipspace-paint-*`,
`clipspace-painted-*`, `clipspace-painted-masked-*`) POST successfully
to `/upload/image` and return 200 with the asset-aware response shape
(`{ name, subfolder: "", type: "input", asset: { id, hash, tags } }`).
The resulting `LoadImage` node references the painted-masked filename by
basename only and re-renders.
Not validated here: OSS ComfyUI core-side smoke. The contract is
symmetric (the OSS `/upload/image` endpoint accepts the same fields the
FE now sends), but a smoke against OSS HEAD is recommended before merge.
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Adds consistent ghost node behavior when clicking "use" on models from
either the model dialog or treeview - matching the Node Library & Node
Search.
## Changes
- **What**:
- Split `createModelNodeFromAsset` into `resolveModelNodeFromAsset` and
`startModelNodeDragFromAsset` to allow sharing logic between the two
drag sources
- Move NodeDragPreview file & use from being node library tab specific
to app level in GraphCanvas so all drag sources share it
- drag listeners now attach on `startDrag` and detach on cancel instead
of living for the tab's lifetime
- Updated `LGraphNodePreview` to accept widgetValues and prepend combo
options with passed value so it shows in the preview as the default
## Review Focus
- refactored positioning to use RAF with useMouse and transform to fix
laggy-follow behavior present in Firefox
## Screenshots (if applicable)
Model dialog + Node library
https://github.com/user-attachments/assets/b227ac43-c6ea-4cf6-86ed-6cfb196fd80e
Model library sidebar
https://github.com/user-attachments/assets/bb546aee-5099-4df9-abe5-68bccd8fa2eb
## What
**B4 (FE-934): `UnifiedPricingTable`** — one pricing table for the
**Jun-5 model** (typeless workspace; personal/team is a **plan**, shown
as a Gamma-style **plan toggle** on one workspace), per **DES-197**. A
new, flag-gated component that will replace the two legacy tables at
cutover (strangler).
### video
https://github.com/user-attachments/assets/82b704a4-101e-4609-8ff5-06b7cf7f9cd7
> **Stacked on #12644 (FE-935 `CreditSlider`).** Base is the slider
branch — retarget to `main` once #12644 merges. The slider commits show
in the diff until then.
## Changes
- **`UnifiedPricingTable.vue`** — plan toggle (personal/team, flag-gated
on `teamWorkspacesEnabled`); personal tier cards (facade `plans` +
`TIER_PRICING` fallback, billing-cycle toggle); team column hosting
`<CreditSlider>`; Enterprise card. Reuses `useBillingContext`; emits
`subscribe`/`resubscribe` (personal) + `subscribeTeam` (team).
- **`SubscriptionRequiredDialogContentUnified.vue`** — host; wires
personal checkout through `useSubscriptionCheckout` (full flow incl.
preview/transition + **new `'success'` step**); team checkout stubbed.
- **Confirm/success screens (DES 3084-15873)** — aligned the reused
`SubscriptionAddPaymentPreviewWorkspace` /
`SubscriptionTransitionPreviewWorkspace` to DES-197 (drop `/member`,
`comfy--credits` icon, plan-specific CTAs **"Subscribe to {plan}" /
"Switch to {plan}"**); added **`SubscriptionSuccessWorkspace.vue`**
("You're all set") as the `'success'` checkout step.
- **`showPricingTable`** — renders the unified host when
`teamWorkspacesEnabled`, legacy `PricingTable.vue` for flag-off. (The
old workspace-variant fork is removed; `PricingTableWorkspace.vue`
becomes unused on flag-on → deleted at cutover.)
- **i18n** — `subscription.planScope` / `teamPlan` / `enterprise` +
`subscription.preview.{subscribeToPlan,switchToPlan}` +
`subscription.success.*` keys.
## Strategy (new component + cutover — per FE-934)
Build new, retire old at cutover (a later single PR deletes
`PricingTable.vue` + `PricingTableWorkspace.vue` + the legacy dialog
host + the dispatch fork; `TIER_PRICING` kept only as the flag-off/OSS
fallback). Avoids half-migrated conditional cruft in the live tables.
## Screenshots
Pricing captured live on the authenticated cloud-prod session
(`local.comfy.org`, flag on). Confirm/success captured in Storybook
(prop-driven) — a **personal** workspace can't reach these live until
**B1/FE-966** flips its billing path off the legacy `/customers/*`
adapter (whose `plans` is always empty); the screens themselves are
unchanged regardless of source.
**Pricing table**
| Personal | Team |
|---|---|
| <img width="420" alt="pricing-personal"
src="https://github.com/user-attachments/assets/2be3b8bc-ac54-41db-8c21-5c950d3e7338"
/> | <img width="420" alt="pricing-team"
src="https://github.com/user-attachments/assets/c4078eb4-ee7d-42f6-bcc3-375686ab7f1e"
/> |
**Confirm your payment / plan change**
| New subscription | Plan change (Pro → Creator) |
|---|---|
| <img width="380" alt="confirm-new-subscription"
src="https://github.com/user-attachments/assets/e371c744-dc64-43e8-b977-73f9f99f85bc"
/> | <img width="380" alt="confirm-plan-change"
src="https://github.com/user-attachments/assets/b1ee5ab3-c572-4c40-9b70-f078d66b78f4"
/> |
**You're all set (success)**
<img width="380" alt="success-all-set"
src="https://github.com/user-attachments/assets/8ec9e90f-8ba4-4cb0-81a7-6b4316e0c19e"
/>
## ⚠️ BE-blocked (deferred)
- **Team checkout**: the slider stop → plan-slug / subscribe-request
shape is undefined (doc **Open Q#2** / "Team-slider contract",
**BE-1254**). `subscribeTeam` is stubbed (toast "coming soon") until BE
provides it.
- **Live discount data**: stops come from the hardcoded **DES-197
fallback** (`teamPlanCreditStops.ts`) until `GET /api/billing/plans`
carries them.
- **Confirm "credits you'll get right away" line** + **async-success
routing** (Stripe-tab/`pending_payment` → in-dialog success) deferred —
see ticket notes; credits-cents unit is the open BE-1254 question.
## Verification
- `vue-tsc --noEmit`: clean (pre-commit).
- `oxlint`/`eslint`/`stylelint`/`oxfmt`: pass (pre-commit).
- `vitest` `useSubscriptionDialog.test.ts` (11) +
`useSubscriptionCheckout.test.ts` (17) + new
`SubscriptionSuccessWorkspace.test.ts` (2): pass.
- Personal checkout reuses the existing `useSubscriptionCheckout` flow;
subscribed → new `'success'` step.
## Not in scope
- Pixel-finalizing vs DES-197 (visual reference = Alex's
**reference-only** #12042); personal-card detail refinement.
- Settings / Misc-UX (FE-768/770); team-checkout wiring (BE contract).
Design: **DES-197** / **3084-15873**. Survey: *FE Billing API
Divergence* (B4 / P1 / P2 / P4).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
This PR intentionally narrows workflow-embedded model metadata handling
so root-level `models[]` and node-level embedded model metadata can
enrich existing missing-model candidates, but can no longer create new
candidates by themselves.
## Why this PR exists
ADR 0009, **Subgraph promoted widgets use linked inputs**, changes
promoted value ownership for subgraphs. That design was implemented by
[#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197),
**Subgraph Link Only Promotion (ADR 0009)**.
Under ADR 0009, a promoted widget is represented as a standard linked
`SubgraphInput` on the host `SubgraphNode`. The host boundary owns the
promoted value identity through the host node locator plus
`SubgraphInput.name`. The interior source widget remains the provider of
schema, type, options, tooltip, defaults, diagnostics, and migration
metadata, but it is not the persistence owner of the promoted value.
This PR is a preparatory cleanup discovered while working on the missing
model detection follow-up required by that ADR 0009 /
[#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197)
behavior. The follow-up needs missing model detection to respect the new
subgraph promoted-widget ownership model. While reviewing that path, we
found that the existing embedded model metadata fallback in
`enrichWithEmbeddedMetadata` was doing more than metadata enrichment.
The important finding was that this fallback was not just attaching
metadata to candidates that had already been detected from live node
widgets. It could also synthesize brand-new `MissingModelCandidate`
entries from workflow JSON metadata, including root-level `models[]`
entries, when no live candidate existed.
That behavior is inaccurate for the missing model system for two
reasons.
First, the normal missing model lifecycle is anchored to a real
node/widget binding. A candidate found from a COMBO or asset widget has
a concrete `nodeId + widgetName` reference. That reference is what lets
the UI surface the error, cache it as a pending warning, and later clear
or resolve it when the underlying node/widget value is fixed. A
root-level `models[]` entry does not reliably provide that anchor. If
metadata-only fallback creates a candidate without a real live widget
reference, the resulting error can be detected but cannot reliably
travel through the existing clearing path. In practice, that can become
an effectively unremovable missing model warning unless the user
downloads exactly the same model referenced by the stale metadata.
Second, a missing model error is meant to mean that a model-selecting
widget on an active node references a value that is not available.
Workflow JSON metadata by itself is not the same source of truth. If a
model only appears in root workflow metadata, or appears in node
metadata that is not represented by an active COMBO or asset widget
candidate, that is a different kind of state from the existing missing
model error model. Treating that metadata as a candidate creates a
second, less reliable detector that is not aligned with the scan/clear
lifecycle.
This is especially important before the ADR 0009 missing-model
follow-up. With linked-input promoted widgets, the host promoted value
is the value that matters. The interior source widget may still carry
stale or default metadata, and it must not become a second source of
truth for missing model errors. A detection path that can create
candidates directly from workflow metadata would make it harder to
reason about which value actually produced the warning.
For those reasons, this PR removes metadata-only candidate synthesis and
keeps embedded metadata in the role it can perform safely: metadata
enrichment. If the live widget/asset scan produces a candidate, embedded
metadata may fill in `directory`, `url`, `hash`, and `hashType`. If no
live candidate exists, the metadata is not enough to create a missing
model warning.
This PR is intended to land before the child PR that updates runtime
missing model detection for ADR 0009 linked-input promoted widgets.
## Changes
- **What**: Restrict `enrichWithEmbeddedMetadata` to enriching existing
candidates instead of creating fallback candidates from unmatched root
`models[]` or embedded model metadata.
- **What**: Remove the now-unused installed-model check callback and
asset-support callback from `enrichWithEmbeddedMetadata`.
- **What**: Remove the now-unnecessary `modelStore.loadModelFolders()`
path from the missing model pipeline, since embedded metadata no longer
performs installed-model fallback detection.
- **What**: Remove dead source-tracking metadata
(`EmbeddedModelWithSource`, source node/widget fields, and widget-name
lookup) that only existed to support metadata-only synthesis.
- **What**: Update missing model tests so they assert the new contract:
metadata enriches live candidates, but does not create candidates
without a live scan result.
- **What**: Delete obsolete fixtures that only covered the removed
metadata-only synthesis path.
- **Breaking**: None expected. This is an intentional narrowing of an
inaccurate fallback detector, not a public API change.
- **Dependencies**: None.
## Review Focus
Please focus on whether the candidate lifecycle now has a single source
of truth: live COMBO/asset widget scanning creates candidates, while
workflow metadata only enriches those candidates.
The intended behavioral change is that a model present only in
workflow-level metadata, with no active node widget candidate
referencing it, no longer appears as a missing model. This avoids
surfacing warnings that cannot be cleared through the normal `nodeId +
widgetName` path.
The expected retained behavior is that active widget-referenced missing
models are still detected by `scanAllModelCandidates`, and metadata from
root `models[]` or node `properties.models` still supplies
download-related fields for those live candidates.
## Screenshots (if applicable)
Not applicable. This is a detection/pipeline behavior change covered by
unit tests.
## Validation
- `pnpm test:unit src/platform/missingModel/missingModelScan.test.ts
src/platform/missingModel/missingModelPipeline.test.ts`
- `pnpm exec eslint src/platform/missingModel/missingModelScan.ts
src/platform/missingModel/missingModelScan.test.ts
src/platform/missingModel/missingModelPipeline.ts
src/platform/missingModel/missingModelPipeline.test.ts
src/platform/missingModel/types.ts`
- `pnpm exec oxfmt --check src/platform/missingModel/missingModelScan.ts
src/platform/missingModel/missingModelScan.test.ts
src/platform/missingModel/missingModelPipeline.ts
src/platform/missingModel/missingModelPipeline.test.ts
src/platform/missingModel/types.ts`
- `pnpm typecheck`
- pre-push hook: `knip --cache`
## Summary
Owner-initiated team→personal downgrade flow: a confirm dialog ("Change
to {plan} plan?", "All other members of this workspace will be
immediately removed", type-"I understand" gate) that removes non-owner
members and then initiates the tier change. No dialog when there are no
other members.
## Changes
- **What**: `DowngradeRemoveMembersDialogContent.vue` (confirm + type-"I
understand" gate, destructive Change plan); `useDowngradeToPersonal.ts`
(validates via `previewSubscribe`, removes every non-creator member via
`workspaceApi.removeMember`, then `useBillingContext().subscribe` with
`needs_payment_method`/`pending_payment` handling à la
`useSubscriptionCheckout`);
`dialogService.showDowngradeToPersonalDialog` (refreshes members,
skip-dialog when no other members; non-dismissable while open).
`subscription.downgrade.*` i18n.
- **Breaking**: none.
## Review Focus
- **Creator protection**: the cloud model is single-owner with no
distinct creator field — a member is protected if `role === 'owner'` OR
they are the current user. To be reconciled with FE-770's
earliest-`joinDate` creator inference when both land (single source: the
`isCreator` predicate). BE-1337 will expose an explicit original-owner
determination.
- **Integration seam**: there is no on-main team→personal trigger
(FE-934 pricing-table plan change is unmerged); the flow is exposed via
`dialogService.showDowngradeToPersonalDialog` for FE-934 to call on
`transition_type: 'downgrade'`.
- **Failure-path hardening** (follow-up commits): `previewSubscribe`
gate runs before any member is removed (a BE-disallowed transition
removes nobody and surfaces the BE reason); `subscribe` outcomes
`needs_payment_method` (payment tab + billing-op polling; a
popup-blocked tab throws so the dialog stays open and a retry
re-subscribes) and `pending_payment` (polling) are handled instead of
discarded; the member list is refetched before the no-members fast path
(a stale empty store can no longer skip the confirm gate);
fast-path/refresh failures toast instead of escaping as unhandled
rejections; the dialog is not dismissable via ESC/overlay-click
(`closable: false` — `dialogStore.updateCloseOnEscapeStates` derives
`closeOnEscape` from `closable`, so `closeOnEscape: false` alone is
overridden).
- **Accepted non-atomicity (until BE-1337)**: removals and the tier
change are separate FE-orchestrated calls — if the user confirms, then
abandons/fails the payment-method step, members are already removed
while the plan stays team (poller surfaces an error toast after 120s).
The dedicated BE downgrade endpoint makes removal+transition atomic.
- Tests: type-gate exact-match; no-dialog when no other members;
preview→remove→subscribe ordering; disallowed preview removes nobody;
payment-method/pending/popup-blocked outcomes; member-refresh gate
(composable + dialogService level); non-dismissable dialog props;
fast-path toast; creator excluded; cancel no-ops; error keeps dialog
open (23 green). typecheck / oxlint / eslint / stylelint / oxfmt / knip
clean.
## Screenshots
Captured live on the PR branch against mocked workspace/billing APIs
(4-member team workspace; `DELETE /api/workspace/members/:id` and `POST
/api/billing/subscribe` intercepted). Flow verified end-to-end: all 3
non-owner members removed, then `subscribe('standard-monthly')` issued;
owner retained.
| Confirm gate (initial) | Phrase typed → enabled | Removing members +
subscribing |
| --- | --- | --- |
| <img width="400" alt="initial"
src="https://github.com/user-attachments/assets/bd5c2d41-edc5-48c2-a44e-b95f9b6bcbd7"
/> | <img width="400" alt="typed enabled"
src="https://github.com/user-attachments/assets/69d5ef0f-56f4-44ce-94a3-f4c05594f403"
/> | <img width="400" alt="loading"
src="https://github.com/user-attachments/assets/073ff7b3-49ca-4fda-9ab8-0fca869f7347"
/> |
In context over the pricing table (the FE-934 trigger seam):
<img width="800" alt="dialog over pricing table"
src="https://github.com/user-attachments/assets/6589f823-f511-42eb-ae2e-5d7d523bb2ee"
/>
Disallowed transition (`previewSubscribe` gate): BE reason surfaced,
dialog stays open, no member removed:
<img width="800" alt="preview disallowed error toast"
src="https://github.com/user-attachments/assets/58759c77-d824-4bfe-80d4-3847e2145456"
/>
Fixes FE-977
## Issue
In the top-right user popover, a **cancelled-but-still-active** personal
subscription renders **both** "Add credits" and "Resubscribe" in the
credits row (the user can still top up *and* re-subscribe during the
grace period). With a wide (7-digit) credit balance, balance + help icon
+ both buttons exceeded the fixed `w-80` (320px) popover and the
trailing "Resubscribe" button spilled past the right edge.
Surfaced during FE-991 (Billing Rework V1) testing. Pre-existing on
`main` — reproducible for any personal owner whose subscription is
cancelled but not yet expired.
## Fix
Make the popover width **fluid** instead of fixed: `w-fit` clamped to
`min-w-80 max-w-96`. It stays **320px** in the common single-action case
(unchanged) and only grows — to **~370px** — when the credits row
actually needs the room for a second button.
## Before / After
**Before (`w-80`)** — "Resubscribe" clipped past the popover edge:
<img width="320" alt="before"
src="https://github.com/user-attachments/assets/439baae8-9e04-4cdf-b43f-098fb5e3853f"
/>
**After — single action (stays 320px):**
<img width="320" alt="after-single"
src="https://github.com/user-attachments/assets/e96f784e-6afd-4286-80c3-1cf0ecec7aa8"
/>
**After — both actions (grows to ~370px, fits):**
<img width="370" alt="after-both"
src="https://github.com/user-attachments/assets/578c1528-24ad-4717-a2b5-33e1af78f048"
/>
## Test
Adds a `@cloud` e2e that opens the popover in the cancelled-but-active
state (mocked `/customers/cloud-subscription-status` with `end_date` + a
7-digit balance) and asserts the "Resubscribe" button's right edge stays
within the popover bounds — same bounding-box pattern as
`workspaceSwitcher.spec.ts`. Validated red→green locally (fails on fixed
`w-80`, passes with the fluid width); single-action width measured at
320px, both-action at ~370px.
---------
Co-authored-by: GitHub Action <action@github.com>
## What
Mirrors the personal/legacy adapter fix (`useLegacyBilling.subscribe` in
#12945, FE-967) on the **team** workspace adapter.
`useWorkspaceBilling.subscribe` performed the write
(`workspaceApi.subscribe`) and then refreshed status + balance with
`Promise.all([fetchStatus(), fetchBalance()])`. A post-write **refresh**
failure rejected the whole call, so the caller saw "subscribe failed"
and could prompt a retry of an **already-active** subscription.
## Fix
The refresh is now non-fatal: `Promise.allSettled` runs the refresh and,
on a rejected refresh, surfaces a soft signal via the existing `error`
ref (`Subscription succeeded, but billing state refresh failed`) and
returns the successful `SubscribeResponse`. Write semantics and the
success / needs-payment / pending branches are unchanged; a failed
**write** still rejects as before.
## Test
Added a regression test: `subscribe` resolves but the post-write refresh
rejects -> still returns the response (no false failure) and records the
soft error.
## Verification
- `pnpm typecheck` clean
- `pnpm test:unit
src/platform/workspace/composables/useWorkspaceBilling.test.ts` -> 41
passed
- eslint / oxlint / oxfmt clean on changed files
## ELI-5
When you cancel a job in the jobs panel, the app used to pick a
different cancel button under the hood depending on which backend it was
talking to and whether the job was already running or just waiting in
line. That meant three code paths for one user action. This swaps all of
that for a single "cancel this job" request that works the same way no
matter the job's state — plus a single "cancel all running jobs" request
for the bulk case.
## What
- Single cancel now calls `POST /api/jobs/{job_id}/cancel`.
- Batch / "cancel all running" now calls `POST /api/jobs/cancel` with
body `{ "job_ids": [...] }`.
- Removed the runtime + job-state branching that previously routed
cancellation through `/api/queue { delete }` or `/api/interrupt`.
- Added two thin client methods (`api.cancelJob`, `api.cancelJobs`) that
target these endpoints and throw on failure so existing error handling
fires.
The "clear queue" (clear-all-pending) action is intentionally
**unchanged** and still uses the existing `/api/queue` path — there is
no jobs-namespace replacement for it, and it is out of scope here.
## Why
The cancel flow had three branches (running vs pending, and one backend
vs another) for a single user intent. The jobs-namespace endpoints are
state-agnostic and idempotent (already-terminal jobs are a successful
no-op), so one call covers every case. Collapsing the branches removes
runtime-specific conditionals from the panel and makes the cancel
behavior identical everywhere.
## ⚠️ Dependency — do not merge before runtime parity
This change relies on the runtime that serves the API exposing **both**
of these endpoints:
- `POST /api/jobs/{job_id}/cancel`
- `POST /api/jobs/cancel`
Exposing these on every runtime this UI runs against is **in flight and
not yet complete**. Until that parity lands, some runtimes will not have
these endpoints, and cancellation would fail there.
**This PR should sit ready and only be merged once that runtime parity
exists.** Do not enable auto-merge. A code comment next to each cancel
site (and on the new client methods) restates this dependency.
## Testing
- `npx vue-tsc --noEmit` — clean (0 errors).
- `npx vitest run src/scripts/api.cancel.test.ts
src/composables/queue/useJobMenu.test.ts
src/components/queue/QueueProgressOverlay.test.ts` — 48 passed.
- `npx eslint` on the touched non-ignored files — clean.
New/updated unit tests cover the single cancel call, the batch cancel
call (including the empty-list no-op), and the error path (request
failure propagates and skips the queue refresh).
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Wires the previously-dormant `loadMore` path of the flat-output assets
provider into the widget select dropdown, so cloud users with more than
one page of outputs can actually reach them. Stacked on #12774 (FE-985);
retargets to `main` when that merges.
## Changes
- **What**: `VirtualGrid`'s existing `approach-end` event now forwards
through `FormDropdownMenu` → `FormDropdown` → `WidgetSelectDropdown`,
which calls `outputMediaAssets.loadMore()` guarded by
`hasMore`/`isLoadingMore` and debounced 300ms — the same idiom as
`AssetsSidebarTab`. A spinner row (`loadingMore` prop) renders below the
grid while a page is in flight. On cloud this drives the FE-985 cursor
walk; on OSS it drives the FE-962 jobs-history cursor walk via
`useAssetsApi`.
## Review Focus
- Verified end-to-end against the dev server with a mocked 100-asset
backend: scroll → `after=cur-40` → `after=cur-80` → stops at
`has_more:false` (100/100, no duplicate fetches).
- **Known platform limitation, not introduced here**: VueUse ≥14's
`throttleFilter` with `leading=false` drops events spaced wider than the
throttle window, so `useScroll`'s `throttle: 64` in `VirtualGrid` never
reports discrete mouse-wheel scrolls — only high-frequency
(trackpad-style) scrolling triggers `approach-end`. This equally affects
the assets sidebar and manager dialog today; bug filed separately with
root cause. Fixing it makes this wiring work for wheel users with no
further changes.
- Underfill edge: `approach-end` cannot fire when loaded items don't
overflow the viewport (shared VirtualGrid trait with the sidebar); with
the 200-item page size this only matters for heavily-filtered media
types.
- Fixes FE-988
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Shrink-wraps the asset browser dialog chrome around its self-sized
BaseModalLayout content.
- Applies the same Reka dialog props to model-widget selection and
direct asset browsing entry points.
## Cause
The Reka dialog cutover moved showLayoutDialog callers onto the shared
Reka DialogContent wrapper. AssetBrowserModal already renders a large
BaseModalLayout with its own modal sizing, but the outer wrapper still
used the default md dialog width. That left the large modal content
anchored from a narrow centered wrapper, pushing the right edge beyond
the viewport.
This change keeps BaseModalLayout as the owner of the asset browser
dimensions. The Reka wrapper now only shrink-wraps the content and
removes its own border, background, and shadow.
## Validation
- pnpm exec oxfmt --write
src/platform/assets/composables/useAssetBrowserDialog.ts
- pnpm exec eslint
src/platform/assets/composables/useAssetBrowserDialog.ts
- pnpm typecheck
- pre-commit hook completed oxfmt, oxlint, eslint, and typecheck
## Screenshot
Before
<img width="1920" height="1028" alt="스크린샷 2026-06-22 오후 9 30 15"
src="https://github.com/user-attachments/assets/1534a0a8-a239-419e-b05f-9c5e43cedeb1"
/>
After
<img width="1918" height="1024" alt="스크린샷 2026-06-22 오후 9 29 39"
src="https://github.com/user-attachments/assets/14ad751e-54c9-4f9e-87f5-805f6ca456d1"
/>
## Observed flake
The e2e test `Canvas Interaction > Can zoom in/out with
ctrl+shift+vertical-drag` (`browser_tests/tests/interaction.spec.ts`)
intermittently failed in CI with:
```
Error: mouse.move: Test timeout of 15000ms exceeded
```
It failed all 3 retries on a single shard while 209 other tests on that
shard passed, and passed on a later re-run — i.e. genuinely flaky, not a
hard break.
## Root cause
The test pressed `Control` and `Shift` *down* once, then ran three
`canvasOps.dragAndDrop` gestures (each performs a `mouse.move(target, {
steps: 100 })`) with three `toHaveScreenshot` assertions interleaved,
and only released the modifiers at the very end — with no `try/finally`.
Two problems:
1. Holding the modifiers across all three drags means every one of the
~300 step-wise `mousemove` events drives litegraph's ctrl+shift zoom
handler (scale recompute + canvas redraw). Combined with the screenshot
captures in between, the main thread can saturate, and a single
`mouse.move` step can stall past the 15s test timeout. That is exactly
the failing call in the signature.
2. Without `try/finally`, a mid-test failure leaves `Control`/`Shift`
stuck down.
## Fix
Switch to the existing `canvasOps.ctrlShiftDrag(from, to)` helper, which
presses and releases `Control`+`Shift` around each individual gesture.
This is the robust pattern already used in `canvasSettings.spec.ts`. The
modifiers are never held across the heavy multi-drag + screenshot
sequence, and are always released.
Test intent, drag coordinates, and all three screenshot assertions are
unchanged.
## Validation
- Verified by reasoning: `ctrlShiftDrag` wraps the same `dragAndDrop`
with `keyboard.down/up` of the same modifiers, called with identical
`Position` args, so behavior and types are preserved.
- Could not run the browser e2e locally (requires a running ComfyUI
backend + Playwright browsers; `node_modules` not installed in this
environment). Relying on CI for the full e2e run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
*PR Created by the Glary-Bot Agent*
---
Update the English copy on the share dialog acknowledgement checkbox to
better reflect the link-based visibility model.
**Before:** "I understand these media items will be published and made
public"
**After:** "I understand anyone with the link can view these files"
Only the `en` locale string was changed
(`shareWorkflow.acknowledgeCheckbox` in `src/locales/en/main.json`), per
the request. Other locale files are intentionally left to the standard
translation sync process.
The component (`ShareAssetWarningBox.vue`) reads this string via
`$t('shareWorkflow.acknowledgeCheckbox')`, so no component change is
needed.
## Verification
- `ShareAssetWarningBox.test.ts` — 12 passed (tests use their own mock
string and are unaffected).
- `ShareWorkflowDialogContent.test.ts` — 19 passed.
- Lint-staged hooks (oxfmt, oxlint, eslint, typecheck) ran on commit and
passed.
- Visual verification: rendered the new copy in a representative
checkbox layout via Playwright and confirmed the user-facing text
matches (screenshot attached).
## Screenshots

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
The **renderer cutover** for Phase 6: every remaining dialog caller is
flipped to Reka, and `createDialog` now defaults `renderer: 'reka'` so
the PrimeVue `Dialog` branch is no longer reached by default (it
survives only as an explicit `renderer: 'primevue'` escape hatch,
deleted in Phase 6b).
> **Stacked on #12848** (mask editor + 3D viewer dialogs + dialog
infra). Per @jtydhr88's review, the heavy, screenshot-bearing surface
(3D + mask editor) was split into #12848 so it reviews and tests on its
own. **Merge #12848 first**, then this PR's base auto-retargets to
`main`.
Parent:
[FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent)
This phase:
[FE-578](https://linear.app/comfyorg/issue/FE-578/phase-6-remove-primevue-dialogconfirmdialog-imports-clean-up-css)
## Changes
- **drop dead `ConfirmationService` registration** — zero
`useConfirm`/`<ConfirmDialog>` consumers remain in `src/`; desktop-ui
keeps its own.
- **flip `showConfirmDialog`** — all six confirm callers render Reka
chrome; width goes from PrimeVue auto-hug to fixed `size:'md'`, matching
`dialogService.confirm()`.
- **flip remaining `dialogService` + composable callers** — signin,
update-password, top-up, workspace family, cancel-subscription, publish,
cloud-notification, edit-keybinding / node-conflict / import-failed,
upload-model, queue-clear-history, delete-assets, share /
open-shared-workflow, subscription pricing. Self-styled panels get a
shared transparent `w-fit` chrome replicating PrimeVue's auto-sized
root.
- **default `createDialog` to `renderer:'reka'`** — cuts over
`showExtensionDialog` (third-party dialogs) and anything unflagged. The
single-commit revert point.
- **retarget class-based e2e selectors** — `BaseDialog` `.p-dialog` →
`getByRole('dialog')`, `BuilderSaveAsHelper` close-X → `getByLabel`,
`shareWorkflowDialog` role-based, dead `confirm-dialog` testid removed.
- honor `[autofocus]` inside Reka dialogs; size the template browser
dialog so the filter bar fits; drop redundant Tailwind width constraints
on the remaining callers.
## Review focus
1. **`modal:false` on the pricing dialogs** — same trade-off as
Settings/Manager (visual overlay without focus trap) because
`PricingTable(.Workspace)` hosts a body-teleported PrimeVue `Popover`.
2. **`w-fit` shrink-wrapped chrome** for self-styled panels — replicates
PrimeVue's shrink-to-fit root.
3. **Confirm width change** (auto-hug → fixed 576px `md`) — intentional
consistency with `dialogService.confirm()`.
## Public API impact
`createDialog` now defaults to Reka. Third-party extension dialogs
render through Reka by default — a fixed `size:'md'` frame with a modal
focus trap instead of PrimeVue auto-width; `renderer:'primevue'` remains
an explicit escape hatch until Phase 6b. Worth a release note for
extension authors.
## Out of scope (Phase 6b)
PrimeVue branch deletion (`GlobalDialog.vue` legacy branch,
`PrimeDialog` import, `.p-dialog` CSS/bridge tokens, `dialogStore`
`pt`/`position`/`unstyled` typing) — lands after this soaks one cloud
deploy cycle.
## 📸 Screenshots — manual verification
Captured via Chrome DevTools (CDP) from this branch running locally in
**cloud mode** (proxied to the `cloud.comfy.org` backend, free Personal
Workspace). Every dialog below now renders through the **Reka** path —
the PrimeVue `Dialog` branch is no longer reached. (Mask editor + 3D
viewers live in the stacked base #12848.)
**Confirm dialog** (`showConfirmDialog`) — Reka chrome at a fixed
`size:'md'`, replacing PrimeVue's auto-hug width — *review focus #3*
<img width="880" alt="confirm-dialog"
src="https://github.com/user-attachments/assets/5d9953c1-4d0c-4ff9-adc7-88dd370c6a24"
/>
**Settings** — renders through Reka
<img width="880" alt="settings"
src="https://github.com/user-attachments/assets/44e3fd3f-8d9b-4322-8fbe-8ce8d94ed15d"
/>
**Edit Keybinding**, stacked on Settings — small-layout `w-fit` chrome;
closing it leaves Settings open (stacked-dismiss holds)
<img width="880" alt="edit-keybinding-nested"
src="https://github.com/user-attachments/assets/d0875c00-7b9c-439d-b24d-ba6770009d08"
/>
**Subscription pricing** (`PricingTable`) — opened with `modal:false`
because it hosts a body-teleported PrimeVue `Popover` — *review focus
#1*
<img width="880" alt="subscription-pricing"
src="https://github.com/user-attachments/assets/3be20397-8a69-4b00-b803-73eff4e0e313"
/>
**Share** and **Publish** (open-shared-workflow + publish) — shared
transparent shrink-wrapped (`w-fit`) chrome — *review focus #2*
<img width="880" alt="share-dialog"
src="https://github.com/user-attachments/assets/16f1c1b5-e35e-4664-a957-2f7f61ad96bd"
/>
<img width="880" alt="publish-dialog"
src="https://github.com/user-attachments/assets/935ff453-5247-430f-9c21-2f500d4bc6e2"
/>
**Workspace** (workspace-family callers)
<img width="880" alt="workspace-settings"
src="https://github.com/user-attachments/assets/8031a352-f6fc-41e4-9567-e26e0c35ecd9"
/>
**Template selector** (`showExtensionDialog` /
`useWorkflowTemplateSelectorDialog`)
<img width="880" alt="templates-dialog"
src="https://github.com/user-attachments/assets/9975ebbe-75ae-4ad9-a90a-248db4850e1a"
/>
**Account / workspace menu** (cloud)
<img width="880" alt="account-menu"
src="https://github.com/user-attachments/assets/5bc0cade-9bd9-49de-8bb4-779d65e211b0"
/>
## What
Adds a creator-only `canManageSubscriptionLifecycle` permission (cancel
/ reactivate / downgrade) to `useWorkspaceUI`:
- Any workspace **owner** keeps `canManageSubscription` (manage payment
/ top-up / change-commit).
- Only the **original owner (creator)** also gets
`canManageSubscriptionLifecycle`.
- **Personal** workspaces (single-member) always get it; **members**
never do.
Driven by an `is_creator` flag on the workspace from `/api/workspaces`,
plumbed through the shared type and the auth/session Zod schema.
## Safe to merge ahead of BE / no regression
- **Pure infra — nothing reads the permission yet.** Consumers land
separately: #12786 (cancel/reactivate), #12789 (downgrade). No code on
`main` references `canManageSubscriptionLifecycle` or `is_creator`.
- **Additive & fails closed.** `is_creator` is optional; when absent the
permission is `false`. Existing permission values are unchanged, so
existing consumers (`SubscribeToRun`,
`SubscriptionPanelContentWorkspace`, `CurrentUserPopoverWorkspace`, …)
behave identically.
## BE contract (confirmed with Hunter, 2026-06-17)
- `is_creator` = current-user-relative boolean on `/api/workspaces`; the
creator is tracked explicitly (not by creation date).
- Intentionally **temporary** — removed once member-removal
auto-provisions a personal workspace.
- The FE-770 members-panel creator-**row** lock additionally needs the
creator **id** (`created_by_user_id`) — separate, still-open ask.
## Follow-up (separate PR, once BE ships)
`is_creator` + `WorkspaceWithRoleSchema` are hand-rolled only because
the cloud ingest OpenAPI doesn't expose the field yet.
`@comfyorg/ingest-types` already generates `WorkspaceWithRole` +
`zWorkspaceWithRole`. Once BE adds `is_creator` to
`services/ingest/openapi.yaml`, regenerate and swap the hand-rolled
interface + schema for the generated ones (TODOs marked in
`workspaceApi.ts` / `workspaceAuthStore.ts`).
## ELI-5
The asset library shows each image's resolution (e.g. "1920×1080") on
its card. It used to get that by measuring the picture the browser
actually drew — fine locally, but on cloud the card draws a small
**downscaled thumbnail**, so it measured the thumbnail and showed the
wrong size (like "512×288") instead of the real one.
Now the card prefers the **true dimensions the server records** for each
image. When the server hasn't recorded them yet (an old image not
backfilled, or something unmeasurable like an SVG), it only falls back
to measuring the drawn image when that image is the original — never a
downscaled thumbnail. If all it can measure is a thumbnail, it shows
**no size rather than a wrong one**: a blank beats a confidently wrong
number.
## Summary
Asset cards render image dimensions from the asset response's typed
`metadata` field (`asset.metadata.width` / `asset.metadata.height`) when
available, falling back to the locally-measured `naturalWidth` /
`naturalHeight` of the rendered `<img>` — but only when the rendered
image was the original, not a downscaled thumbnail.
## Why
`MediaAssetCard.vue` previously read dimensions from the rendered
`<img>`'s `naturalWidth` / `naturalHeight` — correct only when the
runtime serves the original file. Runtimes that serve a downscaled
preview return preview-sized dimensions, so the label could surface the
preview's size rather than the source asset's. Reading the typed
`metadata` field when present fixes that. And when metadata is absent,
the natural-size fallback is now **suppressed if a downscaled thumbnail
was rendered** (`asset.thumbnail_url` present), so the card shows no
dimensions rather than a misleading thumbnail size.
## What changes
- `assetMetadataUtils.ts`: new pure
`resolveDisplayImageDimensions(asset, renderedNaturalSize)` — prefers
server `metadata.{width,height}`, falls back to the rendered natural
size only when no thumbnail was rendered, otherwise returns `undefined`
(no label). Adds an exported `ImageDimensions` type.
- `MediaAssetCard.vue`: `displayImageDimensions` delegates to the
helper; `metaInfo` reads from it and does not branch on runtime
identity.
- `assetMetadataUtils.test.ts`: unit tests for
`resolveDisplayImageDimensions` across every branch (metadata present,
metadata-wins-over-thumbnail, no-thumbnail fallback, thumbnail-guard
suppression, invalid shape, both-absent, undefined asset).
## Compatibility
- **metadata has numeric `width`/`height`** → card displays the typed
(true) values.
- **metadata absent, no thumbnail rendered (original served)** →
unchanged: displays the locally-measured dimensions.
- **metadata absent, downscaled thumbnail rendered** → displays **no**
dimensions. This is the one intentional behavior change: previously the
card showed the thumbnail's size (a wrong value); now it shows a blank
until the real dimensions are available (e.g. after the cloud dimension
backfill), and stays blank for permanently-unmeasurable assets
(SVG/corrupt). Blank instead of wrong.
## Test plan
- [x] `pnpm test:unit` — `resolveDisplayImageDimensions` covered across
all branches (70 tests pass)
- [x] `oxfmt --check` + `oxlint --type-aware` clean on touched files
- [ ] `pnpm typecheck` (vue-tsc) — via CI
- [ ] Manual: asset library — image cards show real resolution on cloud;
un-backfilled / SVG cards show no size label (not a thumbnail size);
non-image cards (video / audio / 3D) still render their size label
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12328-refactor-asset-card-read-image-dimensions-from-typed-metadata-field-3656d73d3650811bb0cff00b6bc2d2e8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Matt Miller <mattmillerai@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
The first thing a user sees when they log in is the paywall prompt which
is a poor UX. This removes the dialog that pops up as the run buttons
are already gated to prompt to subscribe.
## Changes
- **What**:
- Remove extension & invocation
## Summary
- Fix release pipeline where core/* releases hang indefinitely waiting
for tags
## Problem
Draft releases don't create git tags, but `publish-pypi` workflow waits
for the tag to exist. For `core/*` branches, releases were always
created as drafts, causing the pipeline to wait forever.
## Solution
Only use draft releases for prereleases (alpha/beta/rc). Publish all
stable releases (main and core/*) immediately so tags are created.
## Test plan
- [x] Verify workflow change logic is correct
- [ ] Test on next core/1.45 release
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
Re-comment the `ultralytics/bbox` and `ultralytics/segm` entries in
`MODEL_NODE_MAPPINGS` so `UltralyticsDetectorProvider` falls back to the
static combo from `/api/object_info`, restoring the working post-#12075
behavior. Companion to
[Comfy-Org/cloud#4395](https://github.com/Comfy-Org/cloud/pull/4395)
(ONNX dummy-placeholder fix).
## Why
PR #12151 (2026-05-12) re-added these entries before BE-689 (the
cloud-side asset-ingestion fix) landed, reintroducing both halves of the
regression that #12075 originally cited:
1. **Tag lookup mismatch.** Cloud stores model tags as combined values
like `ultralytics/bbox`, but the asset query splits them into `(models,
ultralytics)` with exact-match filtering, so the picker returns no
results.
2. **Submitted value mismatch.** Picker returns basenames like
`face_yolov8m.pt`, but the ingest service validates against the
subdirectory-prefixed form (`bbox/face_yolov8m.pt`) that the static
combo path produces. The combo rejects the basename with `Value not in
list` (the exact error users hit in BE-1116).
Until BE-689 lands both halves of the cloud fix, the asset-picker
registration cannot ship without breaking the node.
## Changes
- **What**: comment out the two ultralytics entries in
`src/platform/assets/mappings/modelNodeMappings.ts` with an inline note
pointing at BE-689 and PR #12075 / #12151 for context.
- **Breaking**: none. The node returns to the same static-combo behavior
it had between #12075 and #12151.
- **Dependencies**: none.
## Review Focus
- The existing assertion in `src/stores/modelToNodeStore.test.ts:278`
already expects `ultralytics`, `ultralytics/bbox`, and
`ultralytics/segm` to NOT resolve to a default provider — that test was
added with #12075 and was left in place when #12151 re-added the
mappings. After this change the mappings file is consistent with the
test again. 55/55 in `modelToNodeStore.test.ts` pass locally.
- This is a targeted revert of the ultralytics half of #12151. The other
entries that PR added (`background_removal`, `frame_interpolation`,
`film`) are unaffected.
## Companion PRs
- Comfy-Org/cloud#4395 — fixes the `ONNXDetectorProvider` half of the
FaceDetailer-broken report (BE-1116) by repairing the dummy-placeholder
mechanism so the static combo carries the right model list. ONNX uses
the static-combo path (no `MODEL_NODE_MAPPINGS` entry), so the
cloud-side fix is sufficient there.
<!-- Fixes BE-689 -->
## Summary
Stabilizes the locale-specific Node Help E2E by setting the locale
through the existing Playwright settings fixture before app bootstrap
instead of racing a workflow reload pulse.
## Changes
- **What**: Removed the brittle in-page locale mutation helper and uses
`test.use({ initialSettings: { 'Comfy.Locale': 'ja' } })` for the
locale-specific documentation case.
- **What**: Keeps the Japanese and English doc routes local to the test,
then verifies the Japanese help content after loading the default
workflow.
- **Dependencies**: None.
## Review Focus
Please focus on whether the E2E now waits on the correct setup boundary.
The previous helper watched `ChangeTracker.isLoadingGraph` after
changing `Comfy.Locale`; once unrelated workflow-load work became
faster, that loading pulse could complete before the helper observed it.
Pre-boot `initialSettings` avoids that timing dependency and uses
existing test infrastructure.
Verification:
- `pnpm exec oxfmt --check browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/nodeHelp.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5176 pnpm
exec playwright test browser_tests/tests/nodeHelp.spec.ts --grep "Should
handle locale-specific documentation" --project=chromium
--repeat-each=10`
## Screenshots (if applicable)
N/A
## Summary
Stabilizes the cloud template filtering E2E by making the test own both
startup asset API responses and the complete template universe it
asserts against.
## Changes
- **What**: Uses the existing `createCloudAssetsFixture([])` fixture so
startup `/api/assets` calls do not create an unrelated error toast that
can intercept the Clear Filters click.
- **What**: Extends `TemplateHelper.mockIndex()` to also mock
`/api/workflow_templates` as an empty custom-template map, so tests that
configure a core template index do not accidentally include custom
templates from the local backend.
- **Dependencies**: None.
## Review Focus
Please focus on fixture ownership. This spec asserts exact template
counts, so `templateApi.mockIndex()` should isolate the core template
index and the custom workflow-template endpoint together. The asset
fixture change is intentionally scoped to this cloud spec and reuses
existing infrastructure instead of dismissing arbitrary toasts or
forcing clicks.
Verification:
- `pnpm exec oxfmt --check
browser_tests/tests/templateFilteringCount.spec.ts
browser_tests/fixtures/helpers/TemplateHelper.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/templateFilteringCount.spec.ts
browser_tests/fixtures/helpers/TemplateHelper.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test
browser_tests/tests/templateFilteringCount.spec.ts --grep "clear filters
button resets" --project=cloud --repeat-each=5`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test
browser_tests/tests/templateFilteringCount.spec.ts --project=cloud`
## Screenshots (if applicable)
N/A
## Summary
`pnpm typecheck:browser` (`vue-tsc --project
browser_tests/tsconfig.json`) currently fails on `main`, turning
`lint-and-format` red for every PR built on it:
```
browser_tests/fixtures/helpers/WorkflowHelper.ts(5,15): error TS2459:
Module '"@/composables/useAppMode"' declares 'AppMode' locally, but it is not exported.
```
## Cause
#12925 (`bc885f383c`, "Decouple run telemetry context from providers")
moved the `AppMode` type from `@/composables/useAppMode` to
`@/utils/appMode`. `useAppMode` now only `import type`s `AppMode` (it no
longer re-exports it), so this fixture's `import type { AppMode } from
'@/composables/useAppMode'` became invalid. The other `useAppMode`
import sites are unaffected — they import the `useAppMode` composable
(still exported), not the type.
## Fix
Import `AppMode` from its actual source, `@/utils/appMode`. One line, no
behavior change.
Verified `vue-tsc --project browser_tests/tsconfig.json` no longer
reports the `WorkflowHelper.ts` error.
<img width="1155" height="648" alt="스크린샷 2026-06-19 오후 11 24 27"
src="https://github.com/user-attachments/assets/01ed2607-662f-4735-b0c2-2f1a2c8a8811"
/>
## Summary
Stabilizes the Mask Editor screenshot E2E by hiding the transient brush
cursor before capturing the dialog.
## Changes
- **What**: Moves the pointer from the mask editor pointer zone to the
Brush Settings panel before the screenshot and asserts that the brush
cursor is hidden.
- **Dependencies**: None.
## Review Focus
Please check that the test still exercises the dialog UI while excluding
only cursor-position noise from the screenshot. The PR also includes the
existing browser-test `AppMode` type import fix needed for
`typecheck:browser` on branches that touch `browser_tests/**`.
Validation:
- `pnpm exec oxfmt --check browser_tests/tests/maskEditor.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm exec eslint browser_tests/tests/maskEditor.spec.ts
browser_tests/fixtures/helpers/WorkflowHelper.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm
exec playwright test browser_tests/tests/maskEditor.spec.ts --grep
"opens mask editor from image preview button" --project=chromium
--repeat-each=10`
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Fixes FE-1014 by making legacy LiteGraph honor backend-provided hidden
widget metadata.
This PR adds a regression test for the Painter node and updates
LiteGraph widget construction so that a backend input spec with `hidden:
true` is mirrored onto the top-level `widget.hidden` property that the
legacy canvas renderer actually reads.
## Problem
Backend node definitions can mark inputs as hidden, for example with
`extra_dict={"hidden": True}`. That metadata already flows into the
frontend widget options as `widget.options.hidden`, which is why Vue
nodes correctly hide those fields.
Legacy LiteGraph, however, does not use `widget.options.hidden` for
canvas visibility. Its rendering, layout, and hit-testing paths check
top-level `widget.hidden` instead. As a result, a field could be hidden
in Vue nodes while still appearing as an editable control in the legacy
LiteGraph canvas.
For affected nodes, this exposes fields that are intended to be
implementation details, schema/version values, or other non-user-facing
inputs.
## Root Cause
The frontend widget construction path copied backend display metadata
into `widget.options`, including:
- `advanced`
- `hidden`
But it did not mirror backend `hidden` metadata into `widget.hidden`.
That created a renderer split:
- Vue nodes and the right panel use `widget.options.hidden`.
- Legacy LiteGraph uses top-level `widget.hidden`.
So backend-hidden widgets were hidden in Vue mode but still visible and
clickable in legacy LiteGraph mode.
## Implementation
The production change is intentionally small and scoped to
backend-provided hidden metadata:
- Continue assigning `inputSpec.hidden` to `widget.options.hidden` as
before.
- When `inputSpec.hidden` is explicitly defined, also assign it to
top-level `widget.hidden`.
This keeps Vue behavior unchanged while making the legacy LiteGraph
renderer receive the same backend hidden signal through the field it
already uses for visibility.
The fix deliberately does not mirror `advanced` into top-level
`widget.advanced`. While investigating this area, I found that many
backend inputs define `advanced`, and changing legacy advanced-widget
behavior would be a much broader behavioral change than FE-1014
requires. This PR only addresses hidden metadata.
## Test Coverage
This PR adds and tightens Painter regression coverage because Painter
currently provides a concrete backend-hidden widget case:
- In Vue mode, the test verifies hidden Painter widgets are not rendered
to the user.
- In legacy LiteGraph mode, the test disables Vue nodes, loads the
Painter workflow, clicks the rows where backend-hidden number widgets
used to be exposed, and verifies the legacy graph editor dialog does not
open.
The legacy test specifically covers the backend-hidden number widgets
`width` and `height`. It uses user-observable behavior rather than
asserting internal widget flags directly.
A follow-up discussion is ongoing about the broader contract between
`widget.options.hidden` and top-level `widget.hidden`, especially for
frontend-extension-only hiding such as Painter `bg_color`. This PR
intentionally keeps that broader renderer-contract question out of scope
and focuses on backend `hidden` metadata from FE-1014.
## Validation
Validated locally with targeted Playwright coverage:
```bash
PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec playwright test browser_tests/tests/painter.spec.ts --project=chromium -g "Does not render hidden standard widgets|Does not open editors for backend-hidden number widget rows"
```
Result:
```text
2 passed
```
Also validated with linting:
```bash
pnpm eslint src/services/litegraphService.ts browser_tests/tests/painter.spec.ts
pnpm eslint browser_tests/tests/painter.spec.ts
```
The commit hooks also passed:
- `oxfmt`
- `oxlint`
- `eslint`
- `pnpm typecheck`
- `pnpm typecheck:browser`
## Notes
The new legacy test was confirmed red before the production fix and
green after the production fix, so it is not a vacuous assertion. The
final cleanup commit only tightens test naming and coordinate handling
while preserving the same regression intent.
## Summary
Split Load3d into Viewport3d (model-agnostic viewport scaffolding) and
Load3d (extends Viewport3d, adds
loader/model/animation/HDRI/recording/gizmo).
Viewport3d exposes only render-loop plumbing, layout, mouse status,
camera orchestration, and the SceneOverlay protocol so future 3D node
viewports can compose it without inheriting model machinery.
- SceneOverlay protocol
(attach/detach/update/onActiveCameraChange/dispose) gives any 3D node a
managed lifecycle slot for plugging scene content into the viewport.
- Viewport3d.setExternalActiveCamera(cam | null) for POV swap: renders
from an externally-owned camera (e.g. a subject camera authored by an
overlay), with OrbitControls detached and the view helper hidden.
- ControlsManager.detach()/attach() back the POV control gating.
- Two-phase init via Viewport3d.start() so subclass field assignments
finish before any render-path code dispatches through overridden
tickPerFrame / isActive.
Behavior preserved for all 5 Load3d consumers (Load3D, Preview3D,
PreviewGaussianSplat, PreviewPointCloud, SaveGLB).
Sets up the upcoming CreateCameraInfo preview and other future
Three.js-based 3D nodes (Pose Editor, Animation Director) to compose
Viewport3d plus their own SceneOverlay implementation.
## Summary
A completed workspace top-up refreshed only the balance, leaving billing
status — and `subscription.hasFunds` (derived from
`statusData.has_funds`) — stale until the next status fetch. The
completed handler now refreshes both.
## Changes
- **What**: `TopUpCreditsDialogContentWorkspace.vue` completed branch —
`await fetchBalance()` → `await Promise.all([fetchBalance(),
fetchStatus()])` (both already exposed on `useBillingContext()`).
- **Breaking**: none.
## Review Focus
- Pre-existing bug (predates the B2 facade; `main`'s top-up already
called `fetchBalance` only). Test validity proven by reverting to
balance-only → the completed case goes red on the `fetchStatus`
assertion.
- Tests: completed → both refresh; pending / failed → neither (3 cases).
typecheck / oxlint / eslint / stylelint / oxfmt / knip clean.
Fixes FE-932
## Summary
Move run-button context assembly out of telemetry providers so telemetry
can initialize without importing app-mode/workspace state.
## Changes
- **What**: Providers now accept completed `RunButtonProperties`;
run-button call sites use a workspace composable to build that payload.
- **Dependencies**: None.
## Summary
- Migrate the website's top nav from bespoke components (`SiteNav`,
`MobileMenu`, `NavDesktopLink`, `PillButton`, `MaskRevealButton`) to
shadcn-vue primitives (`NavigationMenu`, `Sheet`, `Button`), split into
`HeaderMain` → `HeaderMainDesktop` + `HeaderMainMobile`.
- Mobile nav becomes a `Sheet` with drill-down sub-navigation, scroll
lock, sticky CTAs, sr-only i18n title/description, and a back-to-home
logo.
- Desktop nav uses `NavigationMenu` with shared viewport, featured
cards, `NavColumn` extraction, and centralized nav data in
`data/mainNavigation.ts`.
- Adds i18n strings for nav labels, dropdown column headers, close/back
affordances, and the mobile menu description.
## Test plan
- [ ] Desktop: hover PRODUCTS / COMMUNITY / COMPANY — dropdowns open
with featured card + columns, NEW badges render, external links show the
arrow-up-right icon, viewport is shared between triggers.
- [ ] Mobile (<lg): open hamburger sheet — verify logo + close, body
scroll is locked while open, top-level nav scrolls if it overflows, CTAs
stay pinned at bottom.
- [ ] Tap COMMUNITY / PRODUCTS / COMPANY — sub-panel slides over root
nav, in-sheet BACK returns to root, links navigate correctly.
- [ ] Reload `?locale=zh-CN`: dropdown labels, sheet title/description,
BACK / close affordances all localized.
- [ ] No regressions on `/cloud`, `/cloud/pricing`, `/customers`, etc. —
pages that swap CTAs (`BrandButton` → shadcn `Button`) still render at
every breakpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Non preview, `canvasOnly` widgets like `control_after_generate` could be
displayed in the subgraph editor even though promoting them would have
no visual or functional effect when in vue mode.
In vue mode, these entries are now hidden from the list of candidate
items for promotion to reduce confusion.
## Summary
Account preconditions (sign-in / subscription / credits) on running a
workflow now open their modal directly and stay out of the error panel +
error count — previously `subscription_required` fell through to a red
"1 ERROR — Subscription required to queue workflows" banner. This covers
**both** paths: the `execution_error` websocket event and the `POST
/prompt` 402 queue paywall (`{ type: "PAYMENT_REQUIRED", message:
"Subscription required to queue workflows" }`), which is the exact
payload reported in #12840.
## Changes
- **What**: `execution_error` is classified by a pure
`accountPreconditionRouting` resolver (precedence sign-in > subscription
> credits) and routed to the existing modal via
`useAccountPreconditionDialog`; `executionStore` returns early for
preconditions so they never populate `lastExecutionError` /
`lastPromptError` / `lastNodeErrors` → fully excluded from the panel and
`totalErrorCount`. Runtime credit error at a node → credits modal (out
of panel; can name the node).
- **Queue paywall**: the `queuePrompt` catch resolves the same
precondition from the `POST /prompt` 402 response and opens the modal,
short-circuiting before `lastPromptError`, so the queue paywall stays
out of the panel too. The runtime matcher learns the `"Subscription
required to queue workflows"` message.
- **Breaking**: none.
## Before / After
Free-tier queue paywall (`POST /prompt` → 402) on a cloud build:
**Before** — raw `Subscription required to queue workflows` surfaced in
the error panel (no actionable upgrade):
<img width="1600" height="873" alt="before-error-panel"
src="https://github.com/user-attachments/assets/1b76b742-16bf-47e3-9245-17e35f8f1e70"
/>
**After** — clean subscription modal opens; nothing in the error panel
or error count:
<img width="1600" height="873" alt="after-subscription-modal"
src="https://github.com/user-attachments/assets/13d238cb-20bf-4795-a530-5abcf9968dc7"
/>
## Review Focus
- **Routing-only — the run button is intentionally untouched.** The
original AC#3 ("no Subscribe-to-Run button") is superseded by the FE-978
run-lock decision (pre-emptive role-aware lock, Figma 3253-18671).
Complements #12786 (FE-978 run-lock); disjoint file sets.
- Tests: `accountPreconditionRouting` / `useAccountPreconditionDialog` /
`executionStore` — each precondition routes to its modal and is excluded
from the panel/count; precedence resolves on co-occurrence. Plus
Playwright `browser_tests/tests/subscriptionPaywallError.spec.ts` — the
queue paywall (402) stays out of the error panel, with a control
asserting ordinary queue errors still surface. typecheck / oxlint /
eslint / stylelint / oxfmt / knip clean.
Fixes FE-878
Fixes#12840
## Summary
Adds `@comfyorg/comfyui-desktop-bridge-types` as a workspace package in
the frontend monorepo and changes the frontend app dependency to
`workspace:*`.
Adds a dedicated `Publish Desktop Bridge Types` workflow for publishing
`packages/comfyui-desktop-bridge-types` by its own package version,
without coupling it to the generated `@comfyorg/comfyui-frontend-types`
release. The generated frontend types package still emits a concrete
`@comfyorg/comfyui-desktop-bridge-types@0.1.2` dependency instead of
leaking workspace/catalog protocol references.
The Desktop2 missing-model path uses `window.__comfyDesktop2.isRemote()`
when available, but falls back to the legacy
`window.__comfyDesktop2Remote` marker so frontend rollout stays
compatible with older Desktop builds.
Paired Desktop PR: https://github.com/Comfy-Org/Comfy-Desktop/pull/1112
## Summary
Remove unused `export` keyword from `ExportFormatOption` interface. The
interface is only used internally in `constants.ts` and is not imported
elsewhere.
Fixes knip "unused exported types" error.
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
## Summary
Removes the deprecated Group Nodes feature and replaces it with a
load-time migration that auto-converts any group nodes in a loaded
workflow into Subgraphs (with accepted lossiness).
## Changes
- **What**:
- `groupNode.ts` is now a migration-only extension.
`beforeConfigureGraph` registers temporary node types from
`extra.groupNodes` so instances are created during `configure`; a new
`afterConfigureGraph` hook converts every group node in the root graph
to a subgraph (via `LGraph.convertToSubgraph`), re-scanning until none
remain, then deletes `extra.groupNodes`. A failed conversion removes the
offending node so loading never hangs or breaks.
- Kept the minimum needed: `GroupNodeConfig` (builds the
input/output/widget maps), a slimmed `GroupNodeHandler` exposing a
rewritten `convertToNodes()` that no longer depends on the execution
DTOs, the `globalDefs`/`addCustomNodeDefs` path, and the `nodeDefStore`
`Object.assign` shim the migration relies on to detect group nodes.
- Deleted: the Manage Group Nodes dialog (`groupNodeManage.ts`/`.css`),
execution DTOs (`executableGroupNodeDto.ts`,
`executableGroupNodeChildDTO.ts`), the create/builder flow, recreate,
commands, keybindings, menus, the `isGroupNode` branches in the
right-side panel / error grouping / focus composable, the group-node
branches in node templates, dead i18n keys, and the now-unused
`serialise` clipboard helper.
- Rewrote `browser_tests/tests/groupNode.spec.ts` to assert
auto-conversion; deleted the `ManageGroupNode` page object and
`manageGroupNode()` helper.
- Net: ~2,700 lines removed across 23 files (7 files deleted).
- **Breaking**: Group nodes can no longer be created, managed, or
executed. Existing workflows still load — their group nodes are
converted to subgraphs on open.
## Review Focus
- The load-time migration in `afterConfigureGraph` and the rewritten
`GroupNodeHandler.convertToNodes()` (no longer uses the execution
`getInnerNodes()` / DTOs; derives inner node type/index from
`groupData.nodeData.nodes` and relies on `deserialiseAndCreate` +
selection ordering).
- Kept `nodeDefStore`'s `Object.assign(this, obj)` shim: the migration
depends on it to propagate the group-node marker symbol onto the
registered node definition.
### Accepted lossiness
- Group nodes nested inside subgraphs (or inside other group nodes)
convert into the root graph rather than their original container —
essentially nonexistent in real legacy workflows since group nodes
predate subgraphs.
- Temporary `workflow>name` node types stay registered for the session;
instantiating one auto-converts it to a subgraph.
## Verification
`pnpm typecheck`, `typecheck:browser`, `knip`, `oxlint`, `eslint`, and
`oxfmt` are green (also enforced by pre-commit hooks). Unit tests for
the touched files could not be run locally due to a pre-existing
environment error (`file:///assets/images/*.svg` passed to a Node
filename API at import time, which also fails on unmodified test files);
the browser spec requires a live server.
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Cloud users get yanked to `/cloud/survey` mid-workflow with no user
action. The redirect is **downstream of auth**: when the Cloud token is
briefly stale (token rotation / auth-refresh / reconnect window), the
authenticated survey-status check 401s, and the gate turned that
transient 401 into "survey not completed" → redirect.
Surveys are currently disabled for everyone on cloud via dynamicconfig
as the live mitigation. This PR lets us re-enable them safely
**without** waiting on the auth rework.
## Root cause
`getSurveyCompletedStatus()` returned `false` ("not completed") on
**any** non-200 — including a transient 401/403/5xx or network error —
and consumers treat `false` as a redirect to the survey. So a
stale-token 401 (or the page force-reload a 401 triggers in
`GraphCanvas`) bounced a working, already-onboarded user to the survey.
The real root cause of the transient 401s is a separate, still-open
effort: **FE-963** (reactive 401 re-mint + single retry), **FE-950/951**
(unified Cloud JWT), **BE-1125**. This PR does **not** fix those; it
stops the survey from being their user-visible casualty.
## The fix
`getSurveyCompletedStatus` now distinguishes the responses instead of
failing closed on all of them:
- **404** → not completed (show survey). This is the genuine signal: the
cloud backend (`GetSettingById`) returns 404 for a survey key that was
never stored, and a 404 is only reachable after a successful
authenticated read (a stale token 401s, never 404s), so it can't be a
transient false signal.
- **transient 401/403/5xx/network** → treat as completed (fail-safe), so
a working user is never bounced.
- **200** → completed iff `value` is non-empty (unchanged).
**No router change.** The `/` onboarding guard is untouched (router.ts
matches main), so the existing UX is preserved — a not-completed user is
still gated to the survey on load; only the spurious transient-failure
bounce is removed.
## Why #12301 was reverted, and how this differs
#12301 shipped a **blanket** fail-safe (`!response.ok → true`, 404
included), which made the survey unreachable for genuinely-not-completed
users (404 → "completed") and was reverted in #12344. This PR
special-cases **404 as the real not-completed signal** and fails safe
only on transient/ambiguous responses, so onboarding still works.
## Tests
- **Unit** (`auth.test.ts`): 200 non-empty → true; 200 empty / `null` /
missing `value` key → false; **404 → false**; 401/403/500/network →
true.
- **E2E** (`browser_tests/tests/cloudSurveyGate.spec.ts`, `@cloud`): a
transient 401 on `/` does **not** bounce a working user; a genuine 404
on `/` **does** route to the survey.
Linear: FE-739. Root cause (separate): FE-963 / FE-950 / FE-951 /
BE-1125.
## Summary
- **Pricing copy** — replace "concurrent API jobs" wording with
"workflows via API" on Creator/Pro plans, add a third feature line to
Standard, and add a new "Run Workflows via API" entry to the "What's
Included" section (existing feature11 → feature12).
- **Tutorial captions** — wire per-locale VTT caption tracks into
`TutorialDetailDialog`, so the CC button in `VideoPlayer` is now
functional for the learning tutorials. `LearningTutorial.caption` is
typed as `readonly VideoTrack[]`; `VideoTrack` is now exported from
`VideoPlayer.vue`.
- Tailwind class ordering in `PriceSection.vue` shifted due to the
formatter on touched lines.
## Test plan
- [ ] `/pricing` page (EN + zh-CN): Standard plan shows the new "1
workflow via API" line; Creator/Pro show "3/5 workflows via API";
"What's Included" lists "Run Workflows via API" above "Parallel job
execution".
- [ ] Open a learning tutorial → CC button is visible in the player →
toggling CC shows English captions in sync with playback.
- [ ] Tutorials without a `caption` entry hide the CC button.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Fixes FE-485.
This updates workspace auth refresh handling so stale in-flight refresh
responses cannot overwrite a newer workspace context, and exhausted
transient token exchange failures preserve the existing workspace
context while its token is still valid.
## Changes
- Add commit-time request-id guards before `switchWorkspace` writes
workspace state, workspace token, `error`, or `sessionStorage`.
- Track the current workspace token expiry in memory and use it to
distinguish safe transient refresh failures from failures that must
clear context.
- Convert the stale refresh race coverage from expected-failing to a
normal passing regression test.
- Update transient retry coverage to assert valid context and
`sessionStorage` preservation.
## Browser / E2E coverage
No Playwright test was added because this bug is in the Pinia store race
between mocked token-exchange promises, request IDs, token expiry, and
`sessionStorage` commits. The deterministic unit spec directly controls
the ordering that is not practical to force through the browser without
real auth/session infrastructure and artificial network timing hooks.
## Validation
- `pnpm format -- src/platform/workspace/stores/workspaceAuthStore.ts
src/platform/workspace/stores/useWorkspaceAuth.test.ts`
- `pnpm exec vitest run
src/platform/workspace/stores/useWorkspaceAuth.test.ts`
- `pnpm exec eslint src/platform/workspace/stores/workspaceAuthStore.ts
src/platform/workspace/stores/useWorkspaceAuth.test.ts`
- `pnpm exec oxlint src/platform/workspace/stores/workspaceAuthStore.ts
src/platform/workspace/stores/useWorkspaceAuth.test.ts --type-aware`
- `pnpm exec vue-tsc --noEmit --pretty false`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11726-fix-guard-workspace-auth-refresh-races-3506d73d365081b99df3c1bf3d0e008a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
When right clicking an output asset from the assets sidebar panel, the
'insert as node in workflow' action was twice bugged
- The default type, as used for determining filename annotation, was set
to the type of the file. This meant that annotations would never be
applied to the filename
- `temp` outputs would incorrectly be assigned the `output` type.
- My fix for this one gives me a slightly bad taste in my mouth. Parsing
URLs isn't great, but it's cleaner than needing to scan the (potentially
sparse) full outputs to try and find the corresponding output.
## Summary
Update the default workflow to use a more modern model than SD1.5. This
new workflow uses Z-Image Turbo and is the same workflow as the one in
the README for consistency.
## Changes
- **What**: `src/scripts/defaultGraph.ts`
## Screenshots (if applicable)
<img width="1920" height="1152"
alt="{2DD28B9F-A9E7-4DD7-8F07-AF7241F5702E}"
src="https://github.com/user-attachments/assets/6e6ee298-a786-4a8c-adf3-6452df08a995"
/>
---------
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
While a file is uploading to any of the file picker nodes (ie "Load
Image"), the 'select folder' icon is replaced with a loading spinner.
I had previously implemented this with a full progress bar indicating
the rate of upload, but found it particularly uninformative when used on
cloud.
<img width="659" height="494" alt="image"
src="https://github.com/user-attachments/assets/6d3ca82b-360b-44bc-b123-b276aae2c4d6"
/>
## Summary
- Merges the separate Playwright and Storybook bot comments into the
existing `<!-- COMFYUI_FRONTEND_PR_REPORT -->` unified comment, using
named sections via `upsert-comment-section`
- Each workflow independently upserts its own section without clobbering
others
- `pr-report.yaml` cleans up legacy `<!-- PLAYWRIGHT_TEST_STATUS -->`
and `<!-- STORYBOOK_BUILD_STATUS -->` comments on first run
- Perf report: collapses detailed metric tables in `<details>` by
default — only headline FPS/TBT/heap summary and a brief regression
count are visible inline
## Before → After
**Before:** 4–5 separate bot comments per PR (Playwright, Storybook,
Bundle/Perf/Coverage, Chromatic URLs) + CodeRabbit + Codecov
**After:** 1 unified comment with collapsible sections:
```
<!-- COMFYUI_FRONTEND_PR_REPORT -->
<!-- section:playwright:start --> ... <!-- section:playwright:end -->
<!-- section:storybook:start --> ... <!-- section:storybook:end -->
<!-- section:ci-metrics:start --> ... <!-- section:ci-metrics:end -->
<!-- section:chromatic:start --> ... <!-- section:chromatic:end -->
```
(CodeRabbit and Codecov are external and can't be merged)
## How it works
`upsert-comment-section` already existed for the website CI comment.
This PR extends that pattern to the main PR report comment:
1. **Playwright** — shell script writes to `SUMMARY_FILE` when set, CI
workflow uses `upsert-comment-section` with `section-name: playwright`
2. **Storybook** — same pattern, `section-name: storybook`
3. **Bundle/Perf/Coverage** — `pr-report.yaml` now also uses
`upsert-comment-section` instead of replacing the whole comment,
`section-name: ci-metrics`
4. **Chromatic** — `section-name: chromatic`, no script change needed
Both fork and non-fork PR paths updated.
## Perf report noise fix
Previously the full per-metric regression table was shown inline. Now:
- **Visible:** headline summary (avg FPS, P5 FPS, TBT, heap per test) +
"⚠️ N regressions detected" count
- **Collapsed by default:** regression detail table, all-metrics table,
historical variance, trend data
## Test plan
- [ ] Open a PR with frontend changes — confirm single unified bot
comment appears
- [ ] Confirm Playwright section appears with ⏳ then updates to results
- [ ] Confirm Storybook section appears only when storybook-relevant
files change
- [ ] Confirm legacy standalone Playwright/Storybook comments are
deleted by `pr-report.yaml`
- [ ] Confirm perf report shows headline only, details collapsed
- [ ] Test with a fork PR — confirm fork path also works
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11935-ci-consolidate-PR-bot-comments-into-one-unified-comment-3566d73d36508173964cdf596cfae1f8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Removes LiteGraph's vestigial `use_uuids` node-id mode, which was never
enabled anywhere in the codebase.
## Changes
- **What**: Deletes the `LiteGraph.use_uuids` flag and the node-id
branches that read it. Newly created/added/cloned nodes now always
receive integer ids. The `asSerialisable` sort guard drops its
`@ts-expect-error` in favor of an explicit `Number(a.id) - Number(b.id)`
comparison.
- **Breaking**: `LiteGraph.use_uuids` is removed from the public
`LiteGraph` global. It defaulted to `false` and nothing ever set it to
`true`, so behavior is unchanged for all real usage — but the property
no longer exists on the global surface. Litegraph changes can affect
downstream custom-node repos.
`createUuidv4` / `LiteGraph.uuidv4` are intentionally kept — still used
for subgraph ids, slot ids, and clipboard remapping.
## Review Focus
- Confirm hard removal (vs. deprecate-then-remove no-op getter) is
acceptable for the public `LiteGraph` surface.
- `asSerialisable` sort now coerces ids via `Number()`; all serialised
node ids are numeric, so ordering is unchanged.
## Notes
- 2 litegraph test suites (`LGraphCanvas.clipboard.test.ts`,
`SubgraphWidgetPromotion.test.ts`) fail at import with a pre-existing
asset-URL error unrelated to this change (confirmed they fail with these
changes stashed). All 909 tests that ran pass.
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Stop Vitest from crashing on Windows when a unit test imports a Vue
component whose template references an absolute public-asset URL (e.g.
`<img src="/assets/images/default-template.png">`).
## Changes
- **What**: Pass `template.transformAssetUrls.includeAbsolute: false` to
`@vitejs/plugin-vue` under Vitest only. With no dev server, the plugin
forces `includeAbsolute: true`, compiling absolute public-asset URLs
into module imports. Vite's module runner then derives a rootless
`import.meta.url` of `file:///assets/...`, which has no drive letter on
Windows, so Vitest's `createRequire(import.meta.url)` throws
`ERR_INVALID_ARG_VALUE` and aborts collection of any test transitively
importing such a component. Disabling the absolute transform under tests
keeps these as plain string literals (identical rendered output).
- **Breaking**: None — change is scoped to `process.env.VITEST`; dev and
production builds are unchanged.
## Review Focus
`vite.config.mts` — the `vuePluginOptions` const is `undefined` outside
Vitest, so only the test runner is affected.
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Remove unused `export` keyword from `Load3dSerializedBase` type. The
type is only used internally as the return type of `snapshotLoad3dState`
and is not imported elsewhere.
Fixes knip "unused exported types" error.
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
## Summary
On first launch, the templates modal flashed open for a split second
before a deeplinked template (`?template=`) loaded, which felt broken.
## Changes
- **What**: Gate the first-launch templates modal on template URL
intent, alongside the existing shared-workflow (`?share=`) check. When a
template is being opened directly from the URL, the template modal no
longer opens. Behavior is unchanged when no template is in the URL — the
template modal still shows for first-time users.
- Test util: Added browser_tests/fixtures/utils/flashDetector.ts —
installs a pre-navigation requestAnimationFrame sampler that flags if a
[data-testid] element ever renders, even for a single frame. This
catches a brief flash that toBeHidden() (final-state only) cannot.
## Review Focus
`hasTemplateUrlIntent()` mirrors the existing
`hasSharedWorkflowIntent()` (direct `route.query` check plus
preserved-query fallback for the `/user-select` redirect path). Two
regression tests cover both the URL-param and preserved-intent cases.
**Coverage:**
- Unit (useWorkflowPersistenceV2.test.ts): the modal is not opened when
a template param is in the URL, and when template intent is preserved
across the /user-select redirect.
- E2E (templates.spec.ts): templates dialog never flashes when
first-time user opens a template link — verified red-without-fix,
green-with-fix.
Screen Recording
https://github.com/user-attachments/assets/636094d4-0ef0-4e42-af32-d4e6c7ec5731closes#12836
## Summary
Adds a team-gated, label-triggered multi-model Cursor review as a **thin
caller** for the reusable workflow in `Comfy-Org/github-workflows` — the
single source of truth for the panel, judge, prompts, and scripts. This
repo carries only the ~50-line caller, so there's no review logic to
drift out of sync.
## Changes
- **What**: `.github/workflows/pr-cursor-review.yaml` triggers on the
`cursor-review` label and calls
`Comfy-Org/github-workflows/.github/workflows/cursor-review.yml`, pinned
to `047ca48` (github-workflows#9, current main). Inheriting the reusable
workflow brings severity badges, line-anchored inline comments,
diff-size caps, prompt-injection hardening, and optional Slack DMs.
- **Config**: `diff_excludes` restated (overriding replaces the default
wholesale) with this repo's heavy paths added (Playwright snapshots,
generated manager types). Judge and panel both default to Opus 4.8 via
the reusable workflow — no overrides needed.
## Review Focus
- **Access control (the point).** Two layers, no allowlist: (1) only
triage+ users can apply a label in a public repo; (2) the reusable
workflow's secret-bearing jobs don't run on fork PRs, so
`CURSOR_API_KEY` is reachable only on internal branches.
- **Replaces a standalone draft.** Earlier revisions of this branch
carried a self-contained workflow + review/judge scripts; that
duplicated the reusable workflow, so it's been swapped for the thin
caller.
## Prerequisites (already done)
- `CURSOR_API_KEY` secret set on this repo.
- `cursor-review` label created.
- `SLACK_BOT_TOKEN` already present (enables the DM feature).
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Enable Codecov carryforward on the `unit` and `e2e` flags so a missing
or late E2E coverage upload no longer produces a false `codecov/patch`
failure.
## Changes
- **What**: Add a `flags` block to `codecov.yml` marking `unit` and
`e2e` as `carryforward: true`. When a flag's upload is absent for a
commit, Codecov reuses that flag's last known coverage for unchanged
files instead of treating those lines as patch misses.
## Review Focus
The `e2e` flag is uploaded by the separate `ci-tests-e2e-coverage`
`workflow_run` job, which runs after CI and currently fails or skips
intermittently (uploaded with `fail_ci_if_error: false`). When that
upload is missing, the head report holds only the `unit` session, so
E2E-only code paths (canvas, vue-nodes, minimap, glsl, etc.) report as
uncovered and the patch status fails against the full-coverage base.
Carryforward fixes that symptom; the flaky coverage workflow remains a
separate root-cause fix.
Validated with `curl --data-binary @codecov.yml
https://codecov.io/validate` → `Valid!`. Carryforward takes effect after
merge once a `main` build establishes a baseline for both flags.
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Harden the cloud assets media-type filter spec against a VirtualGrid
virtualization flake that intermittently failed CI at the
`waitForAssets(4)` precondition.
## Changes
- **What**: Replace the 7 `waitForAssets(MIXED_JOBS.length)`
preconditions in `assets-filter.spec.ts` with `waitForAssets()` (first
card visible = data loaded), and document why.
## Review Focus
CI artifact (`playwright-report-cloud`) from the failing run showed only
3 of 4 cards in the DOM — the 3D card was missing and the audio card
rendered taller (inline player). `VirtualGrid.vue` sizes its render
window from a single uniform `itemHeight` measured off the first card,
so the taller audio card pushes the 3D card out of the initial window
and it is virtualized out of the DOM until a re-measure (same cause as
#11635).
Requiring all 4 cards to be mounted simultaneously fights
virtualization. `tab.open()` already waits for the first card (data
loaded), and filtering reads the full asset store regardless of what is
mounted, so the per-filter count assertions still provide the real
coverage. No behavioral change to the tests — only the
readiness/precondition strategy.
Verified `pnpm exec eslint` and `pnpm typecheck:browser` pass. Cloud E2E
could not be run locally (needs a running frontend + backend); relies on
the cloud CI job for confirmation.
Related to #11635
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
The `/cloud/pricing` page's "Add more credits anytime" feature claims
unused top-up credits **roll over** to the next month. Per product
(Pablo, #cloud), top-up credits **do not** roll over. Fix the copy to
match policy and the docs.
## Changes
- **What**: `pricing.included.feature5.description` (en + zh-CN) in
`apps/website/src/i18n/translations.ts`.
- Before: "Purchase additional credits at any time. Unused top-ups roll
over to the next month automatically for up to 1 year."
- After: "Purchase additional credits at any time. Top-up credits are
valid for 1 year from the date of purchase and do not roll over with
your monthly plan."
- Keeps the accurate 1-year validity window (matches
`docs/interface/credits.mdx`).
- Scope: this is the **only** rollover claim on the page. The tier cards
(lines 1224/1255/1286) only say "top-ups available" — no change needed.
- **Breaking**: none (copy only).
## Deliberately out of scope
The Slack thread surfaced an **unresolved** question: Pablo said top-ups
don't roll over *"as opposed to subscription credits which do,"* but
`docs/interface/credits.mdx` says monthly credits don't roll over
either. This PR makes **no claim about subscription/monthly credit
rollover** (the new copy is neutral: "do not roll over *with your
monthly plan*"). Resolving that — and the optional FAQ entry / docs
alignment Glary-Bot proposed — needs a product decision first. Flagging
for follow-up.
## Summary
Redesign the run error overlay so its count and copy use the same
grouped error semantics as the redesigned Error tab, while keeping
single-error toast copy precise.
This is a stacked follow-up to #12828. The parent PR redesigns the Error
tab cards and centralizes their grouped/row-based presentation. This PR
applies the same mental model to the compact overlay shown from the Run
button path, so users no longer see one count in the Error tab and a
different count in the overlay.
## Changes
- **What**: Align the error overlay count with the Error tab's grouped
error count.
- The overlay now reads the same grouped error surface that powers the
Error tab hero instead of independently summing raw store error counts.
- Validation/runtime/prompt execution groups use their grouped `count`.
- Missing model/media/node/swap groups keep the row/group count
semantics introduced by the Error tab redesign.
- This avoids cases where the overlay headline says one number while the
panel summarizes another.
- **What**: Preserve the existing single-error toast behavior while
adding explicit multi-row handling.
- A single true leaf still uses the catalog/resolver toast title and
toast message.
- A single grouped execution error with multiple node/input items uses
that group's title/message instead of the generic aggregate copy.
- A single missing model/media group with multiple model/file rows uses
the generic aggregate copy because it represents multiple actionable
rows.
- A single missing model/media row referenced by multiple nodes uses the
group title/message, since there is still only one model/file to
resolve.
- Multiple top-level groups continue to use the aggregate "N errors
found" style copy.
- **What**: Restyle the overlay toast to match the new error-surface
direction.
- Adds a compact dark card with a destructive left accent and a visible
outline for better contrast against the workspace.
- Keeps the close button in the card header area.
- Keeps the primary "View details" action, but adjusts spacing, size,
and typography to better match the Figma direction.
- Removes the older footer-style dismiss action so the overlay behaves
like a focused status toast rather than a secondary dialog.
- **What**: Share error grouping/count helpers instead of duplicating
local logic.
- Extracts execution item-list detection for reuse between the Error tab
render path and count logic.
- Extracts missing-model grouping/count helpers so missing-model row
count semantics have one implementation.
- Removes `groupedErrorMessages`, which became unused after the overlay
copy decision moved to grouped error state.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
- **Stack boundary**: Please review this against
`jaeone/fe-816-error-card-redesign`, not against `main` directly. The
parent PR is #12828 and contains the Error tab card redesign that this
overlay work builds on.
- **Count semantics**: The visible overlay count intentionally changes
from raw store counts to grouped Error tab counts. This is not just a
refactor; it is the intended product behavior so the compact overlay and
the panel hero agree.
- **Overlay message branches**: The overlay can now choose between three
message modes. The goal is to keep precise single-error copy where it is
useful, but avoid showing one node-specific toast message when the
overlay actually represents multiple actionable rows.
| Branch | When it applies | Overlay title source | Overlay message
source | Example output |
| --- | --- | --- | --- | --- |
| Aggregate summary | More than one top-level error group, or one
missing model/media group with multiple actionable model/file rows. |
Generic aggregate title using grouped count. | Generic aggregate
message. | Title: `2 errors found`<br>Message: `Resolve them before
running the workflow.`<br>Example case: one Missing Models group
containing `first.safetensors` and `second.safetensors`. |
| Group summary | Exactly one error group that is not a true single
leaf, but should still be described by the group. This includes one
execution catalog group with multiple items, or one missing model/media
row referenced by multiple nodes. | The group's `displayTitle`. | The
group's `displayMessage`. | Title: `Missing connection`<br>Message:
`Required input slots have no connection feeding them.`<br>Example case:
one validation group with `KSampler - model` and `KSampler - positive`
rows. |
| Single leaf toast | Exactly one group, one actionable row/card, and at
most one node reference. | Resolver/catalog `toastTitle`. |
Resolver/catalog `toastMessage`. | Title: `Model missing`<br>Message:
`CheckpointLoaderSimple is missing missing.safetensors.`<br>Example
case: one missing model file referenced by one node. |
- **Scope control**: This PR intentionally does not redesign the full
Error Overlay flow beyond the compact toast/card. It also does not
revisit the deeper Error tab card layouts already handled in #12828.
- **Accessibility**: The toast keeps `role="status"` for polite
announcement semantics. The duplicate `aria-live` attribute was removed
during cleanup because `role="status"` already implies polite
live-region behavior.
## Validation
- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm knip`
- `pnpm test:unit src/components/error/useErrorOverlayState.test.ts
src/platform/missingModel/missingModelGrouping.test.ts
src/components/error/ErrorOverlay.test.ts
src/components/rightSidePanel/errors/useErrorGroups.test.ts`
- Pre-commit staged checks passed.
- Pre-push `knip` passed.
## Screenshots (if applicable)
<img width="454" height="179" alt="스크린샷 2026-06-16 오후 6 00 10"
src="https://github.com/user-attachments/assets/a85376ba-2b22-4cf8-a6fa-79f83fb8b244"
/>
<img width="453" height="179" alt="스크린샷 2026-06-16 오후 6 00 31"
src="https://github.com/user-attachments/assets/d9a1d4bd-92ab-451a-bb79-e7cfbc3af7c6"
/>
<img width="486" height="148" alt="스크린샷 2026-06-16 오후 6 00 55"
src="https://github.com/user-attachments/assets/b66faf96-65c8-4a22-9ff9-e8ccd450986e"
/>
<img width="395" height="127" alt="스크린샷 2026-06-16 오후 6 01 22"
src="https://github.com/user-attachments/assets/c64443f9-0eba-4b2b-8049-c1887c788b1e"
/>
<img width="384" height="134" alt="스크린샷 2026-06-16 오후 6 01 30"
src="https://github.com/user-attachments/assets/42f4fcae-b003-4df9-8f3a-0fda85a90880"
/>
<img width="376" height="129" alt="스크린샷 2026-06-16 오후 6 01 53"
src="https://github.com/user-attachments/assets/ce9030d0-2a98-4b38-9e7d-7a9c3103960f"
/>
<img width="379" height="128" alt="스크린샷 2026-06-16 오후 6 02 01"
src="https://github.com/user-attachments/assets/3d4ce356-1c22-4e3b-a1d0-fece351d9fbb"
/>
<img width="463" height="133" alt="스크린샷 2026-06-16 오후 6 02 33"
src="https://github.com/user-attachments/assets/6ae13a44-02aa-4167-8878-4906db468ad6"
/>
### before
<img width="539" height="475" alt="Screenshot 2026-06-17 at 9 52 50 PM"
src="https://github.com/user-attachments/assets/dd562cb4-870c-43de-aca4-81e0118735e9"
/>
### after
<img width="529" height="592" alt="Screenshot 2026-06-17 at 9 53 40 PM"
src="https://github.com/user-attachments/assets/ba8ed01e-ac91-4654-bfaa-d8f73923f378"
/>
Design-review polish on the team-workspace billing UI (3 of the items
from the Figma 'Team Plan - Workspaces' review). All three components
are on main.
### 1. Remove dead 'Upgrade' badge in account popover
`CurrentUserPopoverWorkspace.vue` — the white badge beside 'Plans &
pricing' was gated on `canUpgrade`, which is hardcoded `false` (PRO is
the only tier), so it never rendered. Removed the badge markup and the
dead computed. (Figma node 2797-724189.)
### 2. Subscribe-to-Run button height
`SubscribeToRun.vue` — used `size="sm"`, which didn't match the sibling
run/queue button (an `h-8` button group it swaps with in the same slot
via `CloudRunButtonWrapper`). Switched to `size="unset"` + `h-8
rounded-lg gap-1.5 px-4` to match.
### 3. Duplicate border/radius on member 'subscription inactive' dialog
`useSubscriptionDialog.ts` — the layout dialog already zeroes the
*content* border (`border-none shadow-none`), but the *root* pt kept
only `bg-transparent`, so the dialog frame's default border+radius
doubled with the card's own `rounded-2xl border`. Added `border-none
rounded-none shadow-none` to root so only the card's single
border/radius shows. (Figma node 3253-19473.)
Surfaced during Billing V1 design review (team workspaces).
---------
Co-authored-by: GitHub Action <action@github.com>
The workspace name was hard-clipped by the switcher panel's
`overflow-hidden` instead of truncating with an ellipsis. The name span
had `min-w-0` but its parent row `<button>` (`flex flex-1`) did not, so
the button kept its content width and the long name overflowed.
Add `min-w-0` to the row button (and the name span) so the flex chain
can shrink and the name truncates.
Follow-up to FE-769 (#12763, merged) — the component shipped without
this.
## Before / After
<img width="1370" height="538" alt="fe769-before-after"
src="https://github.com/user-attachments/assets/b05d6faf-9dfe-4c93-9941-3c0a9bbfcc2d"
/>
Verified live in the team-workspaces mock (long-named "Acme Studio
Workspace").
### after
<img width="703" height="302" alt="Screenshot 2026-06-17 at 9 54 34 PM"
src="https://github.com/user-attachments/assets/725c3175-b65f-4224-aca9-3de777c95e85"
/>
## Summary
Cancelled / inactive team plans keep members but lock runs; the run
button and the subscription-required dialog are now role-aware — owners
are routed to the pricing/subscribe flow, members (who cannot subscribe)
see "contact your workspace owner to resubscribe".
## Changes
- **What**: `SubscribeToRun.vue` becomes a role-aware locked run button
(owner → "Subscribe to Run"; member → neutral locked "Run" +
contact-owner tooltip; both open the subscription dialog).
`SubscriptionRequiredDialogContentWorkspace.vue` branches on role
(member → read-only contact-owner panel, no pricing/subscribe
affordance; owner → existing pricing/preview; member view suppressed for
`out_of_credits` so the active-but-low-credits path is unchanged).
`subscription.inactive.*` i18n keys.
- **Breaking**: none.
## Review Focus
- Role source =
`useWorkspaceUI().permissions.value.canManageSubscription` (owner /
personal = true, member = false) — the same accessor
`SubscriptionPanelContentWorkspace.vue` uses.
- **No BE work**: the run-gate already exists server-side
(`InactiveSubscriptionError`; `is_active` checked before funds). The
lock is gated on `is_active`, the same field the orchestrator uses, so
FE/BE stay consistent; leftover-credits-while-inactive remains blocked
by design.
- Complements #12785 (FE-878 precondition→modal routing); disjoint file
sets. Design: DES-197, Figma 3253-18670 / 3253-18671 / 3246-13962.
- Tests: `SubscribeToRun` (4) / `CloudRunButtonWrapper` (3) /
`SubscriptionRequiredDialogContentWorkspace` role cases — member sees
contact-owner (no subscribe), owner sees pricing, run locks on
`!is_active` and unlocks when active (22 total); full `test:unit` green.
Fixes FE-978
## Summary
Make `WidgetId` (`graphId:nodeId:name`) the single canonical widget
identity and represent subgraph promoted host widgets as ordinary
store-backed widgets addressed by it. This deletes two whole indirection
layers — the `world/*` widget-entity-IO layer and the
`PromotedWidgetView` runtime — leaving one model: a widget's data lives
in `widgetValueStore` keyed by `WidgetId`, and a `SubgraphNode` input
references it via `input.widgetId`.
**Net +304 lines across 107 files** (5,044 added / 4,740 deleted):
production code is net **−798** (1,521 added / 2,319 deleted) while
tests are net **+1,102** (3,523 added / 2,421 deleted). 10 files deleted
outright, 14 added.
## What got deleted
The old design wrapped every promoted subgraph widget in a synthetic
`IBaseWidget` "view" object with live getters that followed the source
widget, plus a manager to keep view identities stable, plus an IO
indirection layer over the store. All of it is gone:
- `promotedWidgetView.ts` — the `PromotedWidgetView` class (draw /
pointer / DOM-sync / projection / deepest-source resolution getters)
- `PromotedWidgetViewManager.ts` — view reconciliation/caching
- `world/widgetValueIO.ts` — the IO wrapper over `widgetValueStore`
- `world/entityIds.ts` + `world/brand.ts` — the `WidgetEntityId`
branded-id layer and the `entityId` field
- `widgetNodeTypeGuard.ts` — only used by the deleted view
- the per-`SubgraphNode` view machinery (`_promotedViewManager`,
`_cacheVersion`, view-key generation, DOM position-override cleanup) and
every now-dead `isPromotedWidgetView` branch across the panel, menu,
store, and util consumers
- `domWidgetStore` position-override APIs (`setPositionOverride` /
`clearPositionOverride`), only used to render a promoted DOM widget on a
different host node
## Why it's simpler
- One source of truth. A promoted host widget is `WidgetState` in the
store, seeded from the source at promotion (`registerWidget` with a
deep-cloned snapshot) and independent thereafter. No synthetic widget
objects, no runtime source-following, no view cache to invalidate.
- Resolution is data-driven. `resolveConcretePromotedWidget` walks
`SubgraphNode` inputs (`input.widgetId` + `resolveSubgraphInputTarget`)
instead of chasing view objects through `node.widgets`. This also
**fixes two-layer nested promotion** — the previously-skipped parity
test now passes and resolves through to the deepest concrete widget.
- The right-panel Parameters tab renders a subgraph node's promoted
widgets through the **same** store-backed path as ordinary node widgets:
display reads `WidgetState` via `widget.widgetId`, and value writes go
through `widgetValueStore.setValue(widgetId)`.
## Changes
- **What**:
- `WidgetId` branded type + `widgetId()` / `parseWidgetId()` /
`isWidgetId()` and a `WidgetState` type; `widgetValueStore` is
`WidgetId`-native (`registerWidget` / `getWidget` / `setValue` /
`deleteWidget`).
- Promotion creates host `WidgetState` then an input projection
(`input.widgetId`); demotion clears it; serialization and legacy
`proxyWidgets` migration round-trip through `input.widgetId`.
- `promotedInputWidget.ts` projects a store-backed ordinary widget from
an input slot; `SubgraphNode.widgets` is now a projected getter over
inputs (kept Litegraph-shaped so the canvas renderer and extensions
still read `node.widgets`). `invalidatePromotedViews()` is retained as a
no-op for extension compatibility.
- `promotedWidgetControl.ts` applies `control_after_generate` (e.g. seed
increment) on the host node, since the interior control widget is
link-fed and its value is dead; `syncPromotedComboHostOptions` mirrors
interior combo options onto host state.
- `multilineTextarea.ts` extracts the reusable multiline DOM-widget
behavior out of `useStringWidget` and adds promoted multiline
materialization via a `createPromotedHostWidget` app-layer hook (keeping
Litegraph core free of Vue/Pinia/DOM).
- Late-bound `LiteGraph` singleton holder (`litegraphInstance.ts`) to
break a widget-init import cycle.
- **Breaking**:
- `IBaseWidget.entityId` removed — use `widgetId`.
- `SubgraphNode.widgets` no longer exposes the old `PromotedWidgetView`
objects; promoted state lives in `widgetValueStore` keyed by
`input.widgetId` and `widgets` is a projection of inputs. The
`widget-promoted` event now carries the concrete interior widget.
Extension code reading `entityId` or relying on `PromotedWidgetView` is
affected.
## Review Focus
- Promotion / demotion / serialization round-tripping through
`input.widgetId` + `widgetValueStore`, incl. the legacy `proxyWidgets`
migration.
- Snapshot-at-promotion semantics (host widget does not follow the
source after creation), and the combo-options exception via
`syncPromotedComboHostOptions`.
- Two-layer nested resolution in `resolveConcretePromotedWidget` +
`SubgraphNode` nested-source resolution.
- The unified Parameters tab (`TabSubgraphInputs` → `SectionWidgets`):
value edit / rename / favorite / hide / reorder for promoted inputs are
wired through the store but warrant a visual/e2e pass.
- Litegraph-compat seams worth a careful read: projected
`SubgraphNode.widgets`, the canvas-edit `callback` bridge back to the
store, host-level `control_after_generate`, and the late-bound
`LiteGraph` holder / `domWidget.ts` import ordering.
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
When control is held, an active drag operation should cease applying
movements to nodes contained by selected groups. This functionality was
broken in vue mode because of unnecessary reimplementation of the code
for calculating items contained by groups during drag operations
## Summary
L1 prerequisite cleanup, scoped to a single type-preserving refactor:
extract `getAssetStoredFilename(asset)` to collapse the duplicated
`isCloud && asset.asset_hash ? asset.asset_hash : asset.name` branch
from `useMediaAssetActions.ts` into one helper in
`assetMetadataUtils.ts`. No behavior change.
Once BE-933/934 emit `file_path` and the cloud spec sync brings the
field into generated types, only the helper internals change (collapse
to `asset.file_path ?? asset.name`).
## Scope change (per review)
The `mockFeatureFlags` test util and the exported `FeatureFlags` type
that this PR originally also added have been **split out**. They had no
live consumer in the open stack — FE-729~732 (#12322 / #12335 / #12375 /
#12417) don't use them, and FE-780 / FE-781 (#12485 / #12486) still
hand-roll inline `vi.hoisted` mocks — so shipping them here would add a
public surface with no caller. They will be reintroduced bundled with
the first PR that actually adopts the util, where `featureFlag`'s return
type and the "all flags off vs. production defaults" semantics can be
validated against a real consumer.
## Review fixes carried in this PR
- Mock `@/platform/distribution/types` via an `importOriginal` spread so
`isDesktop` / `isNightly` survive the wholesale replacement (only
`isCloud` was re-hoisted before).
- Trimmed the `getAssetStoredFilename` JSDoc; the BE-933/934
future-collapse is now a one-line `TODO` rather than a design-doc
paragraph.
## Review Focus
- The helper is intentionally named `getAssetStoredFilename` to
disambiguate from the existing `getAssetFilename` (which targets
`user_metadata.filename` / `metadata.filename` for serialized-identifier
contexts — missing-model matching, filename schema validation) and
`getAssetDisplayFilename` (UI labels). Folding the `isCloud &&
asset_hash` fallback into either of those would regress
display/identifier sites where the cloud hash is never meant to surface.
- Fixes FE-733
- Parent: FE-601 (L1 umbrella)
- RFC: [Asset Identity
Semantics](https://www.notion.so/comfy-org/RFC-Asset-Identity-Semantics-35a6d73d365080e59d59c98cebae779b)
- Survey: [Asset FE Divergence Survey M1
Scope](https://www.notion.so/comfy-org/Assets-FE-Divergence-Survey-M1-Scope-3616d73d365080d0a9cbf5f2394c12f8)
- Slack thread:
https://comfy-organization.slack.com/archives/C0AUUTS2RQV/p1778815571949519
## Screenshots (if applicable)
N/A — no UI change.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12287-refactor-assets-extract-getAssetStoredFilename-helper-add-mockFeatureFlags-test-util-3616d73d365081c9a1c6e1982728a38a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Matt Miller <matt@miller-media.com>
## Summary
Fix issue where variables typed in the `filename_prefix` of the save
nodes were not interpreted. See issue raised by users here:
https://github.com/Comfy-Org/ComfyUI/pull/13850#issuecomment-4700771342
## Changes
- **What**: Added the following nodes to
`src/extensions/core/saveImageExtraOutput.ts`
- `SaveImageAdvanced`
- `SaveSVGNode`
- `SaveAudioMP3`
- `SaveAudioOpus`
- `SaveAudioAdvanced`
## Summary
Fixes FE-874 by preventing terminal jobs that do not have an
`execution_end_time` from being grouped under the `Undated` section in
the expanded job history / queue UI.
The production change is intentionally small: when grouping completed or
failed jobs by date, the UI now falls back from `executionEndTimestamp`
to `createTime`.
```ts
ts = task.executionEndTimestamp ?? task.createTime
```
This keeps the existing preference for execution completion time when it
is available, while still giving pre-execution terminal jobs a
meaningful date bucket based on when the job was created.
## Root Cause
Some terminal jobs, especially failures that happen before execution
actually starts, can legitimately arrive without `execution_end_time`.
This matches the backend semantics: if there is no execution start,
there may be no execution end timestamp either.
Before this change, the frontend grouping logic treated missing
`executionEndTimestamp` as if there were no usable date at all for
terminal jobs. That caused failed jobs with a valid `create_time` to
appear in the `Undated` group.
At the same time, the list sorting logic already used `createTime`, so
those jobs could appear near the top of the list while still being
labeled as `Undated`. That mismatch made recent failed jobs look like
they had no date, even though the job creation timestamp was present.
## What Changed
- Updated `useJobList` date grouping for terminal jobs:
- `completed` and `failed` jobs still use `executionEndTimestamp` when
available.
- If `executionEndTimestamp` is missing, they now fall back to
`createTime`.
- Added regression coverage for terminal jobs without an execution end
timestamp:
- A failed job without `executionEndTimestamp` is grouped by
`createTime`.
- A completed job without `executionEndTimestamp` is also covered
because the production fallback applies to both terminal states in the
same code path.
- Cleaned up the `useJobList` test harness by replacing the mocked
`vue-i18n` module with a real `createI18n` instance per mount.
- This follows the repo testing guidance to avoid mocking `vue-i18n`.
- Each composable mount now receives a fresh i18n instance, avoiding
shared mutable i18n state between tests.
## User Impact
Failed jobs that never reached execution will no longer show up under
`Undated` when they still have a valid creation timestamp. They will
instead appear under the correct date group, such as `Today`,
`Yesterday`, or a localized month/day label.
This should make the expanded job history easier to scan and avoid the
confusing case where recent failed runs appear at the top while also
being labeled as undated.
## E2E Regression Coverage Rationale
I did not add a Playwright regression test under `browser_tests/` for
this fix because the regression is isolated to the timestamp selection
used by `useJobList` when it builds date groups. There is no changed
user interaction, navigation flow, API request shape, route handling, or
browser-only behavior.
The existing browser coverage already verifies that the Job History
sidebar opens, renders active and terminal jobs, and filters
completed/failed jobs using the mocked jobs route fixture. Adding an E2E
for this specific case would require creating another mocked `/api/jobs`
response with a terminal job that has `create_time` but no
`execution_end_time`, opening the sidebar, and asserting the rendered
date header. That would mostly duplicate the composable-level assertion
through the DOM while adding extra moving parts around relative date
labels, locale/timezone formatting, and the virtualized job list.
The regression is therefore covered more directly and deterministically
at the unit level in `src/composables/queue/useJobList.test.ts`. The new
test drives the same grouping pipeline that the UI consumes and asserts
that terminal jobs without `executionEndTimestamp` are grouped by
`createTime` instead of falling into `Undated`. I also verified the test
fails against the pre-fix implementation with `['Undated']` and passes
with the fallback.
## Notes
This PR does not attempt to synthesize an execution end time. The
backend can validly omit `execution_end_time` for jobs that never
started execution. The frontend fix is limited to display grouping: if
there is no execution end timestamp, use the already-present creation
timestamp as the grouping date.
If product requirements later need the exact terminal failure timestamp
for pre-execution failures, that would require a separate backend/API
timestamp such as a terminal-state or update timestamp. This PR only
fixes the current display fallback.
## Validation
Local validation run before publishing:
```bash
pnpm test:unit src/composables/queue/useJobList.test.ts
pnpm exec eslint src/composables/queue/useJobList.ts src/composables/queue/useJobList.test.ts
pnpm exec oxlint src/composables/queue/useJobList.ts src/composables/queue/useJobList.test.ts --type-aware
git diff --check -- src/composables/queue/useJobList.ts src/composables/queue/useJobList.test.ts
```
The commit hook also ran successfully during the final amend and passed:
```bash
pnpm exec oxfmt --write ...
pnpm exec oxlint --type-aware --fix ...
pnpm exec eslint --cache --fix ...
pnpm typecheck
```
## Screenshots
Before
<img width="346" height="624" alt="Screenshot 2026-06-17 1:35:06 AM"
src="https://github.com/user-attachments/assets/02269f57-038a-4f06-9892-0758ad84d2c7"
/>
After
<img width="352" height="632" alt="Screenshot 2026-06-17 1:35:37 AM"
src="https://github.com/user-attachments/assets/251cd762-2c88-4af6-8218-4af1915727b6"
/>
## Summary
Fixes a Nodes 2.0 node replacement regression where widgets that only
exist on the replacement node were not registered with the widget value
store, causing their Vue-rendered controls to fall back to component
defaults such as `0` instead of the replacement node's real widget
default.
The root cause is that `replaceWithMapping()` replaces the placeholder
node in-place by writing directly to `graph._nodes` and
`graph._nodes_by_id`. That path intentionally preserves the old node id,
but it also bypasses the normal `LGraph.add()` flow that binds widgets
to their owning node id. As a result, newly introduced bindable widgets
on the replacement node could exist on the LiteGraph node object while
remaining absent from `useWidgetValueStore`, which is the state Vue
Nodes reads from when rendering widget controls.
## Changes
- **What**: Bind every bindable widget on the replacement node to the
reused node id inside `replaceWithMapping()` after the replacement node
is inserted into the graph maps and before widget values are
transferred.
- **What**: Preserve the existing widget value transfer behavior for
mapped widgets. Because widgets are now bound before `newWidget.value =
oldValue` runs, transferred values are written through the normal widget
store state instead of only mutating the unbound widget object.
- **What**: Add a focused unit regression check that verifies
replacement-only widgets are bound with the reused node id during node
replacement.
- **What**: Extend the existing node replacement Playwright coverage to
assert the Vue Nodes rendered input for `KSampler.denoise` keeps the
expected replacement value after the replacement flow.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
Please focus on the placement of the widget binding in
`replaceWithMapping()`. The binding happens after the new node has been
assigned the reused id and inserted into the graph's node maps, but
before mapped widget values are copied over from the old node. This
mirrors the important part of the normal graph add flow for widgets
while keeping the in-place replacement behavior intact.
The tests intentionally avoid asserting replacement-node fixture
defaults in isolation. The unit test verifies the actual new side effect
that prevents the regression: `setNodeId()` is called for a bindable
widget that was not present on the old node. The Playwright assertion
then covers the user-visible Nodes 2.0 symptom: the replacement widget
is rendered from the widget store instead of falling back to the Vue
numeric default.
Linear: FE-1070
## Validation
- `pnpm vitest run
src/platform/nodeReplacement/useNodeReplacement.test.ts`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5174 pnpm
exec playwright test --project=chromium
browser_tests/tests/nodeReplacement.spec.ts -g "Widget values are
preserved after replacement"`
- `pnpm lint`
- `pnpm typecheck`
- Commit hook also reran staged formatting/linting and `pnpm typecheck`
during the final amend.
## Screenshots (if applicable)
Before
https://github.com/user-attachments/assets/dc4e8137-d8aa-4a70-9973-5559ed84b90e
After
https://github.com/user-attachments/assets/4c70b9e4-d971-4e94-8d2f-12b0f2b00a09
## Summary
Fix Ctrl+C copy for large subgraphs by encoding clipboard metadata in
bounded byte chunks instead of spreading the full serialized payload
into a single `String.fromCharCode(...)` call.
## Root Cause
<img width="648" height="33" alt="스크린샷 2026-06-15 오후 4 46 52"
src="https://github.com/user-attachments/assets/09aec159-fd10-4979-bfb2-51aec9b51a63"
/>
Ctrl+C uses the native `copy` event path in `useCopy.ts` so ComfyUI can
write serialized node metadata into the system clipboard as `text/html`.
That metadata supports the cross-app / cross-window copy-paste path.
For Unicode safety, the current code first converts the serialized node
JSON to UTF-8 bytes with `TextEncoder`, then converts those bytes into a
binary string for `btoa`. The bug was in this conversion step:
```ts
String.fromCharCode(...Array.from(new TextEncoder().encode(serializedData)))
```
When a selected subgraph is large enough, the UTF-8 byte array becomes
too large to spread as function arguments. The browser throws
`RangeError: Maximum call stack size exceeded` before clipboard metadata
is written, so Ctrl+C appears to fail for large subgraphs.
The right-click / menu copy path was not affected in the same way
because it uses LiteGraph's internal `copyToClipboard()` path directly
and does not go through this system clipboard metadata encoding step.
## Changes
- **What**: Convert UTF-8 bytes to a binary string in `0x8000` byte
chunks before passing the result to `btoa`.
- **Why**: This preserves the existing UTF-8 safe cross-app clipboard
metadata format while avoiding the JavaScript argument-count limit that
caused the stack overflow.
- **Fallback**: Wrap system clipboard metadata encoding/writing in
`try/catch` so the internal `canvas.copyToClipboard()` result is still
produced even if the metadata bridge fails unexpectedly.
- **Dependencies**: None
## Review Focus
- Chunking is only used while building the binary string for base64
encoding. The clipboard payload format remains unchanged.
- Multi-byte UTF-8 data remains safe because chunking happens at the
byte-string construction layer; paste still reassembles the full byte
stream before `TextDecoder` decodes it.
- The unit test exercises the actual `useCopy` copy handler with a large
serialized payload, Unicode metadata, and a partial final chunk.
## Test Plan
- `vitest run src/composables/useCopy.test.ts`
- pre-commit hook: `oxfmt`, `oxlint`, `eslint`, `typecheck`
- pre-push hook: `pnpm knip`
No E2E was added because this regression is isolated to deterministic
clipboard metadata encoding in `useCopy`. The unit test exercises the
actual `copy` event handler with a large serialized payload and Unicode
metadata, avoiding a large workflow fixture and slower browser coverage
for behavior that does not require canvas rendering or end-to-end UI
orchestration.
Linear:
[FE-858](https://linear.app/comfyorg/issue/FE-858/bug-ctrlc-copy-keyboard-shortcut-does-not-work-on-large-subgraphs)
## Summary
Redesigns the Errors tab cards to match the new Figma error-panel spec
(file `Czv0JcCfcUiizeEZURevpq`): every error group is now wrapped in a
single bordered card led by an error-count summary hero, with each
category rendered as a collapsible section whose count lives in a
circular badge rather than a parenthetical title suffix.
> Rebased onto `main` after #12793 was merged. This PR now contains only
the error-card redesign slice.
## Changes
- **What**:
- **New `ErrorCardSection.vue`** — the shared section shell used by
every error type. Renders a 32px header (circular count badge + neutral
title + `actions` slot + collapse chevron) and a `TransitionCollapse`
body. Replaces the per-group `PropertiesAccordionItem`, dropping the old
octagon-alert icon + red title + `(n)` suffix and the sticky-header
behavior.
- **`TabErrors.vue`** — wraps all groups in one `rounded-lg` card
bordered with `secondary-background`. Adds a summary **hero** (large
severity-colored total count, vertical divider, "N Errors detected /
Resolve before running the workflow"). Moves the per-group action
buttons (Install All / Replace All / missing-model Refresh) into the
section's `actions` slot. Adds `getGroupCount()` / `totalErrorCount` and
switches content background to `interface-panel-surface`. Most of the
line count here is re-indentation from the template restructure, not
behavior change.
- **`missingErrorResolver.ts`** — drops the `formatCountTitle` helper so
display titles are `"Missing Models"` instead of `"Missing Models (4)"`;
the badge now carries the count. Toast titles/messages are untouched.
- **`ErrorNodeCard.vue`** — restyles the runtime/validation error-log
box to the Figma spec: borderless `base-foreground/5` surface, `ERROR
LOG` header, 12px non-mono body at 50% opacity, inset footer divider
with Get Help / Find on GitHub links.
- **Row components** (`MissingModelRow`, `MissingPackGroupRow`,
`SwapNodeGroupRow`, `MissingMediaCard`, `MissingNodeCard`,
`MissingModelCard`) — align spacing, fonts, badges, and button sizes
with Figma: 12px row labels, `size="sm"` (24px) action buttons, 16px
count badges (`rounded-sm`, `secondary-background-hover`, 9px), 32px
reference-row heights, `px-3` card padding. Model-name wrapping is kept
independent of its count badge and link button so they never reflow into
the metadata sub-label.
- **i18n** — adds `errorsDetected` (pluralized), `resolveBeforeRun`,
`expand`, `collapse` to `en/main.json`.
- **Breaking**: None. No store, composable, action, or data-flow changes
— all handlers and emitted events are preserved. The only user-visible
copy change is the removal of the `(n)` count suffix from section
titles.
## Review Focus
- **Title copy change**: `"Missing Models (4)"` → `"Missing Models"`.
Search-filter matching against the old `(n)` string no longer applies,
but the count is shown by the badge and the hero total.
- **Sticky header removed**: section headers no longer pin to the top on
scroll (intentional per the new design).
- **Collapse click target**: the old single-button header (which nested
action buttons inside a `<button>` — invalid HTML) is split into a
separate title button and chevron button. Behavior is unchanged and
accessibility improves; the empty space beside an action button no
longer toggles collapse.
- All semantic colors map to existing design-system tokens (no `dark:`
variants, no hardcoded hex). Verified the artifact hex values match the
tokens (e.g. `#262729` = `secondary-background`, `#e04e48` =
`destructive-background-hover`, `#171718` = `interface-panel-surface`).
## Follow-up
This PR intentionally keeps the error-count ownership cleanup out of the
current diff so the card redesign remains reviewable. A follow-up PR
will centralize error counting around a single source of truth so the
Errors tab summary hero, section badges, and any overlay surfaces cannot
drift from one another.
That follow-up will also address the current count mismatch in the
ErrorOverlay and continue the ErrorOverlay redesign there, instead of
expanding this PR after review.
## Screenshots (if applicable)
After
<img width="603" height="703" alt="스크린샷 2026-06-13 오후 1 00 02"
src="https://github.com/user-attachments/assets/065d7c19-9748-4e99-9b43-675a31e92949"
/>
<img width="601" height="197" alt="스크린샷 2026-06-13 오후 1 01 07"
src="https://github.com/user-attachments/assets/0fa1fbda-9091-4a45-9eca-e99c43089c0e"
/>
<img width="617" height="612" alt="스크린샷 2026-06-13 오후 1 02 43"
src="https://github.com/user-attachments/assets/3d67a057-bf65-4e51-bcf5-70ecce851826"
/>
<img width="495" height="723" alt="스크린샷 2026-06-13 오후 1 03 28"
src="https://github.com/user-attachments/assets/6dcc4021-0fc3-4955-a68b-c0533c66a3cf"
/>
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
The redesigned Settings dialog (Figma DES `3253-16079`) is **1280px**
wide, but it rendered at **960px**.
Root cause — the width was capped at 960 in **two** layers:
1. `useSettingsDialog.ts` → `SETTINGS_CONTENT_CLASS` (`max-w-[960px]`)
sizes the Reka dialog shell.
2. `SettingDialog.vue` → `<BaseModalLayout size="sm">` (`SIZE_CLASSES.sm
= max-w-[960px]`) sizes the modal content.
Widening only the shell leaves the inner `BaseModalLayout` at 960 (empty
space on the right). This sets both to **1280px** and lets
`BaseModalLayout` fill the shell (`size="full"`).
The dialog size is **not** a workspace-specific concern, so it applies
to all Settings (OSS + cloud) — no feature-flag gate.
Found during FE-768 designer QA.
## Verification
- Live: dialog measures 1280px, content area 1006px (was 960 / 688).
- `useSettingsDialog.test.ts`: `contentClass` is 1280px (`size:
'full'`).
- `pnpm typecheck` / `lint` / `format` / unit tests green.
## Test Plan
- [x] Settings dialog renders at 1280px with the content filling the
dialog
- [x] Unit test asserts the 1280px sizing
## Screenshots
Settings ▸ Plan & Credits at **1280px** (content fills the dialog; was
960px shell / 688px content area):
**Personal — Pro:**
<img width="720" alt="Settings dialog at 1280px — personal Pro"
src="https://github.com/user-attachments/assets/adc2fd9f-d249-469f-b947-1ec8f674cbb0"
/>
**Team:**
<img width="720" alt="Settings dialog at 1280px — team"
src="https://github.com/user-attachments/assets/e7378067-11a2-411b-b37b-98c8aecb82b1"
/>
---------
Co-authored-by: github-actions <github-actions@github.com>
Node previews are stored by `locatorId`, but sent from the server by
`executionId`. Normally, this difference is reconciled when the event is
received, but this step is skipped when the workflow is backgrounded.
Upon reloading the workflow, these backlogged `executionId`s were
incorrectly mapped directly onto node outputs. Any outputs located
inside a subgraph would then fail to display because `executionId`s are
now `locatorId`s.
This is solved by resolving any `executionId`s at time of output
restoration. Because `executionId`s can only leak into the outputs of
backgrounded workflows, it is safe for resolved `executionId`s to
overwrite any pre-existing `locatorId`s.
It might wind up cleaner to instead properly enforce that the
nodeOutputs cached by change tracker resolve a `locatorId` at time of
receipt. This would follow naturally for properly branded id types, but
would then require resolving `locatorId` from suspended workflows which
is a good bit more involved.
en:'Purchase additional credits at any time. Unused top-ups roll over to the next month automatically for up to 1 year.',
en:'Purchase additional credits at any time. Top-up credits are valid for 1 year from the date of purchase and do not roll over with your monthly plan.',
'zh-CN':
'可随时购买额外积分。未使用的充值积分自动结转至下月,最长保留 1 年。'
'可随时购买额外积分。充值积分自购买之日起 1 年内有效,且不会随月度计划结转。'
},
'pricing.included.feature6.title':{
en:'Pre-installed models',
@@ -1433,10 +1445,19 @@ const translations = {
'Creator 或 Pro 计划用户可从 CivitAI 或 Huggingface 导入自己的模型和 LoRA,打造专属风格。'
},
'pricing.included.feature11.title':{
en:'Run Workflows via API',
'zh-CN':'通过 API 运行工作流'
},
'pricing.included.feature11.description':{
en:'Run Comfy workflows programmatically via API, with concurrency limits based on your plan. Perfect for integrating ComfyUI into your applications, automating batch processing, or building production pipelines. For higher rate limits, reach out to <a href="mailto:enterprise@comfy.org" class="text-primary-comfy-yellow underline">enterprise@comfy.org</a>.',
│ ├── components/ - Page object classes (locators, user interactions)
│ │ ├── Actionbar.ts
│ │ ├── ContextMenu.ts
│ │ ├── ManageGroupNode.ts
│ │ ├── SettingDialog.ts
│ │ ├── SidebarTab.ts
│ │ ├── Templates.ts
@@ -44,7 +43,7 @@ browser_tests/
### Architectural Separation
- **`fixtures/data/`** — Static test data only. Mock API responses, workflow JSONs, node definitions. No code, no imports from Playwright.
- **`fixtures/components/`** — Page object components. Classes that own locators for a specific UI region (e.g. `Actionbar`, `ContextMenu`, `ManageGroupNode`).
- **`fixtures/components/`** — Page object components. Classes that own locators for a specific UI region (e.g. `Actionbar`, `ContextMenu`, `SettingDialog`).
- **`fixtures/helpers/`** — Helper classes that coordinate actions across multiple regions without owning a locator surface of their own (e.g. `CanvasHelper`, `WorkflowHelper`, `NodeOperationsHelper`).
- **`fixtures/utils/`** — Standalone utility functions. Exported functions (not classes) used by tests or fixtures (e.g. `fitToView`, `clipboardSpy`, `builderTestUtils`).
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.