mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-08 16:17:58 +00:00
84f08712776aab113163290bae3df87519e4bf0a
6036 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84f0871277 |
refactor(LivePreview): use useImage and cache last good dimensions
Replace manual @load/@error handlers and imageError ref with useImage from @vueuse/core. Source is wrapped in computed() so useImage re-runs when imageUrl changes. Cache the last successfully loaded naturalWidth/Height in refs that update only when isReady fires. This avoids the placeholder text flickering back to 'Calculating dimensions' each time imageUrl changes during live preview streaming. Test mocks useImage at module level and drives state/isReady/error manually while preserving @testing-library/vue assertions. |
||
|
|
6f7ec5ba34 |
refactor(PackBanner): use useImage and split src ternary into v-if/v-else
Replace isImageError ref + @error handler on <img> with useImage from @vueuse/core. Source is wrapped in computed() so useImage re-runs when nodePack changes. Template now branches with v-if/v-else on isImageError instead of a ternary on :src + :class for clearer reading order. |
||
|
|
62222b7c04 |
refactor(UserAvatar): use useImage with reactive computed source
Replace manual imageError ref + Avatar @error handler with useImage from @vueuse/core. Wrap the source in computed() so useImage re-runs when photoUrl/ariaLabel props change. Test mocks useImage at module level to control the error ref while preserving the @testing-library/vue behavioral test style. |
||
|
|
2e4c9c6fdc |
style: use neutral credits info button (#13461)
## Summary Replaces the Additional credits tooltip trigger with the shared Button component so the icon renders with the neutral muted treatment used by the rest of the UI. ## Changes - **What**: Uses the shared Button component for the Additional credits info action while preserving the existing tooltip and aria label. - **Dependencies**: None. ## Review Focus Confirm this remains a visual-only change scoped to the Plan & Credits credits tile. ## Screenshots (if applicable) Before <img width="397" height="368" alt="스크린샷 2026-07-06 오후 11 30 38" src="https://github.com/user-attachments/assets/9cda1aee-4dc2-4ce1-b001-29d0e09fc07d" /> After <img width="374" height="355" alt="스크린샷 2026-07-06 오후 11 31 00" src="https://github.com/user-attachments/assets/64d0b726-6031-42d4-84d7-35405150698c" /> ## Testing - `pnpm format` - `pnpm lint` - pre-commit hook: stylelint, oxfmt, oxlint, eslint, typecheck - `pnpm test:unit src/platform/cloud/subscription/components/CreditsTile.test.ts` |
||
|
|
b9a0ac0fed |
fix(manager): drop embed=true so the Cloud survey renders its dark theme (#13438)
Remove `embed=true`, so PostHog applies the survey's own dark appearance and it renders correctly. Verified live on testcloud that removing the flag fixes the styling; `distinct_id` user linkage is unaffected. |
||
|
|
b61e54db3b |
Allow forcing icon display as mask or image (#13414)
- Adds support for forcing an icon to display as a mask or image with `icon-mask` and `icon-image`. - Updated the logic so that svg of a solid color (like the claude logo) display as an image by default - Update many svg to consistently use `currentColor` so that they still function as masks by default |
||
|
|
d85ce2bf9e |
feat: add custom nodes waitlist survey to Manager button on Cloud (#13135)
## Summary On Comfy Cloud, show the Manager button and open a hosted survey in the manager modal (in place of the local node manager) so we can gauge demand for custom nodes on Cloud. ## Changes - **What**: `TopMenuSection` shows the Manager button when `isCloud`; clicking it opens `ManagerSurveyDialog`, which embeds a PostHog hosted survey via iframe. The survey URL comes per-environment from cloud config (`manager_survey_url`), with the logged-in user's `distinct_id` appended so responses link to the user. Includes loading/error states and PostHog's `posthog:survey:height` iframe auto-resize. - **Dependencies**: none ## Review Focus - Survey URL is sourced from `remoteConfig.manager_survey_url` (must be set per environment in cloud config); falls back to an error state when unset or malformed. - iframe embedding requires the PostHog survey to be `external_survey` type with embedding enabled. |
||
|
|
df5f5b3367 |
feat: identify auth users to Syft (#13311)
## Summary
Identifies Cloud auth users to Syft via the required `identify(email, {
source })` handoff so Syft enrichment reliably attaches to signup/login
users instead of relying only on GTM page-load capture.
## Changes
- **What**: Adds a cloud-only Syft telemetry provider that reads
`syftdata_source_id` from `remoteConfig`, lazy-loads the Syft SDK
(reusing an already-loaded GTM Syft client when present), and calls
`identify` with `source: 'signup'` or `source: 'login'` on auth and on
session restore. `trackUserLoggedIn()` dedupes against the email already
handled by `trackAuth()` so a fresh login is not identified twice.
- **Dependencies**: None.
## Review Focus
- Preserves the FE-945 startup-blocker fix: the constructor reads only
the `remoteConfig` ref (a plain reactive ref, not Pinia) and never
touches current-user state; the user-email lookup happens only in
`trackUserLoggedIn()`, after app/auth setup. The source id is present at
construction because `main.ts` awaits the anonymous
`refreshRemoteConfig` before `initTelemetry`, and a later authenticated
refresh is picked up reactively on the next `ensureSyftClient()` call.
- SDK loader is idempotent with the current GTM Syft tag during rollout
(one script per `SYFT_SRC`). On load failure it clears its own stub —
guarded by an identity check so it never evicts a real client another
loader installed — letting a subsequent call retry. Long-term cleanup is
to keep one loader path per surface.
- Acceptance should include staging Network verification for
`https://e2.sy-d.io/events` payloads containing an `identify` event for
Google, GitHub, and email auth.
Linear: GTM-168
|
||
|
|
9d5719871a |
Compact vue nodes (#12886)
Updates vue nodes to be compact. This PR does modify the sizing of the asset dropdown (as used on nodes like "Load Image"). There are outstanding concerns about the visibility of the upload button and ongoing work to address this. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/5c866d6f-d83e-40e1-9d87-17b990d94e04"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/2a809e90-13aa-4f95-8b73-3f20b02fd9a1" />| Subsumes #12678 --------- Co-authored-by: Alex <alex@Alexs-MacBook-Pro.local> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
7610a61250 |
test: cover queue display formatting (#13089)
## Summary
Add direct tests for queue job display formatting.
Base: `main`
## Changes
- Covers state icons, pending/initializing labels, running progress,
completed local/cloud output, fallback completed titles, and failed
display.
## Test Results
| | before | after |
| -- | -- | -- |
| `pnpm test:unit src/utils/queueDisplay.test.ts --run` | no direct
queue display test file | ✅ 13 passed |
## Coverage
Superseded by #13332. Historical pre-#13313 branch coverage:
`src/utils/queueDisplay.ts` 22.72% -> 79.54% (+56.82%); overall branches
52.95% -> 53.03% (+0.08%).
Codecov project coverage is intentionally omitted here because it is not
the branch-ratchet metric.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Test-only change; no runtime or production code modified.
>
> **Overview**
> Adds **`src/utils/queueDisplay.test.ts`**, a Vitest suite that
exercises **`iconForJobState`** and **`buildJobDisplay`** from
`queueDisplay.ts` without touching UI or production logic.
>
> Tests use small **`createJob` / `createTask` / `createCtx`** helpers
with a stub **`t`** and clock formatter so expectations assert i18n keys
and formatted values. Coverage includes pending “added to queue” hint,
queued/initializing labels, active vs inactive running progress,
completed local preview vs cloud duration, completed title fallback, and
failed rows with **`showClear`** behavior.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
|
||
|
|
47c8b09ebf |
test: 2/x cover fuse search ranking (#13087)
## Summary
Add direct tests for `fuseUtil` search ranking and filter behavior.
## Changes
- Covers ranking tiers, deprecated penalties, post-processing, empty
queries, auxiliary score comparison, and filter wildcard/comma matching.
## Test Results
- `pnpm test:unit src/utils/fuseUtil.test.ts`: 7 passed.
- `pnpm typecheck`: passed.
- `pnpm test:coverage`: 876 test files passed; 11,759 passed / 8
skipped.
## Coverage
Superseded by #13332. Historical pre-#13313 branch coverage:
`src/utils/fuseUtil.ts` 81.48% -> 92.59% (+11.11%); overall branches
52.93% -> 52.95% (+0.02%).
Codecov project coverage is intentionally omitted here because it is not
the branch-ratchet metric.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit
|
||
|
|
471236e08d |
feat: track subscription cancellation intent and resubscribe clicks (#13368)
## Summary Instruments the churn funnel: cancellation intent, attempt, abandonment, and request failure, plus resubscribe clicks — all client-observed from existing request/response flows, no watchers or polling added. Covers both billing paths: the mainline (`/customers/*` + Stripe portal) path via the "Manage subscription" click, and the workspace path via its in-app cancel dialog. ## Changes - **What**: - New events: `app:subscription_cancel_flow_opened` / `_confirmed` / `_abandoned` / `_failed` and `app:resubscribe_button_clicked`, via `trackSubscriptionCancellation(stage, metadata)` and `trackResubscribeClicked` (registry, PostHog, host sink) - All cancellation events carry a `source` discriminator: - `manage_subscription_button` — the mainline path. Legacy users can only cancel inside the Stripe billing portal, and in-app UI already covers plan changes, so this click is the closest observable cancel-intent signal for ~all production users. Only `flow_opened` fires here (everything past the click happens in Stripe's UI). Probable, not certain, intent — the portal also serves card updates/invoices. - `cancel_plan_menu` — the workspace in-app dialog (allowlist-gated pilot): `flow_opened` on mount, `confirmed` before the API call (failed attempts still register), `failed` with the error message, `abandoned` on "Keep subscription"/close. Successful cancels close via a different path and never emit `abandoned`. - Metadata carries `current_tier`, billing `cycle`, and (dialog path) the `end_date` shown to the user - Resubscribe clicks tracked at both call sites with `source`: `pricing_dialog` (`useSubscriptionCheckout`, also carrying the dialog's `payment_intent_source` from #13363) and `settings_billing_panel` (`useResubscribe`) - Not instrumented on purpose: the workspace "Manage billing" button and the "Invoice history" footer link (portal opens without cancel connotation) ## Review Focus - Deliberately **no** client-side "cancel succeeded" event: outcome truth is server-side. Mainline already has it (`billing:subscription_deleted` from the Stripe webhook in comfy-api); the workspace path needs a `subscription_cancelled` billing event type (separate cloud-repo change). The legacy `useSubscriptionCancellationWatcher` poller emits an undercounted `app:monthly_subscription_cancelled`; analysis should prefer the server event. - `confirmed` fires before the request; growth can join `flow_opened`/`confirmed` → server-side cancelled events by user + timestamp. |
||
|
|
d6c582c399 |
feat(billing): gate consolidated billing behind consolidated_billing_enabled flag (#13359)
## Summary Shields personal-workspace billing code paths behind the new `consolidated_billing_enabled` feature flag so they fall back to the **legacy** billing flow while the flag is `false`. Team workspaces are unaffected and continue to use the workspace-scoped billing flow. ## Changes - Add `consolidatedBillingEnabled` to `useFeatureFlags` (reads the `consolidated_billing_enabled` server flag / remote config, defaults to `false`) and to the `RemoteConfig` type. - New `useBillingRouting` composable — a single source of truth for whether the active workspace uses the workspace vs. legacy billing flow: - team workspaces disabled → legacy - personal workspace + consolidated billing off/missing → legacy - personal workspace + consolidated billing on → workspace - team workspace → workspace - workspace not loaded yet → legacy - Route `useBillingContext` and the affected UI sites (`SubscriptionPanel`, `useSubscriptionDialog`, `UsageLogsTable`, `TopUpCreditsDialogContentLegacy`) through `useBillingRouting` instead of keying on `teamWorkspacesEnabled` directly. - Update the storybook `useFeatureFlags` mock to stay in sync. ## Testing - `pnpm test:unit` for `useBillingRouting`, `useBillingContext`, `useSubscriptionDialog`, and `UsageLogsTable` (new + updated coverage for the routing matrix). Remaining quality gates (`typecheck`, `lint`) are being verified in CI. ## Related Requires the backend PR that adds the `consolidated_billing_enabled` flag to `/api/features`. --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
2ec2a0e091 |
feat: attribute payment intent through paywall, checkout, and top-up telemetry (#13363)
## Summary Answers "why did this user want to pay?" by capturing the triggering product moment at every paywall/upsell entry point and carrying it through checkout and success telemetry. ## Changes - **What**: - Widen `SubscriptionDialogReason` from 4 coarse values to 13 grounded intent sources (`subscribe_to_run`, `upgrade_to_add_credits`, `invite_member_upsell`, `settings_billing_panel`, etc.) - Fire `app:subscription_required_modal_opened` from `useSubscriptionDialog` (the choke point all dialog variants pass through) — the workspace/unified path previously emitted nothing; remove the now-duplicate emitters in `useSubscription` and `usePricingTableUrlLoader` - Add `payment_intent_source` to `BeginCheckoutMetadata`/`SubscriptionSuccessMetadata`, threaded via the existing `reason` prop: dialog → `PricingTable` → `performSubscriptionCheckout` → pending-attempt record, so legacy `app:monthly_subscription_succeeded` carries intent alongside `checkout_attempt_id` - Fire `begin_checkout` on the workspace checkout path (`useSubscriptionCheckout`, personal + team confirm) and the team deep-link util — both previously emitted nothing; `tier` widened to `TierKey | 'team'` - Implement `trackBeginCheckout` in `PostHogTelemetryProvider` (was GTM/host-only, so `begin_checkout` never reached PostHog) - Thread `showSubscriptionDialog(options)` through the billing-context adapters and pass a reason at ~14 call sites; add `source` to `app:add_api_credit_button_clicked` ## Review Focus - `modal_opened` now fires once per dialog actually shown, so a free-tier user clicking Upgrade emits two events (free-tier dialog, then pricing table) where the legacy path emitted one - Intent is threaded explicitly via props/params rather than shared state; `useSubscriptionCheckout` gained an optional second parameter |
||
|
|
9e5fb67b76 |
Show app mode run validation warning (#12557)
## Summary Adds an app mode validation warning so users can see when a workflow has errors before running and jump directly back to graph mode to review them. ## Changes - **What**: Adds a reusable app mode warning banner above the Run button when the execution error store reports workflow errors, including validation and missing asset states. - **What**: Reuses the existing graph-error navigation flow so the warning action switches out of app mode and opens the Errors panel in graph mode. - **What**: Updates the app mode Run button icon and accessible label in the warning state while keeping the Run action non-blocking. - **What**: Adds unit coverage for the warning render/accessibility state and an E2E flow that triggers a validation failure, dismisses the overlay, and opens graph errors from the app mode warning. - **Breaking**: None. - **Dependencies**: None. ## Review Focus The warning intentionally mirrors graph mode behavior: it surfaces the error state but does not prevent the user from clicking Run. This avoids turning display-level validation signals into hard execution blockers. The warning is driven by the existing `hasAnyError` aggregate, so missing nodes, missing models, and missing media are included alongside prompt/node/execution errors. ## Tests - `pnpm format` - `pnpm lint` - `pnpm typecheck` - `pnpm test:unit` - `pnpm knip` - `pnpm test:browser:local browser_tests/tests/appModeValidationWarning.spec.ts` ## Screenshots <img width="461" height="994" alt="스크린샷 2026-06-25 오후 7 00 55" src="https://github.com/user-attachments/assets/f8fc20bf-d572-46b5-9fa4-312e7c4c8076" /> |
||
|
|
be9de941c9 |
refactor: brand link slot and reroute ids (#13296)
## Summary Brand link, reroute, and slot identifiers through LiteGraph, subgraph, and layout flows so raw numeric workflow data is converted at boundaries while runtime APIs keep branded IDs. ## Changes - **What**: Add canonical `LinkId`, `RerouteId`, and `SlotId` types plus minting helpers, then re-export litegraph/layout ID types from those modules. - **What**: Keep `LinkId`, `RerouteId`, and `SlotId` references branded across graph links, reroutes, node slots, subgraph slots, link deduplication, link drop handling, layout storage, and tests. - **What**: Convert raw numeric IDs only at periphery points: serialized workflow DTOs, legacy graph link proxy access, copied/pasted graph data, Yjs/string layout keys, and test fixtures. - **What**: Move slot layout identity onto branded `SlotId` values using stable `node:direction:index` ordering, while keeping DOM dataset values stringified at the boundary. - **What**: Avoid slot-key scans during link drops by carrying the link segment identity directly through the drop path. ## Review Focus - Branded IDs should not be widened back to `LinkId | number` / `RerouteId | number` in runtime APIs. - Serialized workflow shapes intentionally remain numeric for compatibility. - `_subgraphSlot.linkIds` remains `LinkId[]`; call sites should not treat it as raw `number[]`. - `MapProxyHandler` is the compatibility boundary for deprecated indexed `graph.links[id]` access. ## Validation - `pnpm typecheck` - `pnpm test:unit src/lib/litegraph/src/LLink.test.ts src/lib/litegraph/src/LGraph.test.ts src/lib/litegraph/src/LGraphNode.test.ts src/lib/litegraph/src/canvas/LinkConnector.core.test.ts src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts src/lib/litegraph/src/LGraphCanvas.drawConnections.test.ts src/lib/litegraph/src/node/slotUtils.test.ts src/lib/litegraph/src/subgraph/ExecutableNodeDTO.test.ts src/core/graph/subgraph/promotionUtils.test.ts src/core/graph/subgraph/migration/proxyWidgetMigration.test.ts src/renderer/core/layout/store/layoutStore.test.ts src/renderer/core/layout/utils/layoutUtils.test.ts src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts src/scripts/promotedWidgetControl.test.ts` - Commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck` - Push hook: `knip --cache` --------- Co-authored-by: AustinMroz <austin@comfy.org> |
||
|
|
f4e0430072 |
fix: disable global keybindings while a modal dialog is open (#12184)
## Summary
Block background keybindings from firing while a modal dialog (e.g.
Templates) is open, so typing `w` no longer toggles the workflow sidebar
behind the modal.
## Changes
- **What**: In `keybindingService.keybindHandler`, gate command
execution on `dialogStore.dialogStack`. When a dialog is open, only
keybindings whose event target is inside the dialog (`[role="dialog"]`)
fire; all other matches are dropped.
## Review Focus
- The dialog scope check uses `target.closest('[role="dialog"]')` so
dialog-internal shortcuts still work — confirm PrimeVue/Reka dialogs
render with `role="dialog"` on the wrapper (they do; this is the
WAI-ARIA standard the libraries follow).
- Updated `keybindingService.escape.test.ts` "modifiers regardless of
dialog state" case to the new contract (modifiers also blocked),
matching the team consensus in FE-642 that all keybindings should be
disabled when a modal is open.
- New `keybindingService.dialog.test.ts` covers: no-dialog → fires;
dialog open + target outside → blocked; dialog open + target inside →
fires.
Fixes FE-642
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12184-fix-disable-global-keybindings-while-a-modal-dialog-is-open-35e6d73d3650812fbc5dd5490ccde24f)
by [Unito](https://www.unito.io)
Co-authored-by: Dante <bunggl@naver.com>
|
||
|
|
c78592c1ec |
feat: add upload button to dropdown menu filter bar (#12507)
## Summary Add an Upload button to the dropdown popover's filter bar so users can pick a file without closing the dropdown to reach the small upload icon next to the input. The upload button in the dropdown menu includes text and uses the same icon as the external quick upload button. This design ensures that after using it, users will understand that the icon on the external button means upload. Even if users didn't understand it before, they will correctly interpret it next time. related linear FE-581 ## Changes - **What**: - Expose `showPicker()` from `FormDropdownInput`; it calls `HTMLInputElement.showPicker()` on the single existing hidden `<input type="file">` (falls back to `input.click()` on browsers without showPicker). - Add an Upload button in `FormDropdownMenuFilter` that emits `show-picker`, bubbled up through `FormDropdownMenu` to `FormDropdown`, which then calls `triggerRef.showPicker()`. The whole chain runs in the click event's synchronous stack to satisfy the browser's transient activation requirement, so no extra `<input type="file">` is added to the DOM. - Style the button with the project's standard inverted-button tokens (`bg-base-foreground` / `text-base-background`) so it tracks theme changes. ## Review Focus - The `triggerRef!.showPicker()` non-null assertion in `FormDropdown.vue` is intentional: by the time `show-picker` is emitted the trigger is guaranteed to be mounted; a null here would indicate a real bug we want to surface, not swallow. - Verify the new button reuses the same upload path as the inline icon button (single `<input type="file">`, single `handleFileChange`). ## Screenshots <img width="1304" height="1442" alt="CleanShot 2026-06-02 at 14 39 33@2x" src="https://github.com/user-attachments/assets/b2d1cdd8-e28a-467d-8142-afd707264d0e" /> <details><summary>Old Versions</summary> <p> https://github.com/user-attachments/assets/2d64873b-6bec-4eca-aa89-a72dd11aa809 </p> </details> |
||
|
|
00b0c6b434 |
fix: close widget dropdown on outside pointerdown and canvas viewport moves (#12812)
## Summary Model/widget dropdowns stayed open until mouseup, detached from their node when the canvas moved while open, and needed two clicks to dismiss after the inner scrollbar took focus. ## Changes - **What**: - Dismiss the dropdown on `pointerdown` outside the menu/trigger (capture phase) instead of PrimeVue's `click` (mouseup) dismissal. The dropdown now closes the instant a press lands, before a drag or box-select can start, and a focused inner scrollbar no longer swallows the first outside click. - Close the dropdown whenever the canvas viewport moves, by watching the reactive `useTransformState().camera`. This reacts to the canvas abstraction layer rather than guessing input intent, so it covers pan/zoom from any device — mouse drag, trackpad pan, wheel scroll/zoom — where no `pointerdown` ever fires. The popover is teleported to the document body and cannot follow the viewport, so closing is the correct behavior. ## Review Focus - Box-select and node-drag both begin with a `pointerdown` outside the popover, so they are covered by the immediate dismissal path; the camera watch handles pointer-less viewport motion. - `closeOnEscape` and in-menu interactions are unaffected; presses inside the menu or on the trigger are excluded via `composedPath()`. Fixes FE-808 --------- Co-authored-by: Dante <bunggl@naver.com> |
||
|
|
c8ed15da31 |
feat: follow --comfy-api-base for staging and preview backends (#13054)
## Summary Let the running ComfyUI server decide which backend the web UI talks to (and which Firebase project it signs you into), so launching with `--comfy-api-base` just works with the regular bundled frontend. ## Changes - **What**: At startup the frontend reads `/api/features` on every build (not just cloud) and treats the server's `comfy_api_base_url` / `comfy_platform_base_url` as authoritative, falling back to the build-time defaults. When that api base is a staging-tier host (staging, or a `*.testenvs.comfy.org` preview env) and the server hasn't supplied its own Firebase config, the frontend picks the dev Firebase project, derived from the api base. Production is left exactly as it is today. - `main.ts`: load remote config first thing, before Firebase initializes, so every module sees the right values from the first render - `config/comfyApi.ts`: the api/platform getters now read the server's values on all distributions - `config/firebase.ts`: `getFirebaseConfig()` resolves in order: a server-provided config first (cloud), then the dev project for a staging-tier api base, then the build-time default - `platform/remoteConfig/refreshRemoteConfig.ts`: the startup fetch now has a 5s timeout, so a slow or wedged `/features` can never keep the app from mounting; on failure we fall back to the build-time defaults - **Breaking**: None. With no `/features` overrides (production and ordinary self-hosting), behavior is unchanged ## Review Focus - The precedence in `getFirebaseConfig()` (`config/firebase.ts`): server config first, then the staging-tier dev project, then the build-time default. The staging-tier check matches `stagingapi.comfy.org` and any `*.testenvs.comfy.org` host, and falls back to build-time for anything it can't parse. - Running `refreshRemoteConfig()` unconditionally and first in `main.ts`, with the new fetch timeout as the safety net. ## Testing I tested every case by hand, locally, on top of the automated checks. Tested both with `pnpm run build` and `USE_PROD_CONFIG=true pnpm build` and running Comfy from that folder. Pointed a local ComfyUI at each backend with `--comfy-api-base` and signed in with Google each time: - **Production** (default / `https://api.comfy.org`): stays on production and signs into the production Firebase project, identical to today. - **Staging** (`https://stagingapi.comfy.org`): follows it and signs into the dev project. - **Ephemeral preview env** (`https://pr-<n>.testenvs.comfy.org`): the friendly host is accepted as-is, the frontend follows it, lands in the dev project, and Google sign-in completes. The only exception where fronted does not respect the `--comfy-api-base` is when Comfy runs against `prod` and frontend runs with the `pnpm run dev` - due to overridden config(this is expected behavior). Supersedes: https://github.com/Comfy-Org/ComfyUI_frontend/pull/12560 Companion Core PR: https://github.com/Comfy-Org/ComfyUI/pull/14569 ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> |
||
|
|
b132abc64a |
fix: center video asset in the Load Video node preview (#13172)
## Summary Center the Load Video node preview and keep the node from auto-resizing on clip load, so the video letterboxes in place like the Load Image node (FE-1092). ## Changes - **What**: Makes the video stay centered horizontally and vertically - Playwright browser test expectations updated at run https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28267324092 ## Review Focus - Ensure that patterns and idioms were followed ## Screenshots (if applicable) Horizontally centered <img width="902" height="392" alt="image" src="https://github.com/user-attachments/assets/a9fbec56-1613-44b4-a423-9f709a246c63" /> Vertically centered <img width="220" height="1124" alt="image" src="https://github.com/user-attachments/assets/5497f39b-2ea2-4247-a087-a7d89768b4ce" /> Full aspect ratio <img width="433" height="672" alt="image" src="https://github.com/user-attachments/assets/d579fb14-34c6-4963-abc9-034611232d3d" /> Minimum size <img width="217" height="376" alt="image" src="https://github.com/user-attachments/assets/80df0411-3ff1-4050-ac8e-761b7b8a7c40" /> Preview centering is asserted in `browser_tests/tests/vueNodes/videoPreview.spec.ts`. --------- Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
55c52a730a |
Enable cloud PostHog pageviews (#13286)
## Summary This PR enables native PostHog `$pageview` capture for `cloud.comfy.org` by setting cloud PostHog `capture_pageview` to `history_change`. This keeps `autocapture` disabled, preserves the existing custom `app:page_view` event, and lets the PostHog SDK capture the initial pageview plus SPA history navigation pageviews. The goal is to make cross-domain funnel tracking cleaner between `comfy.org` and `cloud.comfy.org`, since `comfy.org` already emits native `$pageview` events. ## Why We want to measure the visitor funnel more accurately across: - `comfy.org` visits - `cloud.comfy.org` visits - signup clicks / signup opened - signup completion - first cloud workflow run - first subscription - first credit purchase Using native `$pageview` on both website and cloud should make PostHog and downstream warehouse/Hex analysis cleaner for trackable users, while leaving custom app pageview telemetry intact for existing consumers. ## Validation - `pnpm test:unit src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts` - `pnpm typecheck` - `pnpm lint:unstaged` - pre-commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck` - pre-push hook: `knip --cache` Note: local validation printed an engine warning because the Codex runtime has Node `v24.14.0` while this repo declares `>=25 <26`; the commands above still passed. |
||
|
|
fbe462143a |
fix: re-export GroupNodeHandler for custom node compat (#13299)
Fixes #13175 #12931 slimmed groupNode.ts down to migration-only and dropped the export on GroupNodeHandler. ComfyUI-Manager still imports it (import { GroupNodeConfig, GroupNodeHandler } from "../../extensions/core/groupNode.js" in components-manager.js), so the legacy shim no longer providing that export throws "does not provide an export named 'GroupNodeHandler'" at module load. That kills the whole Manager extension before setup() runs — which is why the Manager button vanished from the toolbar since 1.47.3 (backend loads fine, frontend JS dies). Just re-adds the export (class is still there, only the keyword was lost) plus the existing @knipIgnoreUnusedButUsedByCustomNodes tag since nothing in src imports it. Tested by loading with ComfyUI-Manager installed: the groupNode.js import error is gone and the Manager button shows again. typecheck/knip/lint pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9dcab4ee96 |
Essentials Cleanup (#13183)
Address several followup comments from #12744 |
||
|
|
dc29f30b02 |
Track theme setting changes via telemetry (#13142)
Fix Color Palette changes not getting tracked, requested by design team. Capture theme changes as `app:setting_changed` telemetry. The only existing hook lived in `SettingItem.vue`, which renders *visible* settings; `Comfy.ColorPalette` is hidden and changed through bespoke theme UI, so it was never tracked. Open to opinions here, we can also remove the hook in SettingItem.vue, and just make everything that was visible opt in. Linear: https://linear.app/comfyorg/issue/GTM-158/track-theme-usage-with-posthog-events |
||
|
|
3377b8e07e |
feat(publish): prefill prior metadata and update published workflows in place (#13139)
> **Goal: make updating a published Hub workflow painless in admin panel.** Today a admin who needs to change a > published workflow has to reject + reshare + republish, which breaks the share link, resets stats, > and blanks the thumbnail — and the rejected backlog can't be cleared. This is one of 3 PRs that fix it: > > | PR | Repo | Fixes | > |----|------|-------| > | [#4505](https://github.com/Comfy-Org/cloud/pull/4505) | `cloud` | Re-publish stops losing data; the editor can read its own published metadata; reviewer search + admin delete | > | #13139 | `ComfyUI_frontend` | The publish dialog prefills prior metadata + thumbnail and updates in place | > | [#10 ](https://github.com/Comfy-Org/comfy-admin-panels/pull/10)| `comfy-admin-panels` | Admins can delete rejected/stale workflows | > > **Merge order:** `cloud` first (the others depend on its endpoints), then `ComfyUI_frontend` and > `comfy-admin-panels` in either order. ## Summary Editor side of the same effort. Re-opening the publish dialog for an already-published workflow now prefills its description, tags, and thumbnail, restores the thumbnail across step/type changes, keeps the local workflow name in sync, and labels the action "Update" instead of "Publish". Depends on the `cloud` PR — that's what finally returns the `share_id` the dialog reads from. ## Changes - **What**: - **Prefill the thumbnail** — `extractPrefill` dropped `thumbnail_url` / `thumbnail_comparison_url`, so only the thumbnail *type* was remembered, never the image. Thread the URLs into `PublishPrefill` and restore them; on submit, send the existing URL when no new file is attached (reuses the `sampleImageUrls` precedent — an existing URL, not a `File`), so re-publishing doesn't blank it. - **Uploads survive navigation** — the thumbnail step kept its `File` in local component state, so leaving the step and coming back blanked a fresh upload. The step is now controlled — files live in the form data, the single source of truth that survives the remount. - **Type-gate the prefilled image** — a restored image must only show on the tab it belongs to; `existingThumbnailType` keeps an image off the video tab (which was hiding the upload prompt) while still restoring it when you toggle back. - **Refetch on rename** — the dialog is a reused singleton, so `onMounted` fires once; a watch on the active workflow path refetches prefill when a rename changes it (the description was going stale). - **Name sync** — editing the name field published a new Hub display name but never renamed the local workflow, so the editor tab (and a reload) kept the old name. Publish now renames the local file when the chosen name differs. - **"Update" CTA** — the intro panel and footer read "Update" (not "Publish") when the workflow is already published, and note that the share link + stats are preserved. ## Review Focus - `existingThumbnailType` is the load-bearing bit for both the preview and submit gating — confirm an image prefill never submits as a video after a type toggle, and that toggling back restores it. - Name sync renames *after* a successful publish and is non-fatal on failure (toast + keep the publish). The Hub record is keyed by workflow ID, so the rename doesn't orphan it — worth a sanity check. ## Screenshots https://github.com/user-attachments/assets/99dd9eff-987f-4ddb-9cf1-e9b40f61e7dc |
||
|
|
4a2393be48 |
chore: drop unnecessary exports on file-local types to satisfy knip (#13204)
Current `main` **fails a fresh `knip` run** with 13 unused exported types (exit 1). They're invisible on main because lint/knip only runs on `pull_request`/`merge_group`, never on push to main — so merge skew (one PR adds an export used by file X; a later PR removes X's usage) accumulates latent failures that ambush backport branches (e.g. #13163, #13162). Each of the 13 is `export`ed but referenced only within its own file (verified 0 importers; ≥2 in-file uses, so not dead code). Fix: drop the redundant `export`. Types cleaned: `VideoSource`, `ObjectInfoResponse`, `PromotedMissingModelWorkflow`, `PixelReadout`, `ResizeDirection`, `ResizeHandle`, `RunButtonTelemetryOptions`, `ResolvedModelNode`, `AccountPreconditionContext`, `SubscriptionDialogOptions`, `MonthlyCreditsUsage`, `MissingMediaReference`, `ResolvedHostWidget`. Reviewer note: `ResolvedHostWidget` and `ResolvedModelNode` sit under `renderer/extensions`/`platform/assets`; no in-repo importers, but if either is intended as published/extension-facing API, prefer a knip `entry`/`ignore` over un-exporting — flag in review and I'll adjust. After fresh `knip`: **0 unused exported types**. Supersedes #13179 (fixed only `AccountPreconditionContext`). Pairs with the push-gate workflow #13203 — merge this first so that gate is green on main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a451a90868 |
FE-1150 feat(rightSidePanel): hide 3D viewport widgets from panel (#13206)
## Summary Add a hideInPanel widget option so a widget still renders on the node body but is omitted from the right side panel. Apply it to the Three.js viewport widgets (Load3D, Preview3D, Load3DAdvanced, SaveGLB), whose non-syncable scene state would diverge if a second instance rendered in the panel. App mode and the subgraph editor are unaffected (they filter on canvasOnly independently). Discussed with @alexisrolland and @PabloWiedemann ## Screenshots before <img width="2206" height="1181" alt="image" src="https://github.com/user-attachments/assets/e536871f-65e6-4d6e-aa61-dc981362214f" /> after <img width="2743" height="1295" alt="image" src="https://github.com/user-attachments/assets/6cc6d252-57ac-464a-a2b7-1ada5ab9e705" /> |
||
|
|
be102899d7 |
1.47.6 (#13194)
Patch version increment to 1.47.6 **Base branch:** `main` --------- Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
abd1a6f10a |
fix: use error colour for runtime execution error node outline (#13184)
## Summary A node that fails at runtime is now outlined in the same red as a node that fails validation, instead of magenta. ## Changes - **What**: The `executionError` stroke in `litegraphService.ts` was hardcoded to magenta (`#f0f`); validation errors use `LiteGraph.NODE_ERROR_COLOUR` (red). Reuse that constant so both error states render consistently. One-line change; `LiteGraph` is already imported. ## Review Focus No test added: asserting a hardcoded stroke colour would be a change-detector test. The two error paths (validation via `has_errors` / `NODE_ERROR_COLOUR`, runtime via `lastExecutionError`) now share the same colour source. |
||
|
|
c16f10b49e |
Long workflow name cleanup (#13180)
When loading a workflow by dragging and dropping an output from the assets sidebar, the very long and unhelpful url would be used as the workflow name. This is fixed by instead using the asset display name | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/5c68ae48-1fa6-40e1-b2fb-6188ccd60391"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/29770c35-da48-4be9-943e-8ee69eb25e6a" />| Additionally, a max width is added to the breadcrumb items to avoid extremely long names. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/508155ec-81d7-4ca5-8910-f42a70c9cb4b"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/d335ceb7-bfeb-481f-a132-c700e017ee0c" />| |
||
|
|
b4ae6344d7 |
Brand local node IDs (#13085)
## Summary Adds a branded local `NodeId` helper and starts separating local node identity from serialized workflow IDs. ## Changes - **What**: Adds central `NodeId` parsing/branding helpers, migrates nearby widget identity types, keeps queue results at the serialized boundary, and removes misleading workflow `NodeId` usage from execution error maps. ## Review Focus Check that the first migration slice keeps serialized/API IDs as raw `number | string` while local UI/store IDs use the branded string type. ## Caveat `SUBGRAPH_INPUT_ID` and `SUBGRAPH_OUTPUT_ID` are now branded local `NodeId` string values internally instead of numeric sentinels. Reviewers should double-check extension compatibility for callers that import `Constants` and compare those values numerically. ## Screenshots (if applicable) N/A --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
feaa4ce82f |
refactor: localize system stats headers and fix PyTorch casing (#12253)
## Summary Localizes the 12 system stats column headers via vue-i18n and fixes the `Pytorch` → `PyTorch` casing typo. ## Changes - **src/locales/en/main.json**: Add 11 i18n keys under `g.systemStats*` namespace - **src/components/common/SystemStatsPanel.vue**: - Import `useI18n` and use `t()` for column headers - Change `header` field to `headerKey` in ColumnDef type - Fix PyTorch casing (was `Pytorch Version`) ## Context Follow-up to PR #11816 per review comment. ## Test plan - [x] Column headers render correctly - [x] Copy System Info includes localized headers - [ ] Verify other locales can override (out of scope - EN only for now) Closes #11870 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12253-refactor-localize-system-stats-headers-and-fix-PyTorch-casing-3606d73d36508134af99f7ca4f9c6593) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
f5d059b720 |
feat(i18n): add Hebrew (he) language support (#12721)
## Summary
Adds full Hebrew (`he` / עברית) localization for the ComfyUI frontend
UI, registered the same additive way as the existing RTL locales (`ar`,
`fa`).
## Changes
- **What**:
- New `src/locales/he/main.json`, `commands.json`, and `settings.json` —
complete, human-reviewed Hebrew translations.
- Exact key parity with `en` (3524 / 125 / 235 keys), verified
programmatically.
- All `{interpolation}` placeholders and `|`-separated plural forms are
preserved (same segment counts as `en`).
- Established technical terms are kept in English (API, GPU, VAE, CLIP,
LoRA, ControlNet, Civitai, Hugging Face, flux, Nodes 2.0, …).
- `dataTypes` and `nodeCategories` are kept as verbatim English
identifiers; option **keys** in
`settings.json`/`menuLabels`/`contextMenu` are left untouched (only
their display values are translated).
- `src/locales/he/nodeDefs.json` is an empty object on purpose, so node
definitions fall back to English and get auto-populated by the release
i18n workflow (per the locale CONTRIBUTING guide).
- Registered `he: { text: 'עברית', loaders: loadersFor('he') }` in
`src/locales/localeConfig.ts` (which automatically adds it to the
Settings → Language dropdown and `SUPPORTED_LOCALE_OPTIONS`).
- Added `he` to `outputLocales` in `.i18nrc.cjs`, plus a Hebrew glossary
block for the CI translator.
## Review Focus
- This follows the approach of #7876 (Persian/Farsi). Like the existing
`ar`/`fa` locales, it translates UI text only and does **not** introduce
RTL layout (`dir="rtl"`) — the app does not currently apply RTL layout
for any locale. I'm happy to follow up with proper RTL layout support in
a separate PR if that's wanted.
- Recurring-term glossary used for consistency: node = צומת, workflow =
תהליך עבודה, queue = תור, widget = פקד, subgraph = תת-גרף, canvas =
קנבס, bypass = עקיפה, prompt = פרומפט.
- Native-speaker review is very welcome. cc translation maintainers
@Yorha4D @KarryCharon @DorotaLuna @shinshin86
## Screenshots
Text-only locale addition — no UI/layout changes. After this change,
**Settings → Language** lists **"עברית"**, and selecting it renders the
UI in Hebrew (untranslated node definitions fall back to English).
---------
Co-authored-by: Yosef Chai <192742853+yosef-chai@users.noreply.github.com>
Co-authored-by: christian-byrne <abolkonsky.rem@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
caabebe145 |
Redesign missing model detection contract for promoted subgraph widgets (#13059)
## Summary Redesign missing-model detection for ADR 0009 promoted subgraph widgets so candidates are created from the widget value the user can actually edit, while still using the concrete interior widget as the schema/options source. ## Why This PR Exists This PR comes from the follow-up missing-model detection work for the ADR 0009 / 1.46 subgraph widget changes introduced by [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197). [#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197) intentionally changed promoted subgraph widgets to be represented through subgraph input links. After that change, the promoted widget on the host `SubgraphNode` is the editable value owner, and linked interior widgets are no longer guaranteed to mirror that value. The old missing-model contract still treated the concrete interior node widget as the effective source of truth in subgraph cases: - recursive scans entered the subgraph and scanned the interior widget value; - candidates were keyed by the interior node/widget identity; - the parent subgraph host mostly received propagated highlight/navigation behavior; - subgraph container widgets were not treated as first-class candidate sources. That contract breaks after ADR 0009. A user can resolve a missing model by changing the promoted host widget to an installed model, while the linked interior widget can still hold the old stale value. If detection keeps scanning the linked interior value, entering the subgraph or reloading the workflow can re-create a false missing-model error that no longer corresponds to the value the user can edit. ADR 0009 also means the same subgraph definition can be reused by multiple `SubgraphNode` hosts. Once missing-model detection moves from the interior definition widget to the promoted host widget, the selected value is no longer a property of the shared definition alone. It is a property of a specific host instance. That makes the old interior-node identity insufficient for mode changes, removal handling, and re-scan behavior: a single interior leaf definition can be reachable through multiple host execution paths, and only the affected host path should add, remove, or restore a candidate. This PR also builds on [#12990](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12990), which narrowed workflow-level `models[]` and embedded model data to metadata enrichment only. Together, the intended boundary is: - live widgets create missing-model candidates; - workflow/root-level `models[]` and node metadata only enrich candidates that already came from a live widget; - promoted widget values are read from the editable host widget, not inferred from stale interior `widgets_values`. ## Changes - **What**: - Introduces promoted-widget scan targets that split: - host promoted widget value and candidate identity; - concrete leaf widget/node definition data. - Scans the outermost unlinked promoted widget on a `SubgraphNode` host as the selected value owner. - Skips linked interior widgets as candidate sources, preventing stale linked widget values from producing duplicate or false missing-model candidates. - Resolves the concrete leaf widget for combo options, asset-widget support, node type, directory lookup, and embedded metadata enrichment. - Keys promoted missing-model candidates by the host execution id and host promoted widget name. - Adds `sourceExecutionId` to promoted candidates so liveness still follows the concrete source execution path, including nested inactive subgraph containers. - Uses source-scope activity for pipeline filtering and async verification, while keeping highlight/store/clearing identity host-keyed. - Removes host-keyed promoted candidates when their source execution scope is removed or bypassed. - Re-scans ancestor subgraph hosts when an interior source path is un-bypassed, so host-keyed promoted errors can reappear correctly. - Handles shared subgraph definitions by deriving promoted source paths from the concrete host instance path, rather than treating the shared definition node id as globally unique. - Shares promoted source resolution between Vue node processing and the right-side panel to avoid drift. - Aligns missing-model clearing across Vue node widgets, legacy canvas widgets, and right-side panel Parameters/Nodes section widgets. - Adds unit coverage for scan identity, source-scope liveness, dynamic mode changes, source-scope removal, shared-definition host isolation, and right-side panel clearing. - Adds nested promoted-widget E2E coverage for OSS and Cloud flows across Vue, Parameters tab, and legacy widget surfaces. - **Breaking**: None expected. - **Dependencies**: None. ## New Detection Contract A missing-model candidate is created from an unlinked final editable value owner. That value owner can be: - a normal node model widget; or - the outermost promoted model widget displayed on a `SubgraphNode` host. For promoted widgets: - the host promoted widget supplies the selected value; - the host execution id and host widget name are the candidate identity; - the concrete leaf widget supplies definition data such as combo options and asset-browser support; - the concrete source execution path is retained as `sourceExecutionId` for liveness only; - linked interior widgets are skipped as candidate sources because their values are not authoritative when driven by a promoted input. For a nested chain: `Outer promoted widget A -> inner promoted widget B -> concrete widget C` only `A` creates the candidate. `B` and `C` are linked along the promoted-input path and are skipped as selected-value sources, while `C` still provides the concrete widget definition used to evaluate `A`. ## Shared Definition And Source-Scope Liveness ADR 0009 promoted widgets make subgraphs behave more like reusable definitions with host-owned inputs. Two host `SubgraphNode`s can point at the same interior subgraph definition while carrying different promoted widget values. In that shape, the missing-model candidate must be keyed to the editable host surface, but the activity check cannot use the host id alone. For example, if two outer hosts share the same nested subgraph definition, one host can select a valid model while the other still selects a missing model. The result should be one missing-model reference, not a single definition-level error and not two errors after one host is fixed. Likewise, bypassing or un-bypassing an interior nested container should affect only the host execution paths that actually pass through that container. This PR therefore separates two concepts: - **candidate identity**: host execution id + host promoted widget name, used for storage, highlight, navigation, and clearing; - **candidate liveness**: concrete source execution path, used for scan-time activity checks, pipeline filtering, async verification, source-scope removal, and re-exposure after mode changes. That separation is the reason this PR updates more than the scan itself. Moving the detection target to the subgraph host also requires the mode-change and removal paths to understand that a host-keyed candidate can be invalidated by a descendant source path, and can need to be restored by re-scanning an ancestor host when an interior source path becomes active again. ## Review Focus Please review the identity split carefully: - candidate/store/highlight/clearing identity should remain host-keyed for promoted widgets; - liveness should use `sourceExecutionId` when present, falling back to `nodeId` for normal candidates; - scan-time activity checks should account for the source node itself and all ancestor subgraph containers; - source-scope removal should remove host-keyed candidates whose concrete source path was removed or bypassed; - un-bypassing an interior source path should re-scan affected ancestor subgraph hosts so host-keyed candidates can reappear; - shared subgraph definitions should not merge errors across different host instances; - linked interior widgets should not produce their own missing-model candidates; - asset-browser eligibility should be resolved from the concrete leaf node type and widget name, not the synthetic subgraph host type; - right-side panel edits should clear host missing-model errors and source validation errors consistently. The E2E matrix intentionally keeps nested promoted workflows only. Nested promoted widgets cover the same editable host path as single promoted widgets while also exercising the `A -> B -> C` chain that can break source-scope liveness and re-scan behavior. The nested fixture also includes multiple host instances that share the same subgraph definition, so it verifies that fixing one host does not accidentally clear or suppress another host's missing-model candidate. Direct/single promoted behavior is still covered at the unit level. ## Non-Goals - This PR does not reintroduce workflow-level `models[]` candidate creation. - This PR does not infer selected model values from `widgets_values`. - This PR does not synchronize linked interior widget values back from promoted host widgets. - This PR does not redesign missing-media scanning; missing media still skips subgraph containers and remains keyed by concrete interior paths. The shared async post-verification active-scope filter is intentionally stricter, so a pending missing-media candidate is no longer surfaced if its own node is bypassed or removed while verification is in flight. ## Validation - `pnpm exec vitest run src/components/rightSidePanel/parameters/SectionWidgets.test.ts src/platform/missingModel/missingModelScan.test.ts src/composables/graph/useErrorClearingHooks.test.ts src/platform/missingModel/missingModelPipeline.test.ts src/platform/missingModel/missingModelStore.test.ts src/utils/graphTraversalUtil.test.ts src/composables/graph/useGraphNodeManager.test.ts src/renderer/extensions/vueNodes/composables/useProcessedWidgets.test.ts --reporter=dot` - 8 files passed, 294 tests passed. - `pnpm exec vitest run src/platform/missingModel/missingModelScan.test.ts src/core/graph/subgraph/resolveConcretePromotedWidget.test.ts src/components/rightSidePanel/parameters/SectionWidgets.test.ts` - 3 files passed, 71 tests passed. - `pnpm typecheck` - `pnpm typecheck:browser` - `pnpm format:check` - targeted ESLint for changed production/unit/E2E files - `git diff --check` - `pnpm build` - `pnpm build:cloud` - OSS affected E2E on the 8188 build: - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts --project=chromium --grep "Changing an OSS .*promoted|Refreshing a resolved promoted|Reloading a resolved nested"` - 5 passed. - Cloud affected E2E on the 8188 cloud build: - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm exec playwright test browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts --project=cloud --grep "Changing a Cloud .*promoted"` - 2 passed; the Cloud legacy promoted asset-modal case still fails until [#13075](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13075) is merged. - Full OSS `errorsTabModeAware.spec.ts` on the 8188 build: - 23 passed; 3 existing paste/clipboard cases failed before the promoted subgraph section with node count remaining at 1 after `clipboard.paste()`. - Commit hooks ran `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`, and browser typecheck where applicable. - Pre-push hook ran `pnpm knip --cache`. ## Screenshots Before https://github.com/user-attachments/assets/6380c1da-1d92-4b70-888e-3ade572c4b5b After https://github.com/user-attachments/assets/4cfc24d6-3dc3-4e36-9b31-72fea6b3d9d5 |
||
|
|
7376402fc6 |
Essentials tab redesign (#12744)
Subsumes #12304 Redesigns the Essentials tab to be frontend designed with more accessible icons and tighter organization. <img width="381" height="1345" alt="image" src="https://github.com/user-attachments/assets/193f7f5f-20c8-4bf0-8304-ec2c990186d0" /> --------- Co-authored-by: comfydesigner <comfydesigner@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
13b42d9b59 |
Ensure dynamic combo children cleanup state (#13073)
#12617 introduced a regression in Dynamic Combos. If two options have child widgets of the same name (such as `bit_depth` on `Save Image (Advanced)`), then widget state would be incorrectly shared between the two widgets. This is resolved by having removed widgets also delete their state. There was previous interest in having widgets of this type keep state when valid. This interest remains, but will require a more controlled intentional implementation in the future. Since the bit depth options on `Save Image (Advanced)` could potentially be expanded in the future, this PR specifically adds a new devtools node for testing with. --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
e604c85b88 |
1.47.5 (#13166)
Patch version increment to 1.47.5 **Base branch:** `main` --------- Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
7ae3ad936c |
When dragging vue nodes, also drag reroutes (#12885)
`selectedItems` was being filtered to nodes and groups. Since no special behaviour is being performed on groups, the 'move groups' code is relaxed to instead 'move all non-node selected items'. |
||
|
|
7b83228cdd |
1.47.4 (#13083)
Patch version increment to 1.47.4 **Base branch:** `main` --------- Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> |
||
|
|
d26f578c81 |
refactor(litegraph): delete dead onGetNodeMenuOptions, deprecate onBeforeChange (#12230)
Stacks on #12228. Part of the LGraph dead-hook cleanup per AUDIT-LG.9. ## What - `LGraph.onGetNodeMenuOptions` — **deleted** (field + dispatcher in `LGraphCanvas.getNodeMenuOptions`). Zero ecosystem consumers. - `LGraph.onBeforeChange` — **deprecated, not deleted**. The field and the dispatch in `LGraph.beforeChange()` are kept, but assigning a handler now emits a one-time `warnDeprecated` nudging migration to `LGraphCanvas.onBeforeChange`. ## Why onBeforeChange is preserved The W2F-1 re-audit found `bmad4ever/ComfyUI-Bmad-DirtyUndoRedo` assigns `app.graph.onBeforeChange = fn` (the listener-assignment pattern). Deleting the field outright would silently turn those handlers into no-ops. Keeping it as a deprecated shim preserves backward compatibility during a grace period while signaling the intended replacement. `onAfterChange` is untouched (so `BennyKok/comfyui-deploy`'s `onAfterChange` wrapper keeps working). `LGraphCanvas.onBeforeChange` remains a separate field, and the canvas dispatch chain `this.canvasAction((c) => c.onBeforeChange?.(this))` is unchanged. ## Tests `LGraph.test.ts` covers the shim: the assigned listener is still invoked, the deprecation warning fires when used, and no warning fires when no listener is assigned. ## Sequencing - Stacks on #12228 - Sequences behind Alex's Phase B (#11939, #11811) --------- Co-authored-by: Connor Byrne <c.byrne@comfy.org> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
cfbc378df3 |
feat(billing): align workspace Plan & Credits panel to DES-186 (FE-768) (#12761)
## Summary Aligns the Settings ▸ Workspaces **Plan & Credits** panel to DES-186: state-driven subtitle, team/personal header variants, design perks, and a shared footer help bar. Stacked on FE-964 (#12734), which owns the CreditsTile content. ## Changes - **What**: - Subtitle per design variants (Figma 3255-21472): Active → "Renews on {date}", Ending → "Ends on {date}". `subscription_status: 'scheduled'` falls back to the Active treatment — the facade exposes no scheduled-plan target/date fields yet, so "Changes to {plan} on {date}" cannot be rendered (template reserved in i18n-ready form; see Linear note). - Team-active header: plan name "Team" + seat-aware workspace total (`seat_summary.total_cost_cents` from `/billing/plans`) as "$X USD / mo"; the Next-month-invoice card reads the same computed so the two can't disagree. Per-member tier price + "USD / mo / member" kept as the plans-unresolved fallback. - CTAs per design + designer annotation: "Manage billing" + "Change plan" on a team plan, "Upgrade plan" on personal. - Team perks (Figma 2993-14789): "Your plan includes everything in **Pro**, plus:" (i18n-t, plan name emphasized) + invite members / concurrent runs / shared credit pool / role-based permissions. - Personal no-subscription variant (Figma 2993-14604): "Free · $0 USD / mo" header + primary Subscribe CTA + "What's included:" with the max-runtime perk → "10 min max runtime" (`subscription.maxDuration.free` set to 10 min per DES 3253-16079). - Footer help bar (Learn more / Partner Nodes pricing table / Message support / Invoice history) extracted into `SubscriptionFooterLinks.vue`, shared by workspace + legacy panels; new surface-specific key for "Partner Nodes pricing table". - **Breaking**: none. ## Review Focus - Seat-aware price source (`currentPlan.seat_summary.total_cost_cents`) vs per-member fallback — fixture locks $320 vs $80. - `'scheduled'` → Active fallback (adapter-level test in `useWorkspaceBilling.test.ts`). - Free-state perk copy: `subscription.maxDuration.free` set to **10 min** per DES 3253-16079 (design confirmed; was 30 min). - Free-state overflow (`⋯`) button intentionally omitted: the only existing menu item (Cancel Subscription) doesn't apply to a free plan. Linear: [FE-768](https://linear.app/comfyorg/issue/FE-768/updates-to-workspaces-tab-of-settings) (Plan & Credits half; Members invite UI ships in #12759) ## Screenshots Captured on `local.comfy.org` dev (cloud-prod backend, authenticated session). Team and free states use client-side API stubs (XHR-level for `/api/billing/*` + `/api/workspace/*`, fetch-level for legacy `/customers/*`) since the test workspaces are unsubscribed; personal-active rows are real account data. | State | Before (FE-964 base) | After (this PR) | |---|---|---| | Team plan — active | <img width="400" alt="before: Pro, $100 USD/mo/member, Renews, Manage Payment/Upgrade Plan" src="https://github.com/user-attachments/assets/622a9a27-1875-4c08-92b7-9e43a8067c59" /> | <img width="400" alt="after: Team, $300 USD/mo, Renews on, Manage billing/Change plan, design perks" src="https://github.com/user-attachments/assets/adb0f767-d508-4455-ad8e-ee2d6ac419dc" /> | | Team plan — ending (cancelled) | <img width="400" alt="before: Expires Jul 10, 2026" src="https://github.com/user-attachments/assets/cb4bb978-4e8b-4372-8ecc-7265f477a828" /> | <img width="400" alt="after: Ends on Jul 10, 2026" src="https://github.com/user-attachments/assets/5466b99a-d016-4eba-a60f-99b87bd2693e" /> | | Personal — active (real data) | <img width="400" alt="before: Renews Mar 10 2027, Manage Payment" src="https://github.com/user-attachments/assets/34018400-930b-4147-bd0d-398fb4d159ee" /> | <img width="400" alt="after: Renews on Mar 10 2027, Manage billing" src="https://github.com/user-attachments/assets/87af7fa7-28cf-4cdf-b9a2-158b2a6eb979" /> | | Personal — no subscription | <img width="400" alt="before: generic not-on-a-subscription prompt" src="https://github.com/user-attachments/assets/7ee5b36e-1e07-4630-a9b7-680f4fad349b" /> | <img width="400" alt="after: Free $0 USD/mo header + Subscribe + What's included" src="https://github.com/user-attachments/assets/1eda1791-e3de-46c3-bb69-df0365545211" /> | --- ## Perk descriptions — designer QA (DES `3253-16079`) CDP-verified the Plan & Credits **perk list** per current plan against Figma. Captured at the full 1280px settings layout (width fix: #12849; the perk text/content is identical at the current 960px). | Plan | "Includes" header | Perks shown | |---|---|---| | Free (no subscription) | What's included: | 10 min max runtime | | Personal — Pro | Your plan includes: | 1 hr max run duration · RTX 6000 Pro (96GB VRAM) · Add more credits whenever · Import your own LoRAs | | Team | Your plan includes everything in **Pro**, plus: | Invite members · Members can run workflows concurrently · Shared credit pool for all members · Role-based permissions | **Free — Figma `3253-16079` (left) vs implementation (right):** | Figma | Implementation | |---|---| | <img width="480" alt="Figma DES 3253-16079 free settings" src="https://github.com/user-attachments/assets/cfe79570-f3ba-4627-a8eb-348b9158f6ac" /> | <img width="480" alt="App free settings — What's included: 10 min max runtime" src="https://github.com/user-attachments/assets/d89898e4-d819-486c-9b4f-c2fd61916783" /> | **Personal — Pro:** <img width="640" alt="App personal Pro — Your plan includes" src="https://github.com/user-attachments/assets/adc2fd9f-d249-469f-b947-1ec8f674cbb0" /> **Team:** <img width="640" alt="App team — Your plan includes everything in Pro, plus" src="https://github.com/user-attachments/assets/e7378067-11a2-411b-b37b-98c8aecb82b1" /> Open items (design): - Free perk now reads **"10 min max runtime"** (`subscription.maxDuration.free` set to 10 min) per Figma `3253-16079` — ✅ applied in this PR. - Personal-plan perk **stacking** (show lower-tier perks under the current tier) is an unresolved Figma thread on this node — not implemented. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
db085eb7a1 |
feat(auth): Cloudflare Turnstile on email signup (origin-gated) (#12924)
Adds a Cloudflare Turnstile widget to the email/password signup form
(web + desktop). The frontend renders the widget and attaches its token
to the signup request; the verification decision is made server-side.
## Design — config-driven, no origin sniffing
* The widget renders **iff** the `signup_turnstile` mode is `shadow` or
`enforce` **and** a `turnstile_sitekey` is present — both delivered via
cloud remote config. OSS / local builds receive no remote config, so it
never renders. Gating is a pure `isTurnstileEnabled(mode, siteKey)`; an
unknown mode normalizes to `off`.
* Submit is blocked only in **enforce**; **shadow** never blocks.
* The token is sent as `turnstile_token` (snake_case, optional) on the
customer-creation request.
* **OAuth** never renders the widget or sends a token (federated
providers are exempt).
## Behavior
* **Decision is server-side** — the frontend only renders the widget and
attaches the token; the backend verifies it and decides allow/block.
* **Mode-driven** — `off` (no-op) / `shadow` (render + attach, never
blocks) / `enforce` (blocks submit until solved).
* **Config-gated** — no `isCloud`/origin check in the client; the widget
is driven purely by the presence of the mode flag + sitekey in remote
config.
* **Fail-safe to off** — an unknown/missing mode or a missing sitekey
resolves to "don't render", so the feature is a no-op until both are
configured.
* The sitekey is a public, client-side value delivered per environment
via remote config; in dev it falls back to Cloudflare's always-pass test
sitekey.
## Files
New: `config/turnstile.ts`, `composables/auth/useTurnstile.ts` (+ test),
`composables/auth/turnstileScript.ts`,
`components/dialog/content/signin/TurnstileWidget.vue`. Edited:
`SignUpForm.vue`, `SignInContent.vue`, `useAuthActions.ts`,
`authStore.ts` (+ test), `remoteConfig/types.ts`,
`locales/en/main.json`.
## Flow
```mermaid
sequenceDiagram
actor U as User
participant FE as Signup form
participant CF as Cloudflare Turnstile
participant API as Backend signup API
Note over FE: renders only when mode is shadow or enforce<br/>and a sitekey is present
U->>FE: open email/password signup
FE->>CF: load widget with sitekey
CF-->>U: challenge (usually invisible)
U-->>CF: solve
CF-->>FE: token (single-use, short-lived)
U->>FE: submit
FE->>API: signup request with turnstile_token
Note over API: verifies the token server-side and<br/>decides allow/block (shadow never blocks)
API-->>FE: allowed, or blocked in enforce
```
## Rollout
Config-driven and a no-op until enabled:
1. **Merge + deploy** the FE — no visible change while the mode is `off`
/ no sitekey.
2. **Set** the `turnstile_sitekey` in remote config per environment.
3. **`signup_turnstile=shadow`** — the widget renders and attaches the
token; the server observes and never blocks.
4. → **`enforce`** — the FE blocks submit until the challenge is solved.
Kill switch: set the mode back to `off` and the widget stops rendering.
## Refactor: shared script loader
The Turnstile script loader was extracted to
`utils/loadExternalScript.ts` (`createScriptLoader`) and now also backs
the existing Typeform embed loader, removing duplicated
singleton/timeout/cleanup logic. Minor behavioral change: when a
matching `<script>` tag already exists in the DOM, the loader polls for
the global to become ready instead of attaching a `load` listener (which
may have already fired).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
|
||
|
|
6eaad99502 |
Add center dividing line to image compare node (#13132)
| Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/37afb473-161c-4350-881e-0ea908e28777"/> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/d5acca61-3687-4c15-8029-ef2c88a06944" />| |
||
|
|
9209a4b923 |
Add long widget values to tooltips (#12864)
If a widget value is long (> 10 characters) and on a known single-line widget (`number`, `combo`, or `string), then the widget's full value will be added to the tooltip. Additionally, margins on combo widgets are slightly tweaked so more of the text displays before truncation occurs. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/fefd76e9-6511-4e98-80f6-030a6dc34fb8" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/0cbc100d-066e-4272-afe9-795e56c12353" />| |
||
|
|
5a01c5b3b4 |
Remove redundant Enter Subgraph action from Errors tab (#13136)
## Summary Remove the redundant **Enter Subgraph** action from Errors tab node cards. This button should not be part of the updated Errors tab design; it remained from the previous implementation and its removal was missed when the new interaction model was introduced. ## Changes - **What**: Removed the Errors tab `Enter Subgraph` button from `ErrorNodeCard`, along with the `enterSubgraph` event plumbing in `TabErrors`. - Removed the now-unused `useFocusNode().enterSubgraph()` helper path, since the Errors tab no longer has a separate subgraph-only action. - Removed the `ErrorCardData.isSubgraphNode` flag and its population in `useErrorGroups`, because it only existed to decide whether to show this button. - Removed the Storybook story and unit-test expectations that were specifically tied to the removed button/flag. - Removed the now-unused English `rightSidePanel.enterSubgraph` i18n entry. Non-English locale files are intentionally left untouched per the repo's localization update policy. ## Why The Errors tab already has a **Locate node on canvas** action. For errors inside subgraphs, that action navigates into the relevant subgraph and centers the target node on the canvas. The removed **Enter Subgraph** action was therefore a weaker duplicate: it entered the subgraph and fit the view, but did not provide the same direct target-node positioning. Keeping both actions made the card UI more crowded and exposed two very similar navigation paths with overlapping intent. The updated design should only keep the more useful locate action, so this PR removes the stale duplicate surface rather than adding another hidden/negative assertion around it. ## Review Focus Please verify that this only removes the Errors tab-specific action. The normal node footer/canvas subgraph navigation behavior remains untouched. Validation run locally: - `pnpm exec vitest run src/components/rightSidePanel/errors/ErrorNodeCard.test.ts src/components/rightSidePanel/errors/TabErrors.test.ts src/components/rightSidePanel/errors/useErrorGroups.test.ts` - `pnpm typecheck` - `pnpm lint` - `pnpm format:check` - `pnpm knip` ## Screenshot Before <img width="335" height="595" alt="스크린샷 2026-06-25 오후 5 33 37" src="https://github.com/user-attachments/assets/545f80e9-68bb-45ef-a4da-0a41012269f6" /> After <img width="344" height="591" alt="스크린샷 2026-06-25 오후 5 34 24" src="https://github.com/user-attachments/assets/7c1f1bf6-c5fd-4a43-9b5c-1392246070a8" /> |
||
|
|
e3049e7c31 |
feat(billing): single billing path — collapse personal/team dispatch to flag-only (B1 / FE-966) (#12953)
## What this does
Collapses the personal-vs-team billing dispatch so it keys ONLY on the
build/flag (`teamWorkspacesEnabled ? 'workspace' : 'legacy'`). Personal
now flows through `useWorkspaceBilling` (`/api/billing/*`), same as team
("personal plan = single-seat workspace"). This converges status /
balance / subscribe / preview / cancel / portal in one change.
Dispatch sites collapsed:
- `useBillingContext.ts` — `type` computed: dropped the
`store.isInPersonalWorkspace` branch → flag-only.
- `useBillingContext.ts` — D3 subscription→store mirror watch: dropped
the `isInPersonalWorkspace` early-return guard so personal also mirrors
into the workspace store.
- `useSubscriptionDialog.ts` — `useWorkspaceVariant` compound predicate
→ flag-only (personal + flag-on now uses the workspace required-dialog
variant).
- `SubscriptionPanel.vue` — already flag-only
(`v-if="teamWorkspacesEnabled"`); no change needed.
## Kept (Risk #6)
- The ~11 raw `workspace.type === 'personal'` checks in
`teamWorkspaceStore.ts` — workspace-TYPE membership logic
(can-delete/leave, fetch-members, switcher), NOT billing dispatch.
Untouched.
- `useLegacyBilling` / `useSubscription` / authStore billing methods
kept intact for the flag-OFF (OSS/Desktop) path.
## Flag-off unchanged
Flag-OFF (OSS/Desktop) still selects `legacy` (`/customers/*`). Verified
by unit test.
## Tests
- `useBillingContext`: flag-ON → personal selects `workspace`; flag-OFF
→ `legacy`; D3 mirror now fires for personal under flag-on.
- `useSubscriptionDialog`: flag-ON → workspace required-dialog variant
for personal; flag-OFF → legacy personal variant.
## Follow-up (deferred, not in this PR)
Post-flip cutover deletion of `useLegacyBilling`-only components:
`PricingTable.vue`, `SubscriptionPanelContentLegacy.vue`,
`TopUpCreditsDialogContentLegacy.vue`, `CurrentUserPopoverLegacy.vue`,
`subscriptionCheckoutUtil.ts`, `useSubscriptionCancellationWatcher.ts`.
- Fixes part of FE-903 (B1)
|
||
|
|
87e84e7280 |
feat(billing): inline Invite-your-team block on team-upgrade success (FE-965 / DES-394) (#12954)
Renders the inline **"Invite your team"** block in the team variant of FE-934's "You're all set" success card (`SubscriptionSuccessWorkspace.vue`), so a buyer can invite teammates right after a team-plan upgrade (DES-394, Figma 3084-18651). - New shared `InviteMembersForm.vue` (chips / `TagsInput` multi-email form); seats capped via `useBillingContext.getMaxSeats(tierKey)`; submit via `workspaceApi.createInvite`. - Team upgrades only — personal / single-seat plans show the plain success card; gated on `teamWorkspacesEnabled` + a team plan. - `workspace_invite_sent` telemetry distinguishing a post-upgrade invite from a Settings invite; success-card i18n + Storybook story. **Stacked on FE-934 #12975** (`jaewon/fe-934-team-subscribe-wire`, the success-card host). The PR base is that branch, so this diff is FE-965's delta only. Re-using the same form in the Settings invite dialog is out of scope here (belongs with FE-768 / a follow-up). ## Screenshots | Team upgrade — invite block | Personal / non-team — no invite block | |---|---| | <img src="https://github.com/user-attachments/assets/be5450fe-2b83-46bd-afbc-00e6d33590b7" width="420" /> | <img src="https://github.com/user-attachments/assets/a91909c7-7629-42ef-80b6-45fdb070a0e8" width="420" /> | Storybook: `Components/SubscriptionCheckoutSteps` → `TeamSuccessWithInvite` (with block) / `SuccessAllSet` (without). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
67009dcda2 |
feat(workspace): promote/demote members via Change role menu (FE-770) (#12782)
Promote / demote workspace members ↔ owners in Settings ▸ Members, per [DES-222 / Figma 2993-15512](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=2993-15512) and the [permissions section 3343-22966](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3343-22966). - Fixes [FE-770](https://linear.app/comfyorg/issue/FE-770/promote-demote-workspace-members-owners-settings-members) - Stacked on #12759 (`jaewon/fe-768-members-invite-ui`) ## Changes - Per-member row (…) menu → **Change role** submenu (Owner / Member, current role check-marked) + existing **Remove member**, replacing the shared PrimeVue `Menu` with the Reka `DropdownMenu`/`DropdownItem` (submenu opens right of parent, flips on collision; scalable for future roles). - **Make [name] an owner?** / **Demote [name] to member?** confirm dialogs (single `ChangeMemberRoleDialogContent`, copy 1:1 from Figma). - `workspaceApi.updateMemberRole` → `PATCH /api/workspace/members/:userId {role}` + `teamWorkspaceStore.changeMemberRole` (local role map update; Role column re-sorts). - **Original-owner guards** (Figma annotations): creator pinned to the top of the list, no row actions for anyone on that row; own row also has no actions. Creator inferred as earliest `joined_at` until BE exposes an explicit flag (tracked as the FE-770 BE blocker — same applies to the endpoint itself, which does not exist yet; UI is wired to the proposed contract). - `DropdownMenu` raised to `z-3000` so the row menu sits above the Settings modal (the Reka popper wrapper copies the content's computed z-index; static `z-1700` lost to dialogs in the `@primeuix` modal sequence). Also drops the always-rendered icon slot in `DropdownItem` so icon-less items (Change role / Remove member) align flush-left. ## User stories verified Viewer = an **owner** (promoted, not the workspace creator), so the creator guard and the self guard are exercised separately. | # | Click → action → expected | | --- | --- | | US1 | Member row (…) → menu shows **Change role ›** + **Remove member** | | US2 | Hover **Change role** → Owner / Member submenu, **current role check-marked** | | US3 | Click the current role (✓) → no dialog, no PATCH (no-op) | | US4 | Member row → **Owner** → "Make {name} an owner?" + "They'll have the same access as you — managing members, billing, and workspace settings." + Cancel / **Make owner** | | US5 | **Cancel** (or ✕) → dialog closes, role unchanged, no PATCH | | US6 | **Make owner** → `PATCH /api/workspace/members/:id {role:'owner'}` → Role column → Owner, row **re-sorts under the creator**, "Role updated" toast, the promoted row keeps its (…) menu | | US7 | Promoted owner row → **Member** → "Demote {name} to member?" + "They'll lose admin access." → **Demote to member** → Role column back to Member | | US8 | **Creator row (earliest joined) has no (…) button** — even for another owner | | US9 | **Own (You) row has no (…) button** — even when not the creator | | US10 | PATCH 500 → "Failed to update role" toast, **dialog stays open**, role unchanged | | US11 | Viewer with `member` role → no row actions anywhere | | US12 | **Remove member** → existing FE-768 "Remove this member?" dialog | ## Tests Each user story is covered by automated tests and confirmed by a manual CDP pass driving the real cloud app (mocked auth + boot + workspace/billing API). | Story | Unit / Component | E2E (Playwright) | CDP (live app) | | --- | :---: | :---: | :---: | | US1 row menu shows Change role + Remove member | ✅ | ✅ | ✅ | | US2 submenu checkmark follows current role | ✅ | ✅ | ✅ | | US3 picking the current role is a no-op | ✅ | ✅ | ✅ | | US4 promote dialog copy (Make owner) | ✅ | ✅ | ✅ | | US5 Cancel leaves role unchanged, no PATCH | ✅ | ✅ | ✅ | | US6 Make owner → PATCH, re-sort under creator, toast, stays demotable | ✅ | ✅ | ✅ | | US7 demote dialog (Demote to member) → role reverts | ✅ | ✅ | ✅ | | US8 creator row has no (…) menu | ✅ | ✅ | ✅ | | US9 own (You) row has no (…) menu | ✅ | ✅ | ✅ | | US10 PATCH 500 → error toast, dialog stays open | ✅ | ✅ | ✅ | | US11 member-role viewer sees no row actions | ✅ | — | — | | US12 Remove member → FE-768 remove dialog | ✅ | ✅ | ✅ | | Layer | File | What it covers | Result | | --- | --- | --- | --- | | E2E (`@cloud`) | `browser_tests/tests/dialogs/memberRoleChange.spec.ts` | 3 tests — guard rows (US1/US8/US9/US12), promote→re-sort→demote round trip (US3–US7), failed PATCH (US10). FE-964 boot pattern: `CloudAuthHelper` + remote-config flag mock + stateful route mocks capturing PATCH args. Reka submenu driven via `ArrowRight` (synthetic hover doesn't open it). | 3 / 3 green | | Component | `ChangeMemberRoleDialogContent.test.ts` | promote/demote copy, confirm → store + success toast + close, error keeps dialog open, cancel | green | | Component | `MembersPanelContent.test.ts` | creator/self rows hide the menu (US8/US9), member-viewer gating (US11) | green | | Composable | `useMembersPanel.test.ts` | menu factory labels/checkmarks/commands, same-role no-op, creator pin in `sortMembers`, `isOriginalOwner` | green | | Store | `teamWorkspaceStore.test.ts` | `changeMemberRole` success/failure, `originalOwnerId` inference | green | | CDP live | full cloud app on `local.comfy.org` (mocked auth + boot) | promote→re-sort→demote round trip with PATCH applied to mock state, guard rows, submenu checkmark, dialog copy, menu/dialog z-index above Settings, forced PATCH 500 → error toast | verified | ⚠️ Merge-gated on the BE role-change endpoint (no `PATCH /workspace/members/:userId` in cloud OpenAPI as of 2026-06-10; see FE-770 BE-blocker comment). ## Screenshots (local dev, workspace/billing API stubbed; vs Figma 2993-15512) | Members (before) | Change role submenu | | --- | --- | | <img alt="members" src="https://github.com/user-attachments/assets/686fec86-fcb5-4942-a745-50f367022ab0" /> | <img alt="submenu" src="https://github.com/user-attachments/assets/d6adeea8-7001-4c8d-91b7-f5bfc47a50d6" /> | | Promote dialog | After promote (Jane → Owner, still demotable) | Demote dialog | | --- | --- | --- | | <img alt="promote" src="https://github.com/user-attachments/assets/af638cde-2fd6-4c37-b203-78801eeb2785" /> | <img alt="after" src="https://github.com/user-attachments/assets/f47dc7af-6b1b-422c-8a9a-5ec889b9af11" /> | <img alt="demote" src="https://github.com/user-attachments/assets/9a861d04-a23b-4cd4-bc54-1ed3a66c6429" /> | |
||
|
|
026b2c4795 |
feat(billing): unify credits into a facade-driven CreditsTile (FE-964) (#12734)
## Summary ### AS IS <img width="1340" height="798" alt="Screenshot 2026-06-10 at 12 22 36 AM" src="https://github.com/user-attachments/assets/61636fa3-e80c-427b-855b-499e1eca67da" /> ### TO BE <img width="1301" height="793" alt="Screenshot 2026-06-10 at 12 22 39 AM" src="https://github.com/user-attachments/assets/62d9f5a6-da92-45df-94e7-cd3c244249f9" /> ### Empty states ([added to DES-247 on 2026-06-11](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3349-29750)) | 0 monthly credits | 0 credits | | --- | --- | | <img alt="credits-empty-monthly" src="https://github.com/user-attachments/assets/b3c55d3b-79b0-47b1-9795-c8bf69d5efe2" /> | <img alt="credits-empty-all" src="https://github.com/user-attachments/assets/919081d6-64e1-483b-9c04-6b085243ebc1" /> | Consolidate the divergent Settings credits surfaces into one facade-driven **CreditsTile**, implementing the DES-247 redesign so personal and team modes always render the same balance from `useBillingContext`. ## Changes - **What**: - New `CreditsTile.vue` — total + `remaining`, a stacked monthly/additional progress bar, colored breakdown rows (`Monthly (refills …)` / `Additional`), refresh, and a permission-gated *Add credits* / *Upgrade to add credits* action. Owns the post-checkout (`focus` / `pending_topup`) balance refresh. - Extracted the duplicated inline credits card out of `SubscriptionPanelContentWorkspace.vue` **and** `SubscriptionPanelContentLegacy.vue` onto the shared tile. - Replaced `LegacyCreditsPanel.vue` (read `authStore.balance` directly) with `CreditsPanel.vue` routed through the tile; repointed `useSettingUI` and deleted the legacy panel. - `creditsProgress.ts` pure helper for the bar math + numeric credit getters on `useSubscriptionCredits`. - i18n keys for the unified tile labels. - DES-247 responsive variants via CSS container queries: below ~350px tile width the `{used} used` label, `remaining` suffix, and breakdown subtitle drop and the additional-credits value stacks under its label; below ~230px the monthly summary compacts (`105K left of 200K`). Additional-credits tooltip copy aligned with the updated design (per design feedback). - Empty states (added to DES-247 via Slack on 2026-06-11, low priority): once the monthly allowance is depleted, an info notice renders under the total (`Monthly credits are used up. Refills {date}` / `You're now spending additional credits.`), the monthly bar section dims to 30% opacity, and an `IN USE` pill marks *Additional credits*; once everything is depleted the notice switches to `You're out of credits. Credits refill {date}` and *Add credits* swaps to the `inverted` (filled-white) Button variant. Gated on a loaded balance so the notice never flashes while fetching. - **Dependencies**: Stacked on **#12622 (FE-904 / B2)** for the facade `tier` / `renewalDate` fields — base this PR against that branch; retarget to `main` once FE-904 merges. ## Review Focus - The tile reads everything from the facade (`balance.*Micros` as cents → credits, `subscription.tier`/`renewalDate`), so legacy and workspace modes share one source. - Monthly allowance still comes from `getTierCredits` (hardcoded tier nominal). With real data the monthly *remaining* can exceed the nominal (rolled-over credits), so the bar clamps to a full segment — same semantics as the prior `{monthly} / {planTotal}` display; the canonical allowance is a BE-1047 follow-up. - `LegacyCreditsPanel` deletion: `CreditsPanel.vue` retains the usage-history table + help links and reads the facade. ## Testing - Unit/component (36 green): `CreditsTile.test.ts` (render, zero-state, free-tier, permission gating, add-credits, mount+manual refresh, plus the empty states: depletion notice copy, `IN USE` badge, `inverted` button when fully out, no-flash-while-loading guard), `creditsProgress.test.ts` (clamping/stacking math), `useSubscriptionCredits.test.ts` (`*_micros`-as-cents), and `SubscriptionPanel.test.ts` updated for the extracted tile. - E2E (`@cloud`): `browser_tests/tests/dialogs/creditsTile.spec.ts` boots the cloud app against mocked Firebase auth + stubbed boot endpoints (no backend) and asserts the tile's total / progress bar / monthly+additional breakdown / add-credits in Settings ▸ Workspace ▸ Plan & Credits, then resizes to a narrow viewport and asserts the responsive variant (labels hidden, compacted `11K left of 21K`). A second test boots with a drained monthly balance (0-monthly notice + `IN USE` badge), then re-mocks a fully drained balance and refreshes the tile in place to assert the out-of-credits state. Both pass locally against a cloud dev server; runs in the `cloud` CI project. Drives a raw page because the shared `comfyPage` fixture expects the OSS devtools backend. - Screenshot-verified the tile at the three DES-247 reference widths (448 / 235 / 204px) against the Figma Responsiveness section — 1:1. - Verified live in the running app (Settings ▸ Workspace ▸ Plan & Credits) against the authenticated backend — renders 1:1 with DES-247. The empty-state screenshots above were captured the same way (authenticated app, real Pro subscription, balance endpoint stubbed to the depleted values via CDP). - `pnpm typecheck` / `typecheck:browser` / `lint` / `knip` green. Implements FE-964 (DES-247). --------- Co-authored-by: GitHub Action <action@github.com> |