mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-10 17:17:55 +00:00
2c7ee46beb22dc3b052d895eb9d808eaf4751724
1907 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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> |
||
|
|
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" /> |
||
|
|
9dcab4ee96 |
Essentials Cleanup (#13183)
Address several followup comments from #12744 |
||
|
|
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" /> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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" /> |
||
|
|
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> |
||
|
|
d60260ac3c |
feat(billing): team-plan subscribe + checkout/confirm screen redesign (FE-934) (#12975)
## Summary Two related streams of FE-934 checkout/billing work on this branch: https://github.com/user-attachments/assets/af629def-543e-4bcd-894d-b35aa032fe0a 1. **Team-plan subscribe** wired to the BE-1254 credit-stop contract (replaces the "coming soon" toast stub). 2. **Checkout / confirm screen redesign** aligned to the updated DES mockup — yearly pricing display, dialog navigation, and the plan-change confirm. <img width="1078" height="427" alt="team subscribe" src="https://github.com/user-attachments/assets/2b5f7192-3c91-4e2d-b495-832ca5a26657" /> ## Changes ### Team-plan subscribe (credit-stop contract) - Team subscribe sends `{ plan_slug: team_per_credit_monthly | team_per_credit_annual, team_credit_stop_id, billing_cycle }` to `POST /api/billing/subscribe` and handles the response like the personal path (`subscribed` → success, `needs_payment_method` → payment URL, `pending_payment` → poll). Slider stops come from `GET /api/billing/plans → team_credit_stops` via `mapApiTeamCreditStops`, falling back to the hardcoded DES-197 stops so OSS / pre-deploy still render. `preview-subscribe` is unchanged — the team confirm step is display-only. - **Internal API change**: `workspaceApi.subscribe(planSlug, returnUrl?, cancelUrl?)` → `subscribe(planSlug, options?: SubscribeOptions)`; the billing facade (`useBillingContext` / `useWorkspaceBilling` / `useLegacyBilling`) and callers were updated to match. ### Checkout / confirm screen redesign (DES mockup) - **Yearly confirm**: headline is now the ÷12 monthly-equivalent with a `{total} Billed yearly` (yearly) / `Billed monthly` (monthly) subtitle; credits show `Each year credits refill to` (×12) for yearly; `Starting today` → `Starts today`. The team confirm now receives the active billing cycle (the subtitle was missing because it wasn't passed), and the redundant header credits/month line was dropped. - **Navigation**: the pricing table stays mounted (`v-show`, not `v-if`) so the plan / billing-cycle / credit-stop selection survives a round trip to the confirm step and back; **Backspace** mirrors the back arrow (ignored while an input/textarea/contenteditable is focused). - **Plan-change confirm**: rewritten from the 2-column current→new comparison to the **single-plan layout**, branching on `previewData.is_immediate` — immediate upgrades show prorated line items (`new_plan.price_cents − cost_today_cents` = credit) + upfront (yearly) / monthly credit refill + a prorated total ("Confirm upgrade"); scheduled downgrades show `Starts {date}`, $0 due today, and an "After that" block ("Confirm change"). - **Storybook**: `SubscriptionCheckoutSteps` stories for each new-sub / upgrade / downgrade variation (props-driven, no API). ## Review Focus - **Merge gate (team subscribe)** — ✅ **resolved**: BE-1254 is now merged in cloud `main`. The live `GET /api/billing/plans` returns `team_credit_stops` (`TeamCreditStop` struct + validation in `common/billing/catalog/catalog.go`, served for every workspace, asserted by the `billing_credit_stops_contract` smoke test). The gate is lifted. - **Fallback safety**: with the contract live, the hardcoded DES-197 stops are now purely the OSS / pre-deploy fallback — they render only when the API doesn't supply `team_credit_stops`. In that window a real subscribe is still impossible (no stop `id`) and surfaces a `teamPlan.unavailable` toast. Open question retained: hide/disable the team CTA in that window instead of toasting? - **Plan-change scope**: the single-plan confirm redesign covers **personal** changes (the real `previewSubscribe` path). **Team** plan changes route through the display-only team confirm and aren't wired to `previewSubscribe` (BE left `PreviewSubscribeRequest` as `plan_slug` only) — team-change proration is a follow-up. - **Dead locale keys**: the old 2-column transition keys (`everyMonthStarting`, `youllBeCharged`, `proratedRefund`, `proratedCharge`, `creditsRefillTo`, `switchToPlan`, `starting`, `ends`, `confirmPlanChange`) are now unused — can be removed in a follow-up cleanup. - **Out of scope**: the success "Your change is scheduled" variant from the mockup. - Based on `fe-934-unified-pricing-table`; base + `main` merged in to resolve conflicts (PR is now mergeable). ## Verification - Confirm / transition component tests green (`SubscriptionAddPaymentPreviewWorkspace`, `SubscriptionTransitionPreviewWorkspace`); oxlint / oxfmt clean locally; full typecheck runs in CI. - Each variation viewable in Storybook → `Components/SubscriptionCheckoutSteps`. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
f19597ce81 |
feat(billing): deep link to open the pricing table (FE-1104) (#13001)
## Summary Adds an in-app deep link that opens the pricing table directly, for driving pilot users straight to subscribe (request from nav/Alex). Resolves [FE-1104](https://linear.app/comfyorg/issue/FE-1104). - `/?pricing=1` — on app load, open the pricing table. - `/?pricing=team` / `/?pricing=personal` — open it on the Team / Personal plan tab (via the existing `UnifiedPricingTable` `initialPlanMode`). - Gated to the **original owner** via `useWorkspaceUI().permissions.canManageSubscriptionLifecycle` (personal user, or a team workspace's original owner). A member or a promoted owner is a **silent no-op**: the app loads normally, the param is stripped, no 404 / error / toast. - Off-cloud (OSS): the loader isn't instantiated, so the param is ignored. - Survives the login redirect via the preserved-query system, same as `?invite` / `?create_workspace`. ## How Mirrors the established URL-loader pattern (`useInviteUrlLoader` / `useCreateWorkspaceUrlLoader`): - `preservedQueryNamespaces.ts` / `router.ts` — register the `pricing` namespace + tracker key. - New `usePricingTableUrlLoader.ts` — hydrate preserved query, read `pricing`, strip the param + `clearPreservedQuery` in a single replace before any await, then `await fetchMembers()` (resolves the original-owner gate; no-ops for personal) and open the table only when the gate allows. - `GraphCanvas.vue` — call the loader in `onMounted` after the create-workspace loader (cloud only; not gated on the team-workspaces flag so it also drives personal/legacy users). - `useSubscriptionDialog.ts` — new `'deep_link'` value on `SubscriptionDialogReason`. ## Telemetry Eligible opens emit the existing `subscription_required_modal_opened` PostHog event with the new `reason: 'deep_link'`. Ineligible-click bounce rate is derivable from the autocaptured pageview URL (`?pricing=…`), so no new event plumbing. ## Stacking / dependencies This feature needs two sibling stacks off `main`: - **FE-934** (`#12666`, base of this PR) — the `UnifiedPricingTable` + `showPricingTable({ planMode })`. - **FE-770** (`#12829`) — the `canManageSubscriptionLifecycle` gate. **Merged into this branch**, so the diff against the FE-934 base includes FE-770's changes until it lands. Review the single `feat(billing): deep link…` commit. Once both land on `main`, rebase onto `main` and the diff collapses to just this feature. Do not merge before FE-770 and FE-934. Post-Billing-V1 follow-up. End-state: swap the FE original-owner heuristic for the BE workspace-level `is_original_owner` flag when it lands (removes the members-fetch). ## Tests - Unit (`usePricingTableUrlLoader.test.ts`, 12 cases): opens for an original owner; `team`/`personal` tab preselect; silent no-op + param-strip for a member/promoted owner; proves the gate is read only after `fetchMembers` resolves; preserved-query restore; empty/non-string/absent/unrecognized param; members-fetch failure strips+clears without opening. - E2E (`browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts`, `@cloud`, 4 cases, verified locally): personal owner opens + URL stripped; `?pricing=team` lands on the active Team tab; team original owner opens (real `is_original_owner` + email gate); team member is a silent no-op + URL stripped. - Typecheck + related unit suites (`useSubscriptionDialog`, `useWorkspaceUI`, `teamWorkspaceStore`) green. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
988dc71955 |
feat(workspace): redesign Members invite UI to DES-186 (FE-768) (#12759)
## Summary
Redesigns the Settings ▸ Workspaces Members invite flow to DES-186:
email-chips invite dialog, Resend/Cancel pending-invite actions, and
explicit team-plan gating.
## Changes
- **What**:
- `InviteMemberDialogContent`: single-email link-generation flow →
comma/Enter/paste-separated email chips (`TagsInput`); batch
`createInvite` via `Promise.allSettled` — on partial failure, failed
emails stay as chips with an error toast; success shows an Invited
state.
- `PendingInvitesList`: inline Copy-link/Revoke buttons → `⋯` overflow
menu with **Resend invite** / **Cancel invite**. New
`teamWorkspaceStore.resendInvite` issues the fresh invite *before*
revoking the old one, so a failed resend never destroys the original.
- FE-built invite links removed
(`getInviteLink`/`createInviteLink`/`copyInviteLink`,
`PendingInvite.token`) — invite delivery is BE email per DES-186.
- New `useTeamPlan` composable: explicit `isOnTeamPlan` (seat-based
`maxSeats > 1` proxy until BE-1254 exposes the plan signal) replaces
`isSingleSeatPlan` branching in
`WorkspacePanelContent`/`useMembersPanel`.
- Personal/no-team-plan state per Figma 2993-14604: "To add teammates,
upgrade your plan." banner + **Upgrade to Team**; "Need more members?
Contact us" footer; upsell dialog copy → Team-plan framing; members sort
by Role column (was Join date).
- **Header layout per design (2nd commit)**: `[Search][Invite +][⋯
workspace menu]` moved from the dialog tab row into the members card
header row (the "N of M members" row) — the tab row right side is empty
in every DES-186 frame. Workspace menu extracted to
`WorkspaceMenuButton.vue`.
- **Single-member gating per design annotations**: when the owner is
alone — search hidden ("Show if more than 1 members"), Active/Pending
segmented tabs hidden, role column hidden, and the Members tab label
drops its count ("Remove '(0)' as well if it's just 1 member"). Tabs
reappear if pending invites exist so they stay manageable.
- **Breaking**: `teamWorkspaceStore` invite-link API removed (no
external consumers found in-repo).
## Review Focus
- Resend semantics (create-first, then revoke) and pending-invites state
update in `teamWorkspaceStore.resendInvite`.
- Invite-link removal assumes BE sends invite emails (DES-186 annotation
"Resends the invite email"). If BE email delivery is not yet live, this
PR should wait on that confirmation.
- Gating matrix: team-active owner → enabled; team at seat cap →
disabled+tooltip; non-team plan → upsell dialog; personal workspace →
disabled.
- Workspace `⋯` menu (Edit/Delete/Leave) now lives only on the Members
tab card header, matching DES-186 — the Plan & Credits tab no longer
exposes it (design shows the plan-card `⋯` there instead, shipped via
FE-964/FE-768b).
## Screenshots
Captured on `local.comfy.org` dev (cloud-prod backend, authenticated
session; team-plan states use client-side XHR-level API stubs for
subscription/members/invites since the test workspaces are
unsubscribed).
| Flow | Before (main) | After (this PR) |
|---|---|---|
| Members list — team plan | <img width="400" alt="before members"
src="https://github.com/user-attachments/assets/be214b95-4783-47ff-9539-59c8a33b5eb9"
/> | <img width="400" alt="after: Search/Invite/menu in the card header
row, Role column, design demo data"
src="https://github.com/user-attachments/assets/841c89d5-2a29-4eed-9c72-4a5ee8bee9f4"
/> |
| Pending invites — row actions | <img width="400" alt="before pending:
inline copy-link/revoke icons"
src="https://github.com/user-attachments/assets/1703850e-86bc-4735-81b3-7530c01ad46f"
/> | <img width="400" alt="after pending: overflow menu with
Resend/Cancel invite"
src="https://github.com/user-attachments/assets/9b1bbe03-d82b-4cf6-86f2-e01d7b898ae7"
/> |
| Team workspace with 1 member | (same chrome as members list: search +
tabs always shown) | <img width="400" alt="after: no search/tabs/role,
tab label without count, Invite + menu in card row"
src="https://github.com/user-attachments/assets/22c7f68a-eb0f-49a9-a203-516cd9b7e02d"
/> |
| Invite dialog | <img width="400" alt="before: single email, Create
link"
src="https://github.com/user-attachments/assets/c19d8bbb-feb5-4fe4-8541-6d52e6ab6600"
/> | <img width="400" alt="after: comma-separated email chips"
src="https://github.com/user-attachments/assets/101fdc7b-d6e0-4f7d-8966-894bbf16b4aa"
/> |
| Invite dialog — submit result | (copies a link) | <img width="400"
alt="after: Invited success state"
src="https://github.com/user-attachments/assets/f539a88c-0250-434c-bf4a-6ce714b30398"
/> |
| Upsell — invite without team plan | <img width="400" alt="before:
subscription required, Creator plan copy"
src="https://github.com/user-attachments/assets/bb2cb9fd-f298-4cb0-b39a-6d59061dcea1"
/> | <img width="400" alt="after: Team plan required, Upgrade to Team"
src="https://github.com/user-attachments/assets/45170ed5-63bd-469a-af12-197a8b7e09ee"
/> |
| Personal workspace — Members tab | (create-workspace hint text) | <img
width="400" alt="after: upgrade banner + disabled Invite"
src="https://github.com/user-attachments/assets/a0ae664b-2d20-4d87-900d-7a36872ecde3"
/> |
Linear:
[FE-768](https://linear.app/comfyorg/issue/FE-768/updates-to-workspaces-tab-of-settings)
(Members half; Plan & Credits panel ships separately stacked on FE-964)
|
||
|
|
52d430d1b6 |
fix(billing): repoint direct-bypass billing consumers to the facade (B3) (FE-933) (#12643)
## What **B3 — Repoint direct-bypass billing consumers to the facade.** Billing data was read from the legacy `useSubscription` store / `authStore` directly (empty or personal-only for team workspaces) instead of the workspace-aware `useBillingContext` facade. FE-933 (parent FE-903). > **Stacked on #12622 (B2 / FE-904)** — depends on the facade `tier` / `renewalDate` fields added there. Base is the B2 branch; retarget to `main` once B2 merges. ## Repointed consumers - **T3 — `SubscribeButton.vue`**: `subscribe_clicked` telemetry `current_tier` ← facade `tier` (was wrong/empty for team users) - **T4 — `PostHogTelemetryProvider.ts`**: PostHog `subscription_tier` person property ← facade `tier` watch (tier-segmented analytics was polluted for team users) - **T5 — `FreeTierDialogContent.vue`**: next-refresh date ← facade raw ISO `renewalDate`, formatted at the display site (the line silently disappeared for team users) - **`useSubscriptionActions.handleRefresh` + `SettingDialog` credits-nav**: balance refresh ← facade `fetchBalance()` (was legacy `/customers`-only `authActions.fetchBalance`) - **`CurrentUserPopoverLegacy.vue`**: tier badge / balance / skeleton / refreshes ← facade (`tier`, `balance`, `isLoading`, `fetchStatus`, `fetchBalance`); tier name via shared `useWorkspaceTierLabel` instead of a duplicated mapping - **`PricingTable.vue`**: `isActiveSubscription` / `isFreeTier` / `tier` / yearly-vs-monthly ← facade; the billing-portal flow (`accessBillingPortal` deep-links + proration) is intentionally unchanged — facade `manageSubscription` is not behavior-identical ## Out of scope (triaged) - `TopUpCreditsDialogContentLegacy` / `SubscriptionPanelContentLegacy` / `useSubscriptionDialog` / cancellation watcher — legacy-mode-only surfaces decommissioned by B1 (FE-966); repointing is churn, and `useSubscriptionDialog` would create a legacy↔facade cycle - `LegacyCreditsPanel` / `UserCredit` — deleted/orphaned by FE-964 (#12734); its successor `CreditsPanel.vue` keeps an `authStore.lastBalanceUpdateTime` watch (no facade equivalent yet) — follow-up after FE-964 lands ## Known semantic deltas (intentional, match shipped facade consumers) - Balance-refresh failures no longer toast: legacy `authActions.fetchBalance` wrapped errors with a toast; facade `fetchBalance` rejections are void-ed, same as `CurrentUserPopoverWorkspace` / `SubscriptionPanelContentWorkspace`. Facade-level error surfacing is a follow-up. - Popover skeleton keys on facade `isLoading` (init-time) rather than per-fetch `isFetchingBalance`, matching the workspace popover. ## Tests - New behavioral coverage: FreeTier renewal-date render/disappear, popover tier badge + balance from facade, current-plan highlight from facade tier+duration, facade-vs-legacy fetchBalance tripwire, PostHog `subscription_tier` from facade tier. - Local gates clean (typecheck / lint / format / dead-code); touched unit files 71/71 pass. ## E2E coverage Browser regression tests live in the stacked #12760 (`billingFacadeConsumers.spec.ts`, `@cloud`): avatar popover tier badge + balance, and the free-tier dialog renewal-date line (T5) rendered from the facade. The team-user telemetry fixes (PostHog person property, telemetry payload) are non-UI observables covered by unit tests that mock only the facade and fail on revert. --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
6068571b35 |
Refactor: Brand node execution and locator IDs (#13071)
## Summary - Brand `NodeExecutionId` and `NodeLocatorId` as distinct required string types. - Route execution/locator ID construction through existing helper functions instead of minting raw strings at call sites. - Update tests and boundary parsing to use branded IDs without conflating them with local `NodeId` values. ## Validation - `pnpm typecheck` - `pnpm test:unit src/types/nodeIdentification.test.ts src/stores/executionStore.test.ts src/renderer/extensions/vueNodes/components/NodeSlots.test.ts src/composables/graph/useErrorClearingHooks.test.ts src/platform/nodeReplacement/missingNodeScan.test.ts -- --runInBand` - `pnpm exec eslint src/types/nodeIdentification.ts src/utils/graphTraversalUtil.ts src/platform/workflow/management/stores/workflowStore.ts src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts src/stores/workspace/favoritedWidgetsStore.ts src/stores/nodeOutputStore.ts src/utils/__tests__/executionErrorTestUtils.ts src/platform/nodeReplacement/missingNodeScan.test.ts src/stores/executionStore.test.ts --cache` Note: full `pnpm lint` timed out after 5 minutes while still in stylelint startup, so targeted lint was run on changed files. ## Open Question - Should root-level node IDs like `1` be considered valid `NodeExecutionId` values, or should `isNodeExecutionId()` require a colon and callers use a separate type/helper for root execution IDs? |
||
|
|
b165b3f999 |
fix: focus keybindings search when opening Manage Shortcuts (FE-845) (#12709)
## Summary Opening the Keybinding panel from the **Manage Shortcuts** button now focuses the **Search Keybindings** field instead of the **Search Settings** field. ## Changes - **What**: The Settings dialog's "Search Settings" input had an unconditional `autofocus`, so opening directly to the keybinding panel always stole focus to the wrong field. Made it conditional (`:autofocus="activeCategoryKey !== 'keybinding'"`) and added `autofocus` to the keybinding panel's own search input. ## Review Focus - `autofocus` maps to the native attribute, which only fires on DOM insertion — flipping the reactive `:autofocus` while navigating between categories inside the dialog will not re-steal focus, so there is no regression for in-dialog navigation. - Added an E2E test verified in both directions: it fails on the original code (Search Settings focused) and passes with the fix (Search Keybindings focused). Fixes FE-845 Co-authored-by: Dante <bunggl@naver.com> |
||
|
|
d7f9754393 |
feat: add bounding boxes and colors widgets (CORE-292) (#12960)
## Summary
Add two reusable node widgets backed by native (non-string) values:
- Bounding boxes editor (BOUNDING_BOXES): draw, select, resize, and
label regions over an optional background image. Value is a native list
of `{ x, y, width, height, metadata }` pixel boxes; the editor works in
normalized space internally and converts at the value boundary,
rescaling when the node's width/height change.
- Colors palette (COLORS): native `string[]` of hex colors, sharing the
PaletteSwatchRow component (usePaletteSwatchRow composable).
Both reactively hide the width/height widgets while a background image
is connected by writing through the widget value store so the Vue node
re-renders.
Some design refer to KJ's node
BE: https://github.com/Comfy-Org/ComfyUI/pull/14537
Screenshot
<img width="3019" height="1470" alt="image"
src="https://github.com/user-attachments/assets/06795772-97e6-4084-9205-e370f955fb28"
/>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
|
||
|
|
48a3ea0e92 |
feat: add HDR/EXR image viewer for SaveImageAdvanced outputs (#13049)
## Summary Browsers cannot render EXR/HDR in <img>, so these outputs showed as broken images. Add a full-screen three.js viewer holding a single WebGL context created on open and released on close, opened via an 'Open in HDR Viewer' action on EXR/HDR outputs in ImagePreview. The layout mirrors the 3D viewer: canvas on the left, grouped controls in a right-hand sidebar. The display pipeline (gamut -> exposure -> linear-to-sRGB -> dither -> clamp, plus clip warnings) is adapted from [HDRView](https://github.com/wkjarosz/hdrview). Source gamut is auto-detected from the EXR chromaticities attribute (Rec.709/Rec.2020) with a manual override. Inspection tools operate on the EXR float data kept on the CPU by EXRLoader: - pixel inspector: hover to read raw RGBA values and coordinates - statistics: min/max/mean/std-dev plus NaN and Inf counts - auto-exposure: set exposure so the max value maps to 1 - channel isolation: view R/G/B/A or luminance individually ## Screenshots (if applicable) https://github.com/user-attachments/assets/22b80718-4b15-41ee-86b5-8fe38a6a82e2 |
||
|
|
a8f8ba7580 |
fix: clamp sidebar tab labels to two lines with tooltip fallback (#12755)
## Summary
Sidebar tab labels no longer overflow the rail: they wrap up to 2 lines
max, then truncate with an ellipsis, with the full name always
recoverable via the hover tooltip (per design spec from Alex Tov in
FE-698).
## Changes
- **What**:
- Labels in `SidebarIcon.vue` now use `line-clamp-2` + `overflow-wrap:
break-word` + `whitespace-normal`, contained within the rail width minus
`--sidebar-padding` so text keeps breathing room from the rail border
(the base Button's `whitespace-nowrap` previously prevented any
wrapping, causing labels like "Input & Output" to be clipped on both
sides)
- Near-fit built-in labels ("Workflows", "Templates", "Shortcuts" —
wider than the floating-mode line) get soft hyphens (``) in their en
label strings, so they break cleanly as "Work-/flows" in floating mode
and render as a single unhyphenated line in connected mode (56px).
`hyphens: auto` can't do this because Chromium skips hyphenation for
capitalized words. Title/tooltip strings are untouched
- Tooltip falls back to the label when an extension registers a sidebar
tab without a tooltip, so clamped text is always recoverable on hover
## Review Focus
- Labels never bleed past the rail or get clipped by the rail's
`overflow-hidden`; long unbroken extension names (e.g.
`WASNodeSuitePreprocessors`) break mid-token across 2 lines + ellipsis,
matching the design mockup
- Soft hyphens live only in `sideToolbar.labels.*`, not in the
title/tooltip keys, so command palette / tooltip text stays clean
- No E2E regression test: the fix is pure CSS layout (line
wrapping/clamping), and per `AGENTS.md` testing guidelines we don't
write tests that depend on non-behavioral styling. The one behavioral
change (tooltip falls back to label) is covered by a unit test in
`SidebarIcon.test.ts`
Fixes
[FE-698](https://linear.app/comfyorg/issue/FE-698/bug-input-and-outputs-text-not-wrapping-in-left-sidebar)
---------
Co-authored-by: Dante <bunggl@naver.com>
|
||
|
|
84319bea13 |
refactor: drop redundant isCloud guards around telemetry calls (#13082)
## Summary
Remove redundant `if (isCloud)` guards around `useTelemetry()?.x()`
calls. `useTelemetry()` already returns `null` in OSS builds, so the
optional-chain calls no-op there — the guards only duplicated that
central contract.
## Changes
- **What**: Drop the `isCloud` guard wrapping telemetry calls across 9
files and remove the 5 now-unused `isCloud` imports (pure dedent —
implementations unchanged). Add two-path (cloud + OSS) characterization
tests for the two previously-uncovered composables
(`useTemplateWorkflows`, `useSubscriptionActions`).
## e2e
In local/OSS mode, useTelemetry() returns null, so no telemetry-related
behavior occurs, and the workflow loads as expected. There are no
local/OSS flow regressions for the exact template workflow paths touched
by the branch.
| before | after |
| -- | -- |
| <img width="1280" height="800" alt="before-01-templates-open"
src="https://github.com/user-attachments/assets/1cccc686-4e3a-4cf0-a578-a653a1383e3c"
/> | <img width="1280" height="800" alt="after-01-templates-open"
src="https://github.com/user-attachments/assets/ff834a58-4375-432a-8cc1-6e04ceeece77"
/> |
| <img width="1280" height="800" alt="before-02-template-loaded"
src="https://github.com/user-attachments/assets/1abd301b-d66d-4819-a0f3-9dff1a1e23b5"
/> | <img width="1280" height="800" alt="after-02-template-loaded"
src="https://github.com/user-attachments/assets/9fbb6903-c085-4744-b683-39b01680c654"
/> |
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Behavior is intended to be unchanged: OSS still no-ops via null
telemetry. Router page-view tracking may run in more build contexts but
remains guarded by optional chaining.
>
> **Overview**
> Removes duplicate **`if (isCloud)`** wrappers around
**`useTelemetry()?.…()`** across onboarding, auth, templates,
subscription UI, and routing. Call sites now rely on
**`useTelemetry()`** returning **`null`** in OSS (optional chaining
stays a no-op there), and several unused **`isCloud`** imports are
dropped.
>
> **`trackPageView`** in the router no longer bails early on cloud-only
or **`window`** checks; it always invokes
**`useTelemetry()?.trackPageView(...)`** on navigation.
>
> Adds characterization tests for **`useTemplateWorkflows`** and
**`useSubscriptionActions`** that assert telemetry fires when the mock
dispatcher is registered and does not when the mock simulates OSS
(**`useTelemetry()` → null**).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
|
||
|
|
07f881fc14 |
feat: float the Media Assets bulk-selection bar (#13043)
## 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>
|
||
|
|
e14b5c6f3f |
feat: wire /api/billing/events into usage history behind teamWorkspacesEnabled (#12955)
## 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>
|
||
|
|
065650b3bf |
fix: open Vue context menu when right-clicking a group (#12971)
## 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> |
||
|
|
e97cca9e4a |
feat: show node preview ghost when adding models from dialog & sidebar (#12765)
## 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 |
||
|
|
49a7b7b558 |
feat(billing): UnifiedPricingTable — one table, personal/team plan toggle (B4 / FE-934) (#12666)
## 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> |
||
|
|
69858538d0 |
Adopt jobs-namespace cancel endpoints in the jobs panel (#12863)
## 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>
|
||
|
|
403353ac77 |
feat: add tab status indicator (running/done/errored) (#10177)
## Summary Adds indicator to show outcome of last job per tab, cleared next time the workflow is activated. ## Changes - **What**: - add workflow status tracking to execution store, handling various events - add icon to tab based on store - handle race condition where job finishes instantly (e.g. invalid workflow or already executed) ## Screenshots (if applicable) https://github.com/user-attachments/assets/8b1d8d8e-57d4-4ac2-9cc3-0d218d6eb0f7 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10177-feat-add-tab-status-indicator-running-done-errored-3266d73d365081a89f5dfd58487bb065) by [Unito](https://www.unito.io) --------- Co-authored-by: bymyself <cbyrne@comfy.org> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
90c523b4a3 |
feat(dialog): flip remaining callers + default renderer to Reka (Phase 6a cutover, stacked on #12848. 6a-2) (#12593)
## 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" /> |
||
|
|
90210292d7 |
fix: add explicit return types for composables to fix TS declaration errors (#13034)
## Summary - Fix TS2742 and TS4060 errors during .d.ts generation in the types build - Add explicit interface definitions and return type annotations for composables ## Changes - `confirmDialog.ts`: explicit `DialogInstance` return type - `useImageCrop.ts`: export `ResizeHandle` interface - `useAbsolutePosition.ts`: `UseAbsolutePositionReturn` interface - `useDomClipping.ts`: `UseDomClippingReturn` interface - `useNodePreviewAndDrag.ts`: `UseNodePreviewAndDragReturn` interface - `i18n.ts`: explicit type annotations for exported functions ## Test plan - [ ] Types build succeeds: `pnpm build:types` - [ ] No new lint errors: `pnpm lint` - [ ] Type check passes: `pnpm typecheck` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Connor Byrne <c.byrne@comfy.org> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
bc885f383c |
Decouple run telemetry context from providers (#12925)
## 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. |
||
|
|
7f25d28b71 |
Filter canvasOnly non-preview widgets in editor (#12957)
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. |
||
|
|
ab6c44aabf |
feat: remove deprecated group nodes, auto-convert to subgraphs on load (#12931)
## 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> |
||
|
|
4c870d84ed |
Fix disabling of linked widgets in props panel (#12896)
| Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/9d602ee3-ff10-48b9-95ca-4c7f5ca57a45" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/36e96aff-60ec-4f8c-b7c9-b4d68e03884c" />| Making reactivity function is a little bit clunkier than I would like, but it'll get simplified in the future by east coast swing |
||
|
|
6850d22d99 |
Redesign error overlay count and toast behavior (#12871)
## 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" /> |
||
|
|
5a846db6cf |
feat(billing): role-aware run-lock for cancelled/inactive team plans (FE-978) (#12786)
## 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 |
||
|
|
e994e4df58 |
refactor: store-backed WidgetId subgraph host widgets; delete widgetValueIO layer (#12617)
## 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> |
||
|
|
c36da042d0 |
Redesign error tab cards with summary hero and unified sections (#12828)
## 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> |
||
|
|
4b979f4ad0 |
feat(dialog): migrate mask editor + 3D viewer dialogs to the Reka renderer (FE-578) (6a -1) (#12848)
## Summary Splits the **heavy, hard-to-test surface** out of the Phase 6 dialog cutover (#12593) into its own independently reviewable, independently testable PR — per @jtydhr88's review feedback that #12593 bundled too many concepts (3D, mask editor, and the renderer cutover) to test thoroughly at once. This PR migrates only the four style-string dialog callers that carry **Playwright screenshot baselines** and **maximize behavior** — the mask editor and the 3D viewers — plus the shared dialog infrastructure they need. **#12593 is rebased on top of this PR** and now contains only the renderer cutover. 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) ## Why this is safe to land alone **The global renderer default stays `'primevue'`.** Every caller migrated here sets `renderer: 'reka'` explicitly, and the infra additions are purely additive. So no other dialog changes behavior and there is no half-migrated state — the default flip and the remaining caller migrations all live in the stacked cutover (#12593). ## Changes **Heavy callers → `renderer: 'reka'` + `size`/`contentClass`:** - Mask editor (`useMaskEditor.ts`) — `mask-editor-dialog` hook class moves to `contentClass` so `browser_tests` selectors keep working unchanged - 3D viewers ×4 (`ViewerControls.vue`, `AssetsSidebarTab.vue`, `JobHistorySidebarTab.vue`, `load3d.ts`) **Infra to reach Reka parity (additive):** - `dialogStore`: `headerClass`/`bodyClass`/`footerClass` (Reka-path analogues of `pt.header`/`pt.content`/`pt.footer`) - `GlobalDialog`: forward the section classes; merge `bodyClass` into the body wrapper - `DialogContent`: maximized re-asserts its dimension classes after the caller's `contentClass` so maximize wins, mirroring `.p-dialog-maximized` `!important` - `tailwind-utils`: teach tailwind-merge the `max-h-none` class so maximize can release the caller's `max-height` - `rekaPrimeVueBridge`: keep a backgrounded reka dialog from dismissing when a stacked dialog opens on top of it - `maskeditor/useKeyboard`: capture keydown so undo/redo survive the Reka focus trap ## Quality gates - [x] `pnpm typecheck` — clean - [x] `pnpm lint` / `pnpm format` — clean (lint-staged) - [x] `GlobalDialog.test.ts` — 25 passing (incl. new section-class + maximize-override + stacked-dismiss tests) - [x] Changed-source unit tests (`useMaskEditor`, `useKeyboard`, `ViewerControls`, `load3d`) — 77 passing - [ ] CI Playwright — mask editor baselines refreshed for the Reka chrome (`browser_tests/tests/maskEditor.spec.ts-snapshots/*`) ## Out of scope (stacked in #12593) The renderer cutover: `showConfirmDialog` flip, remaining `dialogService`/composable callers (signin, top-up, workspace, subscription, publish, share, …), **the `createDialog` default flip to `'reka'`**, e2e selector retargeting, and the `ConfirmationService` removal. PrimeVue branch deletion remains Phase 6b. ## 📸 Screenshots — before (PrimeVue) → after (Reka) Captured via Chrome DevTools against this branch in cloud mode (`cloud.comfy.org` backend), with an input image / `cube.obj` loaded. Only the dialog **chrome** migrates (PrimeVue `Dialog` → Reka `DialogContent`); the editor/viewer content is unchanged. ### Mask editor (`useMaskEditor`) | Before (PrimeVue) | After (Reka) | |---|---| | <img width="430" alt="mask editor before" src="https://github.com/user-attachments/assets/267e63b5-0832-409e-9c41-edf5ff96561f" /> | <img width="430" alt="mask editor after" src="https://github.com/user-attachments/assets/073cd824-8b01-4c07-99e1-a3a054906c7a" /> | ### 3D viewer (`load3d` / `ViewerControls`) | Before (PrimeVue) | After (Reka) | |---|---| | <img width="430" alt="3D viewer before" src="https://github.com/user-attachments/assets/17b2cd2f-18e4-4d9a-9e0e-80ef833db216" /> | <img width="430" alt="3D viewer after" src="https://github.com/user-attachments/assets/9e20a7a5-4d22-40e6-8fa2-ece58b6e4d20" /> | ### 3D viewer — maximized (maximize-wins dimension re-assertion in `DialogContent`) | Before (PrimeVue) | After (Reka) | |---|---| | <img width="430" alt="3D viewer maximized before" src="https://github.com/user-attachments/assets/b705a4d5-4657-41ad-b6f3-95e54494ac9b" /> | <img width="430" alt="3D viewer maximized after" src="https://github.com/user-attachments/assets/188de427-ab58-45a9-8666-967b2908c320" /> | |
||
|
|
e832380c33 |
refactor(billing): unify cancel-status polling into billingOperationStore (B8 / FE-970) (#12788)
## Motivation - **Why B8 exists**: two divergent BillingOp pollers poll the same op-status endpoint with different policies (hand-rolled 2× / 5s cap / 30 attempts vs the store's 1.5× / 8s / 120s). Once B1 (FE-966) routes personal flows through the facade, a single cancel op would be polled by **both** — duplicate requests, with two timeout policies racing on the same state. - **The latent bug — silent failure on a money path**: the bespoke poller treated timeout as a silent return, so `cancelSubscription` resolved and the dialog showed "cancelled successfully" while the backend op could still be pending or fail later. The root cause is structural: the poll outcome was fire-and-forget — no caller consumed it, so there was no channel through which a failure could surface. - **The fix — outcome as an awaited contract**: `startOperation` returns the terminal outcome as a `Promise<BillingOperation>`; `cancelSubscription` awaits it and throws on any non-`succeeded` terminal. Every outcome must now flow through the caller, making silence structurally impossible (timeout/failure → error toast). - **The trade-off this creates**: "the terminal promise always settles" becomes load-bearing — the dialog's loading state hangs on it, and a never-settling path would be worse than the old silence (a permanently locked dialog). The terminal-promise hardening below, and its regression tests, enforce that guarantee. ## Summary Two divergent BillingOp pollers (hand-rolled `useWorkspaceBilling.pollCancelStatus` vs `billingOperationStore`) are unified into one — cancel-status polling now runs through `billingOperationStore`; `pollCancelStatus` and its bespoke backoff/timers/state are removed. ## Changes - **What**: `billingOperationStore` gains `'cancel'` in `OperationType`; `startOperation` now returns `Promise<BillingOperation>` resolving on the terminal outcome (existing subscription / topup callers unaffected — fire-and-forget preserved). `useWorkspaceBilling.cancelSubscription` awaits the shared poller and throws on any non-`succeeded` terminal. One backoff config = store's 1.5× / 8s / 120s. - **Terminal-promise hardening**: the terminal promise always settles — a rejected post-success status/balance refresh no longer leaves `cancelSubscription` hanging with the dialog locked open (`Promise.allSettled`), and a duplicate `startOperation` for an in-flight op joins the same terminal promise instead of resolving instantly with a `pending` snapshot (which the cancel path would read as failure). - **Breaking**: none — the `cancelSubscription(): Promise<void>` contract is unchanged for `CancelSubscriptionDialogContent` / `useBillingContext`. ## Review Focus - **Intentional behavior change**: a cancel **timeout** is now a terminal outcome that **throws** (`billingOperation.cancelTimeout`), so the dialog surfaces an error toast — instead of the old silent success-ish return (which could show success while the op was still pending). Success / failure semantics otherwise preserved (success → status refresh + `isSubscribed: false`; failure → throw with message). - FE-only refactor (B8); cleanest after FE-904 but independent of it. Relates FE-932 (shared status-refresh path). ## Tests 90 unit tests green across the four affected suites (fake timers for all polling): - **`billingOperationStore.test.ts` (33)** — cancel terminal outcomes (succeeded / failed / timeout) resolve the awaited promise with the right status + i18n message; cancel suppresses the store's toasts and settings-dialog side effects; success refreshes status/balance and sets `isSubscribed: false`; backoff progression, 8s cap, 2-min timeout; transient poll errors keep polling. Regression guards for the hardening: post-success refresh failure still settles the terminal promise (reproduced as a hang before the fix), and a duplicate `startOperation` joins the in-flight terminal promise instead of resolving with a `pending` snapshot. - **`useWorkspaceBilling.test.ts`** — `cancelSubscription` drives the shared poller; throws the op error on `failed`, throws on `timeout`, falls back to a generic message when `errorMessage` is absent; a failing cancel API propagates without starting the poller. - **`CancelSubscriptionDialogContent.test.ts` (8)** — locks the dialog half of the behavior change: a rejected `cancelSubscription` shows an error toast and keeps the dialog open; success closes the dialog with a success toast. - **`useSubscriptionCheckout.test.ts`** — unchanged, confirms fire-and-forget callers are unaffected. Both hardening regressions were proven red→green locally: before the fix the terminal-hang test timed out at 5s and the duplicate-start test resolved `pending`. Gates: vue-tsc / oxlint type-aware / eslint / oxfmt clean; full CI green including all Playwright shards and the cloud project. The existing `cancelSubscriptionDialog.spec.ts` e2e (@ui, open/close/escape flows) is unchanged and green; cloud-backend e2e for billing flows is tracked separately in FE-991. Fixes FE-970 |
||
|
|
6d43320b93 |
Simplify missing model error presentation (#12793)
## Summary Simplifies the Missing Models error card as the fifth slice of the catalog-driven error-tab redesign. This PR is intentionally larger than the previous slices because Missing Models is the only remaining error type where the card UI, OSS download flow, Cloud import flow, and shared model-import dialog all have to move together to preserve the resolution path. The high-level goal is to make Missing Models behave like the other simplified error cards: show the exact missing item, show the affected nodes, keep locate actions predictable, and only expose actions that can actually resolve the problem. This follows the staged error-tab cleanup plan: 1. #12683 refined validation, runtime, and prompt error presentation. 2. #12705 simplified missing media error presentation. 3. #12735 simplified missing node pack error presentation. 4. #12768 simplified swap node error presentation. 5. This PR simplifies missing model presentation and the model-import handoff. ## Why This PR Is Larger Missing Models has more resolution paths than the previous error groups: - OSS can refresh model state, download individual models, and download all available models. - Cloud cannot download directly from the panel; it resolves supported rows through the model import dialog. - Some Cloud rows cannot be resolved through import at all because the node/widget cannot consume imported model assets. - Importing from Cloud needs to know the originating missing-model row so it can lock the expected model type and apply the imported model back to the affected widgets. - Already-imported files can still be unusable if they were imported under a different model type than the missing node expects. Because of those constraints, splitting the card layout from the dialog handoff would leave either a misleading Import button or an import dialog that does not know what it is resolving. This PR keeps that behavior in one reviewable unit. ## User-Facing Behavior ### Shared Missing Models Card - Replaces the older grouped presentation with compact model rows. - Shows each missing model as the primary row label. - Shows model metadata as a smaller sublabel instead of using large section headers. - Keeps locate-node controls visually consistent with the other simplified error cards. - Keeps rows expandable when multiple nodes reference the same missing model. - Shows the affected node rows under expanded models. - Allows single-reference rows to locate the affected node without rendering an extra duplicate child row. - Keeps unknown rows visible, including their affected nodes, instead of silently hiding them. - Removes the old library-select UI from the Missing Models card. ### OSS Behavior - Keeps the refresh action available from the Missing Models group header. - Keeps individual Download actions for downloadable models. - Moves file size out of the Download button label and into the row sublabel. - Keeps Download all when multiple downloadable models are available. - Places Download all at the bottom of the card rather than competing with the group header. - Leaves rows without a download URL as non-downloadable instead of rendering a broken action. ### Cloud Behavior - Shows Import only for missing models that can be resolved by importing a model asset of the required type. - Separates models that cannot be resolved through Cloud import into an Import Not Supported section. - Gives unsupported rows a direct explanation: nodes referencing those models do not support imported models, so users need to open the node and choose a supported built-in model or replace the node with a supported loader. - Treats unknown model type/directory as unsupported for Cloud import, because the import dialog cannot lock a valid model type and the node cannot safely consume the imported asset. - Keeps affected nodes visible in the unsupported section so users still have a path to locate and replace the node manually. ## Cloud Import Dialog Changes The shared model import dialog now accepts missing-model context when opened from the Missing Models card. When that context is present: - The dialog shows which missing model will be replaced. - The dialog lists the affected node/widget references that will be updated. - The model type selector is locked to the required model directory/type. - The Back/import-another path is disabled when it would break the targeted missing-model flow. - Import progress can be associated with the originating missing-model row. - After import completion, matching missing-model references are applied automatically where possible. - If the selected file is already imported under an incompatible model type, the dialog shows a targeted failure state explaining why this import cannot resolve the missing model. This keeps the generic import dialog reusable while adding only the context-specific behavior needed for Missing Models. ## Implementation Notes - `MissingModelCard.vue` owns the card-level grouping and OSS/Cloud section decisions. - `MissingModelRow.vue` owns per-model row rendering, expansion, locate actions, import/download actions, and row-level progress states. - `useMissingModelInteractions.ts` remains the interaction layer for locating nodes and applying resolved model selections. - `UploadModelDialog.vue`, `UploadModelConfirmation.vue`, `UploadModelFooter.vue`, `UploadModelProgress.vue`, and `useUploadModelWizard.ts` receive the missing-model context needed by the Cloud import handoff. - `MissingModelLibrarySelect.vue` is removed because the simplified card no longer exposes that inline selection path. - Locale and selector changes are limited to the new simplified row/section states and removed unused Missing Models strings. ## Tests Added / Updated - Unit coverage for Missing Models card grouping and row states. - Unit coverage for importable vs unsupported Cloud rows. - Unit coverage for model row expansion, locate actions, progress display, and action availability. - Unit coverage for upload confirmation/footer/progress behavior when a missing-model context is present. - Unit coverage for incompatible already-imported model handling. - E2E coverage for OSS Missing Models presentation. - E2E coverage for mode-aware Missing Models interactions. - Cloud E2E coverage for importable rows vs Import Not Supported rows. - Cloud E2E coverage for opening the import dialog with missing-model replacement context. ## Review Focus - Cloud import eligibility: unsupported or unknown model rows should not expose Import as if the row can be resolved automatically. - Missing-model context in the import dialog: the required model type should be locked, and the affected node/widget references should be clear. - OSS parity: OSS should keep refresh, individual Download, and Download all while visually matching the simplified Cloud card where possible. - Narrow side panel behavior: row labels may wrap, but link, primary action, and locate controls should not overlap. - Scope boundaries: this PR intentionally does not redesign Missing Node Pack / Swap Node / Missing Media again; visual parity issues shared across those cards can be handled in a follow-up unification pass if needed. ## Validation - `pnpm format` - `pnpm lint` - `pnpm typecheck` - Related unit tests: 8 files / 84 tests passed - `pnpm build` - OSS Missing Models E2E: `errorsTabMissingModels.spec.ts` passed, 8/8 - Mode-aware Missing Models E2E subset passed, 11/11, excluding unrelated local paste clipboard cases - `pnpm build:cloud` - Cloud Missing Models E2E: `errorsTabCloudMissingModels.spec.ts` passed, 3/3 - Final Claude review: no Blocker or Major findings ## Breaking / Dependencies - Breaking: none. - Dependencies: none. ## Screenshots OSS <img width="575" height="393" alt="스크린샷 2026-06-12 오전 12 25 27" src="https://github.com/user-attachments/assets/f5c44f95-711a-4d3d-99bd-f39ac2bb2012" /> <img width="659" height="351" alt="스크린샷 2026-06-12 오전 12 24 37" src="https://github.com/user-attachments/assets/4bb65a47-c1aa-408b-836b-a1998412f815" /> Cloud <img width="688" height="357" alt="스크린샷 2026-06-12 오전 12 23 59" src="https://github.com/user-attachments/assets/9330a7e7-9f22-420f-82b3-dde0fb2b3dd1" /> <img width="531" height="437" alt="스크린샷 2026-06-12 오전 12 21 13" src="https://github.com/user-attachments/assets/734bd911-f6f7-4872-8868-bb927ddeedd8" /> New import model flow https://github.com/user-attachments/assets/c094c670-62b9-47ce-bfe1-2d09f4f7359d |
||
|
|
b9112f9bd7 |
feat(load3d) FE-999: export point cloud / splat files as-is (#12810)
## Summary PLY, SPZ, SPLAT and KSPLAT have no THREE.js exporter, so the 3D node now offers them as export options only when the loaded file already uses that format, and exports the original file unchanged instead of attempting a conversion. Mesh files keep the convertible GLB/OBJ/STL/FBX set. Splat models (spz/splat/ksplat and gaussian-splat ply) previously had exportable: false, which hid the export UI entirely; enable it so these formats can be downloaded as-is. Also fixes STL export dropping its originalURL fast-path arg. ## Screenshots (if applicable) https://github.com/user-attachments/assets/8417c697-eb25-4cfb-af44-3855b814fa5d |
||
|
|
1d5801d6ef |
feat: track funnel telemetry attributes (#12778)
## Summary Adds the frontend telemetry attribution needed to analyze settings, app-mode, and sharing funnel usage for MAR-321: re-enables three funnel events that were disabled by default, attaches app-mode/view-mode/dock-state context to UI click, run, and share events, and adds a per-session `shell_layout` snapshot plus right-side-panel toggle tracking. ## Changes - **What**: - Removes `setting_changed`, `template_filter_changed`, and `ui_button_click` from the code-default `DEFAULT_DISABLED_EVENTS` lists in the Mixpanel and PostHog providers, so these events now send by default (see deployment note). - `ui_button_click` now requires an `element_group`; all call sites are tagged (`sidebar`, `queue`, `actionbar`, `breadcrumb`, `error_dialog`, `errors_panel`, `graph_menu`, `graph_node`, `selection_toolbox`, `node_library`, `workflow_actions`, `cloud_notification`, `app_mode`, `top_menu`, `right_side_panel`) and the GTM provider forwards the field. - Run events (`run_button_clicked`, GTM `run_workflow`) now carry required `view_mode`/`is_app_mode` plus a new `dock_state` (`docked`/`floating`), read from the `Comfy.MenuPosition.Docked` localStorage key by a new `getActionbarDockState()` util. - Share funnel events (`share_flow`, `share_link_opened`, `shared_workflow_run`) now carry required `view_mode`/`is_app_mode`. A new `useShareFlowContext()` composable dedupes the source/view-mode context across the share dialog, URL copy field, and `useShareDialog`. GTM `share_flow` forwards the new fields and still omits `share_id`. - `shared_workflow_run` attribution is snapshotted onto the queued job at queue time, so switching app/graph mode while a job runs no longer misattributes the completion event (falls back to live values when no snapshot exists). - New `shell_layout` event fired once per session at graph-ready (cloud only): `view_mode`, `is_app_mode`, `dock_state`, `actionbar_position`, `active_sidebar_tab`, `right_side_panel_open`, `bottom_panel_open`, `open_workflow_tabs`. Forwarded by Mixpanel and PostHog; not sent to GTM. - The right side panel open button (top menu) and close button now fire `ui_button_click` (`right_side_panel_opened`/`right_side_panel_closed`), covering the panel open-rate gap. - **Dependencies**: None. ## Review Focus - `view_mode`/`is_app_mode` changed from optional to required (typed as `AppMode`) on run/share metadata — check no call sites were missed. - The queue-time snapshot in `executionStore` (`queuedJob.viewMode ?? mode.value`) and its regression test. - Share IDs remain limited to the providers/events that already carry share attribution (GTM still strips `share_id`). - `shell_layout` cadence is once per session (graph-ready idle callback), matching the gap analysis's "session snapshot" wording. Linear: MAR-321 Validation: - `pnpm test:unit src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts src/platform/telemetry/utils/getShellLayoutSnapshot.test.ts src/platform/workflow/sharing/components/ShareWorkflowDialogContent.test.ts src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts src/stores/executionStore.test.ts src/components/TopMenuSection.test.ts src/components/graph/selectionToolbox/InfoButton.test.ts src/components/rightSidePanel/errors/useErrorActions.test.ts src/views/GraphView.test.ts` - `pnpm typecheck` - `pnpm lint` - `pnpm knip` - `git diff --check` ## Deployment note `telemetry_disabled_events` is currently unset in the prod/staging/test dynamicconfig rows, so the code-default change here is what enables these events. The remote value remains available as a kill switch, but it **replaces** the code defaults rather than merging: if ops sets it to re-disable an event, the list must include every event that should stay disabled (`tab_count_tracking`, `node_search`, `node_search_result_selected`, `help_center_*`, `workflow_created`), not just the new ones. |